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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions