Async/Await vs. Callbacks vs. Threads
Prompt
Explain how a single-threaded runtime like Node serves thousands of concurrent requests, and what async/await is actually doing under the hood compared to spinning up a thread per request. Then tell me where that model falls apart.
How this round runs
This is a conversation that escalates — I'll keep asking "and what happens when…" until I hit the edge of what you know. I want the event-loop mechanism and an honest read on when concurrency is not parallelism, not a memorized "async is non-blocking" line.
Model answer
I'll separate concurrency from parallelism first, because that's where this usually goes wrong. A thread-per-request model uses OS-level parallelism: each thread has its own stack, the scheduler preempts them, and on multiple cores they genuinely run at the same time — but each thread carries memory and context-switch overhead, so thousands of them get expensive.
A single-threaded event-loop runtime gets concurrency without parallelism. async/await is
sugar over promises, and a promise is a handle to work the runtime hands off — a socket
read, a timer, a DB query — to the OS or a background pool. await doesn't block; it
suspends the current function, yields control back to the event loop, and the loop runs
other ready work while that I/O is outstanding. When the I/O completes, the continuation is
queued and resumes. So one thread can have thousands of requests in flight because at any
instant almost all of them are parked waiting on I/O, not computing. Callbacks are the same
hand-off mechanism written inside-out — promises just flatten the nesting and give you
try/catch error handling instead of an error-first argument on every call.
Where it falls apart — the honest edge: the model only wins when work is I/O-bound. A
CPU-bound task (parsing a huge payload, hashing, image work) runs on the event-loop thread
and there's no await point to yield at, so it blocks the loop and every other request
stalls behind it. The fix is to move that off-thread — worker threads, a child process, or
native async APIs. The boundary I'd flag: I can describe backpressure conceptually — a fast
producer overwhelming a slow consumer needs the consumer to signal "pause" — but the exact
stream-level mechanics differ per runtime and I'd verify the specific API.
- Led with concurrency vs parallelism instead of 'async is non-blocking'
- Described the mechanism: await suspends and yields to the loop, the runtime hands I/O off, the continuation is queued on completion
- Connected to the real win: thousands in flight because they're parked on I/O, not computing
- Framed callbacks and promises as the same hand-off, with promises flattening nesting and fixing error handling
- Honest boundary on backpressure — conceptually clear, defers on the per-runtime API details
- And what happens when you run a CPU-bound loop with no await inside it on the event-loop thread? → it blocks the loop; every other request stalls until it finishes; offload to a worker/process
- A fast producer feeds a slow consumer — what stops memory from blowing up? → backpressure: the consumer signals pause, the producer stops until drained
- Python threads exist — why don't they parallelize CPU work the way Go's goroutines or Java threads do? → the GIL serializes bytecode execution; use processes for CPU-bound parallelism