identity-service
OpenID Connect provider. Device-bound sessions, TOTP second factor, sign-in through external providers, account deletion under KVKK and GDPR.
Issues the tokens and keeps the sessions the other services rely on.
- A session is bound to the device it was opened on; a revoked session keeps the time and the reason.
- Second factor with TOTP.
- External sign-in through one adapter per provider; a provider is added after a sign-in against the real provider.
- The user deletes the account; the other services delete their data on the
identity.user.deletedevent. - Integration tests run on Postgres and Redis; the load tests are below.
- Deployed to AWS on demand from the infrastructure repository and removed after use.
Load tests
The infrastructure repository creates the AWS instances for a run: one machine with nginx, the service, PostgreSQL 18, Redis and Mailpit, and a load generator in the same subnet. Each step runs three times for 60 s; the latency charts show the median of the three p99s. Capacity is the highest step that stays under the limit without the server falling behind.
The current version on a c7a.large, 2 cores:
- 100 password logins/s with p99 under 250 ms.
- 1,200 token refreshes/s with p99 under 50 ms, nginx reaching the service over a Unix socket.
- One login per 144 refreshes held at 1,200 requests/s: about 720,000 active users who refresh once every 600 s.
The first campaign compared 2 and 4 cores:
- c7a.large, 2 cores
- c7a.xlarge, 4 cores
- limit, 250 ms
p99, ms
logins per second
| logins per second | p99, ms |
|---|---|
| 17 | 28.8 |
| 35 | 29.8 |
| 48 | 39.8 |
| 55 | 52.6 |
| 62 | 82.4 |
| 69 | 115.4 |
| 76 | 136.3 |
| logins per second | p99, ms |
|---|---|
| 34 | 23.4 |
| 69 | 28.6 |
| 96 | 33.3 |
| 110 | 38.4 |
| 123 | 45.5 |
| 137 | 55.5 |
| 151 | 68.4 |
Capacity 76 logins/s on 2 cores and 151 on 4: at the next step the server fell behind. Every login verifies an Argon2id hash, so login capacity grows with the cores.
- c7a.large, 2 cores
- c7a.xlarge, 4 cores
- limit, 50 ms
p99, ms
refreshes per second
| refreshes per second | p99, ms |
|---|---|
| 237 | 6 |
| 474 | 12.3 |
| 663 | 20.8 |
| 758 | 27 |
| 852 | 33.2 |
| 947 | 42.6 |
| 1,042 | 49.9 |
| 1,184 | 92.8 |
| refreshes per second | p99, ms |
|---|---|
| 463 | 4.3 |
| 925 | 6.1 |
| 1,295 | 8.9 |
| 1,480 | 11.5 |
| 1,665 | 15.7 |
| 1,850 | 23.2 |
Capacity 1,042/s on 2 cores and 1,850/s on 4. On 2 cores the whole machine was 96 % busy at the ceiling: nginx, PostgreSQL and Redis share the cores with the one Node.js process.
- TCP
- Unix socket
- limit, 50 ms
p99, ms
refreshes per second
| refreshes per second | TCP | Unix socket |
|---|---|---|
| 300 | 6 | 6.1 |
| 600 | 12.9 | 13.3 |
| 840 | 24.6 | 21.7 |
| 960 | 29.6 | 26.7 |
| 1,080 | 37 | 32.1 |
| 1,200 | 50.9 | 38.6 |
| 1,320 | 72.4 | 58.9 |
Capacity 1,080/s over TCP and 1,200/s over the socket, 11 % more. docker-proxy, which copies every byte between nginx and a published port, used close to a tenth of a core at the ceiling; over the socket that work is gone.
Past login capacity, the first version kept a queue of every login waiting for its hash. The current one bounds it: past capacity a login is refused at once with 503 and Retry-After, and a login still waiting after one second is refused instead of verified.
- Unbounded queue
- Bounded queue
- twice capacity
logins that succeeded, %
seconds from the start of the overload, by when a login was sent
| seconds from the start of the overload, by when a login was sent | Unbounded queue | Bounded queue |
|---|---|---|
| -15 | 100 | 100 |
| -10 | 100 | 100 |
| -5 | 100 | 100 |
| 0 | 100 | 50.1 |
| 5 | 67.7 | 52.6 |
| 10 | 0 | 54 |
| 15 | 0 | 54.3 |
| 20 | 0 | 55.7 |
| 25 | 0 | 55.1 |
| 30 | 0 | 55.5 |
| 35 | 0 | 55.8 |
| 40 | 0 | 55 |
| 45 | 0 | 54.2 |
| 50 | 0 | 55.7 |
| 55 | 0 | 57.3 |
| 60 | 0 | 100 |
| 65 | 0 | 100 |
| 70 | 0 | 100 |
| 75 | 0 | 100 |
| 80 | 0 | 100 |
| 85 | 0 | 100 |
| 90 | 0 | 100 |
| 95 | 0 | 100 |
| 100 | 0 | 100 |
| 105 | 0 | 100 |
| 110 | 0 | 100 |
| 115 | 0 | 100 |
| 120 | 0 | 100 |
| 125 | 0 | 100 |
| 130 | 0 | 100 |
| 135 | 0 | 100 |
| 140 | 0 | 100 |
| 145 | 15.3 | 100 |
| 150 | 100 | 100 |
| 155 | 100 | 100 |
| 160 | 100 | 100 |
| 165 | 100 | 100 |
| 170 | 100 | 100 |
| 175 | 100 | 100 |
Without a bound no login succeeded from 10 s to 145 s: the service went on verifying passwords for clients that had already given up, also through the two minutes at half capacity that followed. With the bound it served about 93 logins/s through the overload, refused the rest at once, timed nobody out, and was back under the latency limit in the first five seconds after the overload.