The Node.js Event Loop, Streams and Backpressure
Why Node is fast until you block it, how streams make backpressure explicit, and how I used the same ideas in a CMS migration tool and in real-time UIs. Grounded in two books and one tool I actually built.
On this page
Node.js runs your JavaScript on a single thread with an event loop. That one fact explains most of what makes Node fast, and most of what makes it fall over.
The event loop in one paragraph
Node hands slow operations (network, disk, timers) to the operating system or a small thread pool, and keeps running
other work while it waits. When an operation completes, its callback is queued, and the event loop picks it up. One thread
can juggle thousands of connections because it never sits waiting. The catch: anything synchronous and slow blocks
everyone. A big JSON.parse, a tight loop over a large array or a synchronous crypto call stalls every request on that
process.
The practical rules:
- Don’t block the loop. Move CPU-heavy work to worker threads, a separate service or a queue.
- Know the order of things. Microtasks (resolved promises) run before the next timer or I/O callback, which matters when you’re debugging ordering bugs.
- Measure event-loop lag. If the loop is late, everything is late. It’s one of the first metrics I’d put on a Node service (Observability).
Streams make backpressure explicit
A stream processes data in chunks instead of loading it all into memory. More importantly, streams handle backpressure: when the consumer is slower than the producer, the producer pauses until the consumer catches up.
import { pipeline } from 'node:stream/promises';
await pipeline(
readExportedItems(), // a Readable source
transformToNewModel, // a Transform, one item at a time
writeToTargetApi, // a Writable that respects the target's rate limits
);
pipeline wires the pieces together, propagates errors and handles backpressure for you. If the target API is slow, the
reader slows down rather than filling memory.
Where I’ve used it
The clearest example is a tool I built in Node.js at Elanco to move whole sites from one headless CMS to another (A Resumable CMS Migration Tool). Hundreds of items per site, plus images and videos. The design that worked was export to disk first, then a work queue processed in batches, streaming large media rather than buffering it, with checkpoints so a failed run could resume where it stopped (Resumable Batch Migrations). That’s backpressure and idempotency, learnt on a real job rather than from a diagram.
The same idea shows up on the front end. In the real-time trading UIs I build at a fintech, a socket can deliver far more updates than a screen can paint. Decoupling the feed from rendering, and only painting the latest value, is client-side backpressure (Zustand Transient Updates). On the server side of a stream, the equivalent is conflation: when a client falls behind, send it the latest state rather than every intermediate update (Designing Snapshot and Delta APIs).
What the books added
Node.js Design Patterns is the best explanation I know of the reactor pattern, async control flow and streams, and of why classic patterns (observer, proxy, factory) look different in an async, module-based world. Architecting the AI-Driven Node.js Backend applies the same thinking to a newer kind of slow dependency: model calls. Treat them like any unreliable network call, with timeouts, retries, queues for long jobs and observability from day one.
Checklist for a Node service
- Never block the loop; offload CPU-bound work.
- Stream large payloads; never buffer unbounded data.
- Bound every queue and buffer, and decide what happens when it’s full: wait, drop or conflate.
- Time out every outbound call, and make retried operations idempotent.
- Watch event-loop lag, memory and open handles in production.
Related: Message Passing · Threads and Processes · Regress, Don’t Grind: Graceful Degradation
- #nodejs
- #event-loop
- #streams
- #backpressure
- #async
- #performance
↩ Linked from (3)
…conflation is usually right: a slow client needs the latest state, not every step in between. More on that in The Node.js Event Loop, Streams and Backpressure.
…practice versus theory. Reliability. Timeouts, retries with idempotency keys, circuit breakers, backpressure (The Node.js Event Loop, Streams and Backpressure). Observability from the server side. Tracing, SLOs and error…
- Spring and Java, RefreshedGrowing
Neither is better in general. Spring's structure pays off as a domain and team grow; Node's simplicity pays off for small, I/O-heavy services (The Node.js Event Loop, Streams and Backpressure).
↪ Links to (8)
Related by topic
- Growing
REST API Design Trade-offs: Bulk Deletes, Request Bodies and Cancellation
Backend - Growing
Snapshot and Delta Sync over WebSockets
Real-time UIs - Seed
Kubernetes
Platform engineering - Evergreen
Message Delivery Semantics
Distributed systems