$ npm run repro
npm run repro
> repro
> node server.mjs & trap 'kill $!' EXIT; node client.mjs
server: listening on https://localhost:4433 (holdMs=750)
undici 8.6.0, nodejs v26.4.0
H2 fetch() POST proto=2.0 peak=1/12 wall=9060ms
sendHeaders offsets (ms): 2, 758, 1513, 2269, 3023, 3777, 4531, 5284, 6039, 6795, 7551, 8305
H2 fetch() GET proto=2.0 peak=12/12 wall= 759ms
sendHeaders offsets (ms): 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1
H2 request() POST proto=2.0 peak=12/12 wall= 761ms
sendHeaders offsets (ms): 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2
H2 request() GET proto=2.0 peak=12/12 wall= 754ms
sendHeaders offsets (ms): 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
H1 fetch() POST proto=1.1 peak=12/12 wall= 765ms
sendHeaders offsets (ms): 5, 8, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10
H1 fetch() GET proto=1.1 peak=12/12 wall= 760ms
sendHeaders offsets (ms): 3, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7
H1 request() POST proto=1.1 peak=12/12 wall= 761ms
sendHeaders offsets (ms): 3, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9
H1 request() GET proto=1.1 peak=12/12 wall= 759ms
sendHeaders offsets (ms): 2, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5
$ node --version
v26.4.0
$ uname -a
Darwin $HOSTNAME.local 24.6.0 Darwin Kernel Version 24.6.0: Tue Apr 21 20:18:11 PDT 2026; root:xnu-11417.140.69.710.16~1/RELEASE_ARM64_T6020 arm64
$ sw_vers
ProductName: macOS
ProductVersion: 15.7.7
BuildVersion: 24G720
I ran into this making requests to the OpenAI API, which supports H2, and where every request happens to be a POST. This bug means that a single nodejs instance can only make a single LLM request concurrently—a significant bottleneck!
Bug Description
Concurrent POST requests to the same origin using
fetch()execute without concurrency when that origin supports h2.This is because
fetch()POST bodies are converted to aReadableStream, and requests with stream bodies do not (currently) execute with concurrency on the same http2 connection. Meanwhile, because a single h2 connection supports inter-request concurrency, the agent won't open more than one h2 connection to the same origin. The confluence of these two factors means that if you're making highly-concurrent POST requests to the same origin, when h2 is enabled, they'll actually execute sequentially instead.Reproducible By
I set up a minimal reproduction in this repo: https://github.com/wgoodall01/repro-undici-h2
sleep()and responds. It also tracks stats on the max count of inflight requests.fetch()or undicirequest(), and GET or POST, printing stats for each combination of the above.npm install && npm run reproOn my machine, it outputs the following:
You can see that the
H2 fetch() POSTline indicates the requests aren't running concurrently.Expected Behavior
The
H2 fetch() POSTline reports similar wall-clock latency and sendHeaders offsets to the other test cases.Environment
Additional context
I ran into this making requests to the OpenAI API, which supports H2, and where every request happens to be a POST. This bug means that a single nodejs instance can only make a single LLM request concurrently—a significant bottleneck!
Prior Art
undici@8.4.0with concurrent POST requests over H2undici.request(), however, that fix doesn't apply tofetch()requests.LLM Disclosure