http3: a buffered response body goes out under backpressure, not into retention whole - #276
Merged
Merged
Conversation
… 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.
This was referenced Oct 4, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
CanQueueSendallows, 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'ssrc/and against this branch: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:
1 MiB bodies (ManagedBuffered, 64 conns × 32 streams): main completes none, every connection closed at the backstop; this PR 2,461 req/s.