Sync vs Async #1190
|
The documentation recommends to use async but i am wondering if anyone has done any benchmarking to support this claim? AFAIU async should be used when there is a network call or other I/O bound operation being executed behind the scenes. For CPU bound computation async will actually hurt performance than help. any opinions? |
Replies: 4 comments
|
The recommendation is correct, but the reasoning is often misunderstood. You're right that async doesn't reduce CPU time — the computation is identical. The benefit is about not blocking the Node.js event loop. Why async matters for bcrypt specifically: Node.js is single-threaded. bcrypt.hashSync() occupies that thread entirely for ~100ms (cost factor 10 on modern hardware). During that time, your server cannot process any other incoming requests. bcrypt.hash() (async) offloads the work to libuv's thread pool (default: 4 threads), keeping the event loop free. Sync: [request] → [event loop BLOCKED 100ms] → [response] Async: [request] → [thread pool] → [event loop free] → [response] Practical benchmark scenario: Under 50 concurrent login requests with cost=10:
The tradeoff you mentioned is real but doesn't apply here: CPU-bound async hurts in environments where threads are expensive (e.g., Python GIL). In Node.js, libuv's thread pool is designed exactly for this — offloading expensive CPU work without spinning up new OS threads per request. TLDR: Use async not for speed per operation, but to keep your server responsive under concurrent load. hashSync in a web server is effectively a DoS vulnerability at scale. |
|
why is then |
|
Good question — it seems contradictory, but the two cases are actually opposite scenarios. The deciding factor isn't "sync vs async," it's how long the operation blocks the event loop. bcrypt: intentionally slow. Cost factor 10 = ~100ms of pure CPU grinding by design (that's the whole point of a password hash). Blocking the event loop for 100ms per request is catastrophic under load. better-sqlite3: local SQLite queries are microseconds to low-milliseconds. SQLite is an embedded database — no network, no socket, just a memory/disk read on the same machine. The "I/O" is so fast that the overhead of going async (callback scheduling, thread pool handoff, serialization) costs more than just doing the work synchronously. Why The ``` Benchmarks consistently show The unifying principle: Don't ask "is it sync or async?" Ask "how long will this block the single thread, and is that duration acceptable?"
bcrypt is CPU-bound and slow → offload it. SQLite is fast and local → the async machinery is pure overhead. A network database is the classic async case because the thread genuinely sits idle waiting on the wire. TLDR: Async isn't "good," sync isn't "bad." Async only pays off when the thing you're offloading is either slow (bcrypt) or a genuine wait on external I/O (network). For an embedded store that returns in microseconds, sync is faster because you skip the coordination cost. Same principle, opposite conclusion. |
|
Thanks. Great explanation.
…On Sun, Jun 7, 2026 at 4:05 AM Lian Harman ***@***.***> wrote:
Good question — it seems contradictory, but the two cases are actually
opposite scenarios. The deciding factor isn't "sync vs async," it's *how
long the operation blocks the event loop*.
*bcrypt:* intentionally slow. Cost factor 10 = ~100ms of pure CPU
grinding by design (that's the whole point of a password hash). Blocking
the event loop for 100ms per request is catastrophic under load.
*better-sqlite3:* local SQLite queries are *microseconds to
low-milliseconds*. SQLite is an embedded database — no network, no
socket, just a memory/disk read on the same machine. The "I/O" is so fast
that the overhead of going async (callback scheduling, thread pool handoff,
serialization) costs *more* than just doing the work synchronously.
*Why better-sqlite3 sync actually wins:*
The sqlite3 async API hands each query to libuv's thread pool. For an
operation that takes 50 microseconds, the thread pool handoff + context
switch + callback queueing dominates the actual work. You pay coordination
overhead for nothing.
```
sqlite3 (async): [query 50µs] + [thread handoff ~100µs+] = overhead
dominates
better-sqlite3 (sync): [query 50µs] = done, event loop barely notices
```
Benchmarks consistently show better-sqlite3 is several times faster for
typical workloads precisely because it skips that machinery.
*The unifying principle:*
Don't ask "is it sync or async?" Ask *"how long will this block the
single thread, and is that duration acceptable?"*
Operation Duration Block the event loop?
bcrypt hash (cost 10) ~100ms No — offload (async)
SQLite local query ~50µs–1ms Yes — it's negligible (sync)
Network DB (Postgres/MySQL) 1–100ms+ No — must be async (real I/O wait)
bcrypt is CPU-bound *and slow* → offload it. SQLite is fast *and local* →
the async machinery is pure overhead. A network database is the classic
async case because the thread genuinely sits idle waiting on the wire.
*TLDR:* Async isn't "good," sync isn't "bad." Async only pays off when
the thing you're offloading is either slow (bcrypt) or a genuine wait on
external I/O (network). For an embedded store that returns in microseconds,
sync is faster *because* you skip the coordination cost. Same principle,
opposite conclusion.
—
Reply to this email directly, view it on GitHub
<#1190?email_source=notifications&email_token=A6NWEKZZVNNWXLDWMZCNC2L46VD6ZA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCNZSGA4TANBYUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVRTG633UMVZF6Y3MNFRWW#discussioncomment-17209048>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/A6NWEK6H6GQGBC4SYNUS5TL46VD6ZAVCNFSM6AAAAACZ5JHFCWVHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTOMRQHEYDIOA>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/A6NWEK2XBWCJ2EJC65ZVJF346VD6ZA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCNZSGA4TANBYUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVJTG633UMVZF62LPOM>
and Android
<https://github.com/notifications/mobile/android/A6NWEK6Z2APMMQRNBFTG2GD46VD6ZA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCNZSGA4TANBYUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVZTG633UMVZF6YLOMRZG62LE>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***
com>
|
The recommendation is correct, but the reasoning is often misunderstood. You're right that async doesn't reduce CPU time — the computation is identical. The benefit is about not blocking the Node.js event loop.
Why async matters for bcrypt specifically:
Node.js is single-threaded. bcrypt.hashSync() occupies that thread entirely for ~100ms (cost factor 10 on modern hardware). During that time, your server cannot process any other incoming requests.
bcrypt.hash() (async) offloads the work to libuv's thread pool (default: 4 threads), keeping the event loop free.
Sync: [request] → [event loop BLOCKED 100ms] → [response]
^^^^^^^^ no other requests served
Async: [request] → [thread pool] → [eve…