#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
#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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
#163 claimed
bcryptjsblocks 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 inroutes/customers.tsuse it — there is nohashSyncorcompareSyncanywhere insrc.A smaller effect is real, though, and this measures it instead of arguing about it. The probe is
GET /api/customers/mewith 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:
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
scriptsas well assrc, so the one file in it is held to the same standard as the rest.