Learning objective. Understand how QUIC's idle timeout is computed from both endpoints'
max_idle_timeout declarations, why it has a 3×PTO floor, how keepalive works (there is no
dedicated frame; you send a PING), and how a silent idle close differs from an immediate close.
The effective timeout is the minimum of the two #
max_idle_timeout (0x01) is a duration in milliseconds, and like every transport parameter it
is a unilateral declaration (§7.1), but this one combines with the peer's in a
specific way:
"Each endpoint advertises a max_idle_timeout, but the effective value at an endpoint is computed as the minimum of the two advertised values (or the sole advertised value, if only one endpoint advertises a non-zero value)." ([RFC 9000 §10.1])
So if endpoint A offers 30 s and B offers 10 s, both run on a 10 s idle timeout — the shorter declaration wins for everyone. A value of 0 (or the parameter's absence) means "no limit from my side"; the timeout is disabled entirely only when both endpoints omit it or send 0 ([RFC 9000 §18.2]). By advertising a timeout, an endpoint also commits to closing the connection immediately if it gives up before that time, so the peer's expectation stays accurate ([RFC 9000 §10.1]).
What resets the timer #
The idle timer is reset by activity in either direction ([RFC 9000 §10.1]):
- An endpoint restarts its idle timer when it receives and successfully processes a packet from the peer.
- It also restarts the timer when it sends an ack-eliciting packet, provided no other ack-eliciting packet has been sent since it last received one (so a burst of sends doesn't keep resetting it artificially).
There is a safety floor. To avoid a pathologically short effective timeout on a slow path,
endpoints "MUST increase the idle timeout period to be at least three times the current Probe
Timeout (PTO)" ([RFC 9000 §10.1]). This guarantees room for several probes to be sent and lost,
tying loss recovery (§3.2) to connection longevity, before the connection is
declared idle. The effective timeout is therefore max(min(local, peer), 3 × PTO).
Keepalive: there is no keepalive frame #
QUIC has no dedicated keepalive mechanism. To hold a connection open through a lull, an endpoint
sends an ack-eliciting packet, in practice a PING frame (0x01), whose entire job is to
elicit an ACK ([RFC 9000 §19.2]). Receiving it resets the peer's idle timer:
"an endpoint could send a PING frame periodically, which will cause the peer to restart its idle timeout period." ([RFC 9000 §10.1.2])
Two cautions shape how often. Too frequent, and "unnecessary sending of PING frames could have a detrimental effect on performance" ([RFC 9000 §10.1.2]). Every keepalive is a real packet that can trigger ACKs and congestion accounting. Too infrequent, and NAT/firewall state expires: experience shows "sending packets every 30 seconds is necessary to prevent the majority of middleboxes from losing state for UDP flows" ([RFC 9000 §10.1.2]). The practical keepalive interval is therefore below both the effective idle timeout and the ~30 s middlebox horizon.
Idle close vs immediate close #
The two ways a QUIC connection ends differ in whether anything is sent ([RFC 9000 §10]):
- Idle timeout is a silent close: when the timer expires, "the connection is silently closed and its state is discarded" ([RFC 9000 §10.1]). No packet goes out — the peer discovers it independently when its own timer fires. This is why the timeout must be symmetric-ish: both sides need to give up around the same time.
- Immediate close sends a
CONNECTION_CLOSEframe (0x1c/0x1d), tearing down all streams at once ([RFC 9000 §10.2]). The sender enters a closing state and the receiver a draining state, each held for about 3×PTO to absorb stray packets, before the state is dropped. Protocol violations and application shutdown take this path.
Worked example: a long-lived idle API connection #
Follow Figure 7.3-1. A declares 30 s, B declares 10 s, so both run a 10 s effective timeout. A
sends a request; B's timer resets. Then the application goes quiet. At 8 s (safely under 10 s)
A sends a PING; B receives it and resets to a fresh 10 s, and B's ACK (ack-eliciting handling
aside) keeps A's side lively too. The connection survives the lull on one cheap packet. If instead
A had stayed silent, at 10 s both timers would expire and each side would silently discard the
connection — no CONNECTION_CLOSE, no goodbye. Reopening later would mean a fresh handshake (or a
0-RTT resumption, §2.3).
A flow-control stall can masquerade as idleness. If a sender is blocked on flow control with
nothing ack-eliciting in flight, no packets flow, and the idle timer can expire and kill a
connection that is merely waiting for credit. The fix is the DATA_BLOCKED/STREAM_DATA_BLOCKED
signal from §7.2: announcing "I'm blocked" is also an ack-eliciting packet that keeps
the connection alive. Silence is indistinguishable from death to the idle timer.
editorial Choose max_idle_timeout for how quickly you want
dead peers reclaimed, then set keepalive below it based on whether you must survive NATs. A
mobile client that wants a connection to persist across screen-off lulls should keepalive at
15–25 s (under the ~30 s middlebox limit) even though that costs packets on radio; a data-center
service with no NATs and cheap reconnects can skip keepalive entirely and let idle connections
die at a short timeout to free memory. Don't set a very long idle timeout as a substitute for
keepalive: the middlebox will drop your flow long before the timeout fires, and you'll discover
it only on the next doomed send.
Takeaways #
The effective idle timeout is the minimum of the two advertised max_idle_timeout values, floored
at 3×PTO, and it resets on received packets and ack-eliciting sends. QUIC has no keepalive frame:
you send a PING, spaced under both the timeout and the ~30 s middlebox horizon. Idle expiry closes
the connection silently, unlike the CONNECTION_CLOSE of an immediate close. Idle timeout is the
last of the core transport parameters; §7.4 turns from configuring a connection to
choosing which QUIC version it even speaks.