CONCEPT Cited by 1 source
Microtask hop cost¶
Microtask hop cost is the per-invocation CPU + memory
overhead of resolving a Promise through JavaScript's
microtask queue — even when the resolution value is
already available. Promises resolve via the microtask
queue rather than synchronously, so every await on a
resolved Promise still costs one queue enqueue + dequeue
+ callback invocation hop.
The shape of the cost¶
When await promise runs:
- Current function suspends, captures its continuation.
.then(continuation)is called on the Promise.- If Promise is already resolved: microtask enqueued immediately. If pending: microtask enqueued on resolution.
- Current synchronous work continues (or call stack unwinds).
- Microtask queue drained: continuation invoked with value.
Steps 3-5 are the hop. Even when the Promise is
pre-resolved (Promise.resolve(value)), the hop still
runs — JavaScript spec requires it. This is what makes
await Promise.resolve(x) not a no-op.
Why it matters on streaming hot paths¶
A streaming pipeline running await reader.read() per
chunk pays this hop per chunk. At streaming SSR scale
(millions of chunks per hour per instance), the hop
cost compounds:
"Consider what happens when you call
reader.read()on a native WebStream in Node.js. Even if data is already sitting in the buffer: [...] resolution goes through the microtask queue. That is four allocations and a microtask hop to return data that was already there." (sources/2026-04-21-vercel-we-ralph-wiggumed-webstreams-to-make-them-10x-faster)
The hop itself isn't free:
- Closure allocation — the
.thencallback capturing continuation state. - Queue entry — a small object in the microtask queue.
- Queue drain + invocation — cost scales with how full the queue is under load.
- Allocation pressure → GC scavenger runs more often, stealing request CPU.
Not visible on synthetic benchmarks¶
At single-call latency, the hop is ~nanoseconds and
invisible. The cost only surfaces when you're paying
it per item in a large pipeline — which is why it
can be missed in naive await someRead() benchmarks
but dominates when a streaming SSR pipeline enqueues
thousands of chunks.
Cannot be eliminated, only reduced¶
Unlike ReadableStreamDefaultReadRequest allocation,
which is implementation-discretionary, the microtask hop
is language-level. Promise.resolve(x) still
schedules through the microtask queue to preserve
Promise semantics (the consumer's callback must run
asynchronously, even if the value is immediately
available, so side-effect ordering in between is
preserved).
Mitigations target batching rather than elimination:
- Yield batches per hop.
Uint8Array[]per iteration instead of one chunk per iteration — one hop per batch of N chunks, not N hops. new-streams uses this by default. - Skip hops in composition. When source + all
transforms + sink are known at pipe-construction
time, the implementation can fuse into a single
Node-level
stream.pipeline()call with zero WHATWG-level hops per chunk. See patterns/record-pipe-links-resolve-at-sink. - Synchronous-style APIs.
Stream.pullSyncin new-streams; synchronousIterator::next()analogs in other languages.
Relationship to promise-allocation-overhead¶
concepts/promise-allocation-overhead is the umbrella
cost category — allocation + microtask + GC +
coordination promises. Microtask hop cost is the
scheduling sub-class. You can reduce allocation
without reducing hops (e.g. reuse a pre-allocated
Promise) and still pay the hop. You can reduce hops
without reducing allocation (e.g. batch reads but still
allocate new {value, done} per batch).
Cross-runtime generality¶
- Node.js + V8: microtask queue per event loop iteration.
- Browsers (V8, JavaScriptCore, SpiderMonkey): spec-identical microtask handling.
- Bun (JavaScriptCore + Zig scheduler): also pays the hop but with different queue drain semantics; cited as part of why Bun's streaming SSR path is 28 % faster TTLB than Node (sources/2026-04-21-vercel-bun-runtime-on-vercel-functions).
- Cloudflare Workers (V8 isolate): same hop cost; canonical instance of SSR GC pressure at 10-25 % of request CPU (sources/2025-10-14-cloudflare-unpacking-cloudflare-workers-cpu-performance-benchmarks).
Seen in¶
- sources/2026-04-21-vercel-we-ralph-wiggumed-webstreams-to-make-them-10x-faster
— canonical wiki instance naming the hop as one of
the four per-
read()costs (alongside request-object - Promise + result-object allocation). The concepts/synchronous-fast-path-streaming optimisation preserves the hop (unavoidable) while eliminating the other three.
Related¶
- systems/v8-javascript-engine — the engine whose microtask queue implementation this concept sits on.
- systems/web-streams-api — the spec surface where per-chunk hops dominate cost.
- systems/fast-webstreams — the library targeting this cost class.
- concepts/promise-allocation-overhead — the umbrella cost category this is a sub-class of.
- concepts/synchronous-fast-path-streaming — the optimisation that preserves the hop but eliminates surrounding allocation.
- concepts/hot-path — why per-call cost matters at per-request scale.
- concepts/event-loop-blocking-single-threaded — sibling concept at the event-loop-blocking scale rather than per-call scale.