AdvancedConcurrency, scheduling, and distributed effects~50 min

Concurrency, Scheduling, and Distributed Effects

How Node's event loop phases differ from the browser's, backpressure and scheduling fairness, true shared memory with SharedArrayBuffer, structured concurrency, and the realities of distributed systems.

By the end of this lesson, you can

  • Explain how Node's event loop phases differ from the browser's single task/microtask model
  • Explain what backpressure is and why an unbounded queue is a common production failure mode
  • Explain what SharedArrayBuffer and Atomics provide that message passing does not, and their isolation requirements
  • Apply structured concurrency principles to ensure a cancelled operation's children are also cancelled
  • Explain why a distributed system must tolerate at-least-once delivery and design for idempotency and eventual consistency

Why it matters

Asynchronous JavaScript in Depth covered the browser’s microtask/macrotask model. Systems that genuinely scale — a busy Node server, a multi-worker pipeline, anything talking to another service over a network — need a deeper model: Node’s own event loop phases, true shared memory, disciplined cancellation, and an honest account of what a network actually guarantees.

Node’s event loop phases

Node’s event loop, unlike the browser’s simpler task/microtask model, cycles through distinct phases, each handling a specific category of callback:

Selected Node event loop phases, in their cycle order
PhaseHandles
timerssetTimeout/setInterval callbacks whose delay has elapsed
pollI/O callbacks (file, network) — the loop mostly waits here
checksetImmediate callbacks — run right after the poll phase
close callbackse.g. socket.on('close', …)

Microtasks (Promise continuations, recall the Event Loop) still drain completely between every phase transition — Node layers its own phase structure on top of the same microtask-first principle, not a replacement for it.

Backpressure and scheduling fairness

Backpressure is what happens when a fast producer overwhelms a slower consumer — recall Streams’s note about .pipe() handling it automatically. Without it, an unbounded queue between producer and consumer simply grows without limit:

// No backpressure — if processItem is slower than items arrive, this queue grows forever
const queue = [];
source.on('data', (item) => queue.push(item));

// Bounded — a fixed-size queue applies backpressure by refusing new work until there's room
class BoundedQueue {
  #items = [];
  #maxSize;
  constructor(maxSize) {
    this.#maxSize = maxSize;
  }
  async push(item) {
    while (this.#items.length >= this.#maxSize) {
      await new Promise((resolve) => setTimeout(resolve, 10)); // wait for room
    }
    this.#items.push(item);
  }
}

Starvation is the related failure where one task never gets scheduled at all, because higher-priority or more frequent work keeps consuming the loop’s attention — a risk in any system with more than one priority level of scheduled work, worth testing for explicitly under load, not just assumed away.

True shared memory: SharedArrayBuffer and Atomics

Recall from Web Workers that postMessage copies data — no sharing. SharedArrayBuffer is the deliberate exception: a genuine block of memory multiple threads (workers) can read and write directly, with Atomics providing race-free operations on it:

const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);

// In one worker:
Atomics.add(view, 0, 1); // atomic increment — safe even if another thread does this simultaneously

// In another worker sharing the same sab:
Atomics.load(view, 0); // reads the current value safely

Structured concurrency

Structured concurrency means a concurrent operation’s lifetime is bound to its parent’s — when the parent is cancelled, every child operation it started is cancelled too, with nothing left “dangling” independently:

async function processAll(items, signal) {
  const controller = new AbortController();
  signal.addEventListener('abort', () => controller.abort()); // parent cancellation propagates to children

  try {
    await Promise.all(items.map((item) => processItem(item, controller.signal)));
  } finally {
    controller.abort(); // ensure no child outlives this function, success or failure
  }
}

Distributed realities

Once a network is involved, guarantees change fundamentally. A request can fail after the server successfully processed it, purely because the response was lost — this ambiguity is unavoidable, not a bug to fix:

What a network genuinely guarantees, and what it doesn't
AssumptionReality
A request either fully succeeds or fully fails, and the caller always knows whichFalse — a lost response looks identical to a lost request from the caller's side
Retrying is always safeOnly for an idempotent operation — recall idempotent from Asynchronous JavaScript in Depth
Two systems agree on data immediately after a writeOften false — many distributed stores are eventually consistent, converging after a delay
Messages arrive in the order sentNot guaranteed by many transports — explicit sequencing is needed if order matters

Because a caller can never distinguish “the request never arrived” from “the response never arrived,” a reliable distributed system is designed around at-least-once delivery as the realistic baseline — meaning every operation that could be retried must be idempotent, or carry a deduplication mechanism (an idempotency key) that makes a duplicate delivery harmless.

Check your understanding

A producer pushes items into an unbounded in-memory array faster than a consumer processes them, indefinitely. What eventually happens?
Or reveal the answer without checking

Answer:The process eventually runs out of memory, since nothing is applying backpressure to slow the producer down
With no backpressure mechanism, an unbounded queue simply grows for as long as the producer outpaces the consumer — eventually exhausting available memory. A bounded queue (or a mechanism like .pipe()'s automatic backpressure) is what prevents this.

A client sends a payment request, but the network drops the response before the client receives it. What should the client assume about whether the payment succeeded?
Or reveal the answer without checking

Answer:It genuinely cannot know from this alone — the server may have processed it before the response was lost, so a safe retry requires an idempotency mechanism
A lost response is indistinguishable, from the client's perspective, from a lost request or a server that failed before processing — the operation's actual outcome is genuinely unknown. Blind retry is unsafe for a non-idempotent operation like a payment; an idempotency key (or checking the actual current state) is needed to retry safely.

Practice: warm-up

Explain why postMessage between a main thread and a worker is safe by default (no data races possible), while SharedArrayBuffer + Atomics requires deliberate care to avoid one.

Stuck? Reveal one hint at a time.

  1. Hint 1

    postMessage copies data via structured clone — recall Web Workers — so each side always has its own independent copy, never the same memory.

  2. Hint 2

    SharedArrayBuffer is the deliberate exception: both sides genuinely read and write the exact same memory, which is precisely what makes a race between two simultaneous writes possible without Atomics.

Reveal the explanation

Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.

postMessage always copies data (via structured clone) — each side ends up with its own independent copy, so there’s no shared memory location either side could race on. SharedArrayBuffer is specifically the mechanism that removes that copying, giving both sides genuine access to the same underlying memory — which means an uncoordinated read-modify-write from two threads simultaneously can interleave incorrectly. Atomics operations are specified to be indivisible (atomic), preventing that specific class of race for the operations they cover.

Practice: apply it

Sketch (as code or clear pseudocode) how you would modify a function that fetches and processes a list of URLs concurrently so that cancelling the overall operation also cancels every still-pending individual fetch — structured concurrency, not independently-running children.

Stuck? Reveal one hint at a time.

  1. Hint 1

    A single AbortController created for the whole operation, with its signal passed into every individual fetch call, is the core of the pattern.

  2. Hint 2

    Aborting that one controller — whether from an external cancellation or a caught error — cancels every fetch still using its signal.

Reveal a reference solution

Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.

async function fetchAll(urls, externalSignal) {
  const controller = new AbortController();
  externalSignal?.addEventListener('abort', () => controller.abort());

  try {
    return await Promise.all(
      urls.map((url) => fetch(url, { signal: controller.signal }).then((r) => r.json())),
    );
  } finally {
    controller.abort(); // ensure every fetch is cancelled, whether we succeeded, failed, or were cancelled
  }
}

Summary

  • Node’s event loop cycles through distinct phases (timers, poll, check, and others); microtasks still drain completely between every phase transition, layered on top of that structure.
  • Backpressure prevents a fast producer from overwhelming a slower consumer via an unbounded queue — a common real production failure mode when absent; starvation is the related risk of a task never getting scheduled under contention.
  • SharedArrayBuffer + Atomics provide genuine shared memory between workers, unlike postMessage’s always-copied data — and require cross-origin isolation headers, a deliberate security response to Spectre-class attacks.
  • Structured concurrency means cancelling a parent operation must explicitly propagate to every child it started — this doesn’t happen automatically without deliberate AbortController wiring.
  • A distributed system must be designed around at-least-once delivery as the realistic baseline — every retryable operation needs to be idempotent, since a caller can never reliably distinguish a lost request from a lost response.