Skip to content
matthewcm.dev

    ↑ ↓ move · ↵ open · tab scopeopen graph →

    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.

    Growingtended planted 3 min read
    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

    ↪ Links to (8)