test(perf): measure what concurrent hashing actually costs a bystander request (#163) #172

Merged
bermudalamb merged 1 commits from feature/163-measure-hash-latency into main 2026-08-24 17:02:53 -05:00
Owner

#163 claimed bcryptjs blocks the event loop and that every login stalls every other request in flight. That was asserted without measurement and is wrong: the asynchronous API chunks its work and yields between rounds, and all six call sites in routes/customers.ts use it — there is no hashSync or compareSync anywhere in src.

A smaller effect is real, though, and this measures it instead of arguing about it. The probe is GET /api/customers/me with no cookie, chosen for doing almost nothing: it rejects before touching the database, so nearly all of its latency is time spent waiting for the event loop rather than work of its own. The load is real registrations against the real route, because the question is what a deployed server does rather than what bcrypt does on a bench.

Measured on the dev machine, Node 24, cost 12:

Concurrent registrations Each registration (p50) Bystander p95 Bystander worst case
idle 0.3 ms 5.4 ms
1 213 ms 0.6 ms 102 ms
4 803 ms 101.9 ms 405 ms
8 1626 ms 15.6 ms 808 ms

Both columns are linear in the number of queued hashes. Registration is roughly 200 ms times the concurrency, because the hashes serialize onto the one thread. The bystander's worst case is roughly 100 ms times the concurrency, which matches the coarseness of the chunks measured earlier — about 100 ms of un-yielding time per hash.

The median stays under a millisecond throughout, so this is a tail-latency characteristic and not the stall the issue described.

The benchmark creates real customers and deletes them again, because it is normally pointed at a development database that nothing truncates.

Lint now covers scripts as well as src, so the one file in it is held to the same standard as the rest.

#163 claimed `bcryptjs` blocks the event loop and that every login stalls every other request in flight. That was asserted without measurement and is wrong: the asynchronous API chunks its work and yields between rounds, and all six call sites in `routes/customers.ts` use it — there is no `hashSync` or `compareSync` anywhere in `src`. A smaller effect is real, though, and this measures it instead of arguing about it. The probe is `GET /api/customers/me` with no cookie, chosen for doing almost nothing: it rejects before touching the database, so nearly all of its latency is time spent waiting for the event loop rather than work of its own. The load is real registrations against the real route, because the question is what a deployed server does rather than what bcrypt does on a bench. Measured on the dev machine, Node 24, cost 12: | Concurrent registrations | Each registration (p50) | Bystander p95 | Bystander worst case | | --- | --- | --- | --- | | idle | — | 0.3 ms | 5.4 ms | | 1 | 213 ms | 0.6 ms | 102 ms | | 4 | 803 ms | 101.9 ms | 405 ms | | 8 | 1626 ms | 15.6 ms | 808 ms | Both columns are linear in the number of queued hashes. Registration is roughly 200 ms times the concurrency, because the hashes serialize onto the one thread. The bystander's worst case is roughly 100 ms times the concurrency, which matches the coarseness of the chunks measured earlier — about 100 ms of un-yielding time per hash. The median stays under a millisecond throughout, so this is a tail-latency characteristic and not the stall the issue described. The benchmark creates real customers and deletes them again, because it is normally pointed at a development database that nothing truncates. Lint now covers `scripts` as well as `src`, so the one file in it is held to the same standard as the rest.
bermudalamb self-assigned this 2026-08-24 17:02:03 -05:00
bermudalamb added this to the Customer and Admin UI review findings project 2026-08-24 17:02:03 -05:00
bermudalamb added 1 commit 2026-08-24 17:02:03 -05:00
test(perf): measure what concurrent hashing actually costs a bystander request (#163)
SonarQube Analysis / sonarqube (pull_request) Failing after 5m9s
Linting / lint (pull_request) Successful in 2m5s
313b48f582
#163 claimed `bcryptjs` blocks the event loop and that every login stalls every other request in flight. That was asserted without measurement and is wrong: the asynchronous API chunks its work and yields between rounds, and all six call sites in `routes/customers.ts` use it — there is no `hashSync` or `compareSync` anywhere in `src`.

A smaller effect is real, though, and this measures it instead of arguing about it. The probe is `GET /api/customers/me` with no cookie, chosen for doing almost nothing: it rejects before touching the database, so nearly all of its latency is time spent waiting for the event loop rather than work of its own. The load is real registrations against the real route, because the question is what a deployed server does rather than what bcrypt does on a bench.

Measured on the dev machine, Node 24, cost 12:

| Concurrent registrations | Each registration (p50) | Bystander p95 | Bystander worst case |
| --- | --- | --- | --- |
| idle | — | 0.3 ms | 5.4 ms |
| 1 | 213 ms | 0.6 ms | 102 ms |
| 4 | 803 ms | 101.9 ms | 405 ms |
| 8 | 1626 ms | 15.6 ms | 808 ms |

Both columns are linear in the number of queued hashes. Registration is roughly 200 ms times the concurrency, because the hashes serialize onto the one thread. The bystander's worst case is roughly 100 ms times the concurrency, which matches the coarseness of the chunks measured earlier — about 100 ms of un-yielding time per hash.

The median stays under a millisecond throughout, so this is a tail-latency characteristic and not the stall the issue described.

The benchmark creates real customers and deletes them again, because it is normally pointed at a development database that nothing truncates.

Lint now covers `scripts` as well as `src`, so the one file in it is held to the same standard as the rest.
bermudalamb merged commit 5d72c1c88b into main 2026-08-24 17:02:53 -05:00
bermudalamb deleted branch feature/163-measure-hash-latency 2026-08-24 17:02:54 -05:00
bermudalamb moved this to Review in Customer and Admin UI review findings on 2026-08-24 17:02:59 -05:00
bermudalamb moved this to Ready for Release in Customer and Admin UI review findings on 2026-09-09 13:33:51 -05:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bermudalamb/redefined-designs#172