Async & Event Loops
Async/await as an execution model rather than syntax: futures and promises, the event loop, worker threads and web workers, and the two lessons that matter most — async is not parallelism, and a CPU-bound handler stalls the loop for everyone.
A queue of ready callbacks drained by one logical execution context, with the runtime performing I/O somewhere else. That shape buys you freedom from data races on your own heap — and buys you nothing at all against race conditions across await points, which is where the bugs actually are.
Q · If one event loop drains every callback in turn, what can still change between the two halves of my handler?
The syntax reads like blocking code; the execution model is start → suspend → return control → other work runs → resume. Everything you must reason about lives in the gap: state can change across an await, and the code around it stops being atomic the moment you add one.
Q · What actually happens at an `await`, and what may have changed by the time the next line runs?
A handle to a result that does not exist yet: pending, then settled with a value or an error, once and permanently. The interesting questions are who runs the work, when it starts, what happens if nobody looks at the handle, and whether the result can be consumed twice.
Q · What does a handle to unfinished work actually promise me, and who is doing the work while I hold it?
Real OS threads in Node, each with its own event loop, its own heap and no shared objects by default. You move CPU work off the main loop by sending a message; the cost is that "sending" means structured-cloning or transferring, and the moment you reach for SharedArrayBuffer you get real data races back.
Q · How do I get CPU work off the event loop without giving up the safety the single-loop model was buying me?
The browser version of the same bargain, with a harder constraint: the main thread also renders. Anything over roughly 50 ms on it is a dropped frame or an unresponsive click, so the question is not "is this slow?" but "does this belong on the thread that paints?".
Q · Which work must leave the thread that renders, and what can actually cross the boundary?
An honest decision, with real costs on both sides. The loop wins on many waiting tasks, cheap per-task memory and a simpler shared-state model. Threads win on blocking libraries, parallel CPU work and code that is easier to read and debug. Most production systems end up hybrid, and that is not a failure of the choice.
Q · For this workload, does an event loop or a thread-per-task model actually cost less — in latency, memory, and engineer-hours?
Marking a function async does not make it run on another core. Async lets one execution context switch between tasks *while they are suspended* — so it buys you nothing at all when the tasks never suspend. A CPU-bound loop inside an async function is a CPU-bound loop that also allocates a state machine.
Q · Why did wrapping this CPU-heavy function in `async` and awaiting them all together change nothing?
One synchronous 200 ms handler does not cost 200 ms — it costs 200 ms multiplied by everything that was waiting. The work is correct, the endpoint is fast in isolation, and the symptom appears on completely unrelated routes, which is why this is diagnosed late and blamed on the wrong service.
Q · Why did one slow synchronous function make every unrelated endpoint slow at the same time?