§3.5
Chapter 03 · Reliability, Loss Recovery, and Congestion Control Mechanics

§3.5Practical Walkthrough Using Loss and ACK Traces to Validate Recovery

RFC 9002Runnable lab

Learning objective. Read a real loss-and-recovery trace end to end (confirming that reliability held, that loss was detected promptly, and that the congestion window responded the way Chapter 3 says it should) so you can validate recovery on your own captures.

The lab: induce loss, capture the sender's view #

Everything in this chapter is observable. The lab in labs/03-5-loss-recovery/ runs a loopback transfer where the server streams 256 KB to the client but drops about one in eight of its own datagrams after the handshake. QUIC must still deliver every byte, and the server (the sender here) is the endpoint that runs loss detection and congestion control (§3.1§3.3), so its qlog records the whole story. Run it with the pinned toolchain:

BASH
uv run labs/03-5-loss-recovery/loss_recovery.py

Its genuine output (numbers vary a little per run):

captured output — uv run labs/03-5-loss-recovery/loss_recovery.pyTEXT
payload:        262144 bytes
transfer:       262144 bytes received -> complete (despite induced loss)
induced drops:  37 of 298 server datagrams (~1 in 8)
sender qlog:    [server] 299 packets sent, 37 declared lost + retransmitted

congestion window (sender) — the sawtooth of loss recovery:
  initial:   12000 B   (= kInitialWindow, 10 x max_datagram_size)
  peak:      25660 B   (grown by slow start / congestion avoidance)
  min:        2400 B   (halved into recovery on a congestion event)
  smoothed_rtt: 0.61 -> 1.13 ms (loopback)

Reading it against the chapter #

ReceiverSendercwnd = 12000 B(kInitialWindow),slow startsome data packets dropped on the pathrecovery:packet_lostlogged, cwnd halvedinto recoverya recovery-periodpacket is acked,enter congestionavoidanceunder sustained loss,cwnd bottoms at2400 B(kMinimumWindow-)data packets (many)ACKs, RTT samples, cwnd growstoward peakACK with gaps (missing packetnumbers)retransmit the lost frames (newpacket numbers)
Fig. 3.5-1What the trace shows. The window grows while ACKs flow, halves each time a loss is detected from an ACK gap, and — under sustained one-in-eight loss — bottoms out at the minimum window, all while every byte is eventually delivered.RFC 9002 §6, §7

Four things in that output validate that recovery worked exactly as designed:

  • Reliability held. All 262 144 bytes arrived even though 37 datagrams were dropped. The lost frames were resent in new packets and delivered (§3.1). This is the headline check: a correct QUIC stack never loses stream data to packet loss.
  • Loss was detected, not guessed. The sender logged 37 recovery:packet_lost events (one per dropped datagram), driven by the ACK gaps the client reported, not by blind timeouts. With loss this frequent, threshold detection (§3.1) does the work and the PTO rarely fires.
  • The window shows the sawtooth. It starts at exactly 12000 BkInitialWindow, 10 × the 1200-byte max datagram size (constants.md §8). It grows to a peak while ACKs flow, and each detected loss halves it into recovery (§3.3). Under sustained one-in-eight loss it bottoms at 2400 B, which is kMinimumWindow (2 × max_datagram_size): the controller refuses to shrink below that floor.
  • RTT stayed sane. smoothed_rtt is sub-millisecond (loopback), confirming the RTT estimator tracked the path rather than being thrown off by the retransmissions.

What "healthy recovery" looks like #

On a real capture, the same four signals tell you whether recovery is behaving. Loss events should track actual drops, not exceed them (excess means spurious retransmissions: the sender resending packets that were only reordered, §3.4). Time from a loss to its retransmission should sit near one RTT, not one PTO. A PTO-length gap means threshold detection failed to fire, usually because ACKs stopped. And the congestion window should recover between losses rather than flatlining at kMinimumWindow. A window pinned at the floor means the path is in or near persistent congestion (§3.2).

In practice

editorial Load a qlog into qvis (qvis.quictools.info, run locally for offline safety) and open the congestion view to see this window trajectory as an actual sawtooth over time, with loss events marked. Far easier to read than raw JSON. When a transfer is "slow," this view answers the first question immediately: is the sender loss-limited (deep, frequent sawtooth), RTT-limited (window fine, throughput still low), or application-limited (window never fills)? Chapter 11 (§11.3) builds this into a repeatable measurement workflow.

Takeaways #

A recovery trace is validated by four checks: all data arrived, losses were detected from ACKs rather than timeouts, the congestion window shows a halving-and-recovery sawtooth bounded by kMinimumWindow, and RTT estimates stayed stable. You have now seen QUIC detect and recover from loss on a real connection, closing the reliability chapter. Chapter 4 turns from whether data arrives to how it is organized — streams, flow control, and multiplexing for performance.