quic: a bare FIN counts as sent only when ngtcp2 wrote it, not on any packet - #278
Merged
Merged
Conversation
… packet With the congestion window full and an ACK due, ngtcp2 answers a stream write with a packet holding only that ACK and reports the stream frame unwritten (pdatalen -1). PumpOut marked a bare FIN sent on any packet, so that ACK swallowed it: the stream was done as far as the replay list knew, nothing retried it, and the peer waited for an end that never came. Under h3x, 22 of 1.5M streamed HTTP/3 responses hung until the client gave up - 22 FINs counted this way, 22 streams never closed. A bare FIN is now sent only when the write reports 0 bytes consumed, as ngtcp2 documents.
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.
With the congestion window full and an ACK due, ngtcp2 answers a stream write with a packet holding only that ACK and reports the stream frame unwritten (
*pdatalen = -1, as its docs say).PumpOutcounted a bare FIN as sent whenever a packet came back, so that ACK swallowed it: the stream was done as far as the replay list knew, nothing retried the FIN, and the peer waited for an end that never came. A bare FIN now counts as sent only when the write reports 0 bytes consumed.A streamed HTTP/3 response that calls
CompleteAsyncafter its last flush ends with a bare FIN, on both stacks. h3x saw it as requests that never complete (13-43 per run of ~1.5M). A probe counted the FINs marked sent without being written - 22, against 22 hung requests and 22 streams that never closed - and ngtcp2's own log (a diagnostic build) showed each one:cwnd limited, then a 1-RTT packet holding only an ACK. h2load never showed them: in duration mode a stream still open at the end is not a failure.Tests
quic: a FIN parked behind a full congestion window is still sent(new): the server fills its window on one stream and resets it, so the bare FIN it then sends on another is the only thing waiting. The client sends a packet after a lost one - an immediate ACK under RFC 9000 13.2.1 - so the parked FIN is the server's next write while the window is still full.QuicTestClientgainsSend(data, fin, lost),PumpUntiland per-stream byte and FIN tracking.All suites pass: E2E 232, Unit 60, Chaos 47, Http 44, Tls 151 (the 7 kTLS tests skip without sudo), File 4.
Before and after
h3x, 16 conns × 32 streams, 1 KiB streamed responses, 6 s:
* one run lost a whole connection (32 requests) to a handshake timeout under load, which main shows as well - the kernel caps this machine's UDP buffer at 208 KiB.
h2load, 1 KiB, 16 conns × 32 streams, 4 interleaved rounds with a second build of main as control; within-round median vs main: