QUIC’s acknowledgment delay mechanism is actually a sophisticated backpressure system designed to reduce network overhead and improve throughput, not just a simple timer.

Let’s watch QUIC in action with a simulated connection. Imagine two clients, client-a and client-b, sending data to a server, server-x.

# On client-a
curl --http3 https://server-x/large-file

# On client-b
curl --http3 https://server-x/another-large-file

On server-x, we can observe the incoming QUIC packets and their corresponding acknowledgments. We’ll use tshark to capture and filter QUIC traffic.

# Capture QUIC traffic on server-x (UDP port 443)
sudo tshark -i eth0 -f "udp port 443" -Y "quic.ack_delay" -T fields -e frame.number -e ip.src -e quic.ack_delay -e quic.packet_number

You’ll see output like this, showing packets being received and their associated ACK delay values:

150    192.168.1.101    0.002000    12345
155    192.168.1.101    0.000000    12346
162    192.168.1.102    0.003000    56789
168    192.168.1.101    0.000000    12347
175    192.168.1.102    0.001000    56790

The quic.ack_delay field here represents the actual delay, in microseconds, that the receiver waited before sending an acknowledgment. Notice how it’s not always zero. This is the core of the mechanism.

The problem QUIC’s ACK delay solves is the "ACK explosion" common in TCP. In TCP, every packet received (or a small group of packets) often triggers an immediate acknowledgment. When you have many connections sending data concurrently, this can lead to a massive number of ACK packets flooding the network, consuming bandwidth that could otherwise be used for actual data. QUIC’s ACK delay allows a receiver to batch multiple incoming data packets and send a single acknowledgment that covers all of them, significantly reducing ACK overhead.

Internally, when a QUIC endpoint receives a data packet, it doesn’t immediately send an ACK. Instead, it starts a timer. If other data packets arrive before this timer expires, the endpoint can include their packet numbers in the same ACK frame. The timer’s maximum value is configurable, often referred to as max_ack_delay. This value is advertised by the client and server during the handshake.

The max_ack_delay is a crucial tuning parameter. A smaller max_ack_delay (e.g., 10ms) leads to faster ACKs and quicker loss detection, which is good for latency-sensitive applications on lossy networks. A larger max_ack_delay (e.g., 50ms or 100ms) increases ACK batching, reducing overhead, which is beneficial for high-bandwidth, stable networks or when dealing with many connections.

To configure this on a server like Nginx with QUIC support, you might see directives in your nginx.conf:

http2 {
    listen 443 http2 quic reuseport;
    listen [::]:443 http2 quic reuseport;

    # Default is 25ms
    quic_max_ack_delay 50ms;
    # ... other http2/quic settings
}

Here, quic_max_ack_delay 50ms; tells the server to wait up to 50 milliseconds before sending an acknowledgment if no new data packets arrive. This allows it to potentially bundle ACKs for multiple incoming data packets within that window.

The actual ack_delay value observed in packet captures will be the minimum of the determined delay and the negotiated max_ack_delay for that connection. The receiver also has logic to send an ACK sooner if it believes a packet might be lost, preventing excessive delays that could harm loss detection.

What many people don’t realize is that the ack_delay isn’t just a static configuration. The QUIC protocol dynamically adjusts the effective ACK delay based on observed network conditions and packet loss. If the receiver detects packet loss, it will reduce its ACK delay to speed up retransmission of lost data. This dynamic adjustment is key to QUIC’s resilience.

The next logical step after tuning ACK delay is understanding how to monitor and optimize the PTO (PTO) or Path Maximum Transmission Unit (PMTU) discovery within QUIC.

Want structured learning?

Take the full Quic course →