Skip to content

HTTP2 POST requests over fetch() to the same origin can't run concurrently #5494

Description

@wgoodall01

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 a ReadableStream, 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

  • The server script handles each request over h1 or h2, does sleep() and responds. It also tracks stats on the max count of inflight requests.
  • The client script makes concurrent test requests over HTTP1/HTTP2, fetch() or undici request(), and GET or POST, printing stats for each combination of the above.
  • You can run both with npm install && npm run repro

On my machine, it outputs the following:

$ 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

You can see that the H2 fetch() POST line indicates the requests aren't running concurrently.

Expected Behavior

The H2 fetch() POST line reports similar wall-clock latency and sendHeaders offsets to the other test cases.

Environment

$ 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

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

LLM Disclosure

  • I wrote everything in this issue myself.
  • I used Claude Fable 5 to track down this issue in my codebase and put together a first pass at the repro, which I then hand-edited for clarity.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions