Skip to content

http3: a buffered response body goes out under backpressure, not into retention whole - #276

Merged
MDA2AV merged 1 commit into
mainfrom
fix/h3-buffered-backpressure
Oct 4, 2026
Merged

MDA2AV merged 1 commit into
mainfrom
fix/h3-buffered-backpressure

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Oct 4, 2026

Copy link
Copy Markdown
Owner

A buffered ioxide.http3 response handed its whole body to the QUIC layer in one SendStream, so send retention grew by the body's size at once. Past the 32 MiB backstop the connection is closed as a producer that ignored backpressure: one 40 MiB response, or 32 one-MiB responses on a connection (h2load -m 32), and every such connection was lost.

Bodies over the 4 KiB inline limit now go in 64 KiB pieces while CanQueueSend allows, and the rest waits in a per-connection queue that the capacity signal (OnSendCapacityAvailable, acks draining retention) works through. A streamed connection keeps its own callback. nghttp3 already fed its egress this way; its tests here are guards.

This is the fix #275 pointed at - that PR made the limits configurable.

Tests

H3LargeResponseTests (new), each run against main's src/ and against this branch:

test main this PR
a 40 MiB response, ioxide.http3 (sync and async handler) FAIL - 0 bytes, connection closed pass
40 × 1 MiB at once on one connection, ioxide.http3 (sync and async) FAIL - 0 of 40 pass
the same two on nghttp3 pass pass
ioxide.http3 streamed response past the high-water (guard for the callback) pass pass

With only the capacity callback removed, both ioxide.http3 shapes stop at exactly the 16 MiB high-water, so the resume half is needed as well.

H3TestClient.GetConcurrent (new) keeps several requests in flight on one connection.

All suites pass: E2E 238, Unit 60, Chaos 47, Http 44, Tls 151 (the 7 kTLS tests skip without sudo), File 4.

Bench

h2load, 1 KiB, 16 conns × 32 streams, 2 reactors, 4 interleaved rounds with a second build of main as control; within-round median vs main:

sample this PR control
Http3/ManagedBuffered +1.3% +0.8%
Http3/ManagedStreamedBoth -0.1% +0.3%
Http3/Nghttp3Response +0.8% +1.9%
Http3/Nghttp3Buffered -2.1% -2.9%

1 MiB bodies (ManagedBuffered, 64 conns × 32 streams): main completes none, every connection closed at the backstop; this PR 2,461 req/s.

… retention whole

The pure-C# stack handed a large body to the QUIC layer in one call, so send retention
grew by its whole size at once: 32 one-MiB responses on a connection crossed the 32 MiB
backstop and every such connection was closed, as if the producer had ignored backpressure.
Bodies now go in 64 KiB pieces while CanQueueSend allows, and the rest as the capacity
signal reports acks draining retention. The response's body is held until it has gone.
nghttp3 already fed its egress this way.
@MDA2AV
MDA2AV merged commit d2a91f3 into main Oct 4, 2026
1 check passed
@MDA2AV MDA2AV mentioned this pull request Oct 4, 2026
@MDA2AV
MDA2AV deleted the fix/h3-buffered-backpressure branch October 5, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant