Node.js Foundations: Event Loop Work, Backpressure, and Cancellation
Keep request handlers responsive by distinguishing asynchronous waiting from CPU work, bounding concurrency, and propagating stream failures.
On this page
Async does not move computation off the event loop
Node executes JavaScript callbacks on an event-loop thread. A handler declared async still runs synchronous work on that thread until it reaches an actual asynchronous wait. Parsing a huge payload, performing a long loop, or using a synchronous filesystem API can delay unrelated requests even if the handler returns a promise.
Some Node APIs use operating-system asynchronous I/O; others use a worker pool. These are not unlimited resources. Expensive crypto and filesystem operations can contend for pool capacity. CPU-heavy JavaScript may need worker threads or separate processes, while ordinary I/O-bound work needs nonblocking APIs and bounded concurrency. Measure event-loop delay alongside request latency to distinguish these cases.
Promise aggregation is not a concurrency limit
Promise.all waits for all supplied promises; it does not control how many operations were started. Mapping ten thousand URLs to requests can allocate ten thousand active operations before Promise.all begins waiting. Instead, use a bounded worker pool or a maintained concurrency limiter. Define the limit according to downstream capacity, memory, and latency rather than choosing the largest number a local test survives.
const results = await Promise.all([
Promise.resolve({ id: "one", ok: true }),
Promise.resolve({ id: "two", ok: true }),
]);
console.log(results.map((result) => result.id));
This preset demonstrates result aggregation, not real network parallelism. If one promise rejects, Promise.all rejects, but sibling operations are not automatically canceled. Use a shared AbortSignal where the underlying APIs support it and decide whether partial results are acceptable.
Backpressure bounds memory
A stream producer may be faster than its destination. Ignoring writable backpressure lets buffers accumulate until memory is exhausted. When write returns false, wait for drain before producing more data, or use Node's stream pipeline utilities so error propagation and backpressure are handled together.
Do not load a multi-gigabyte file into one string simply because the API offers readFile. Streaming can preserve bounded memory, but only if downstream transforms also operate incrementally. A transform that accumulates all chunks before emitting has recreated the original memory problem inside a stream interface.
Cancel ownership, not just the response
When a client disconnects or a deadline expires, propagate cancellation to owned I/O and close resources in finally blocks. An aborted HTTP response is not proof that a database query or background task stopped. Some drivers cannot cancel a running operation; bound such work and document its lifecycle instead of assuming an AbortSignal is universal.
Test a slow consumer, a destination error, a disconnect, and an overloaded dependency. Check memory, active operations, and cleanup after the test completes. A successful response under ideal load does not show that the process remains responsive under saturation.