QUIC and HTTP/3

Understand QUIC protocol and HTTP/3 for next-generation web performance. Covers connection establishment, multiplexing improvements over HTTP/2, migration support, and the patterns that reduce latency and improve reliability on modern networks.

QUIC and HTTP/3 are modern transport and application layer protocols that together reduce latency, improve reliability, and enable efficient multiplexing for web and real-time applications. They matter when you need fast, resilient, and concurrent connections—especially over mobile networks, high-latency links, or unreliable paths where TCP’s limitations become apparent. Use them when your service is latency-sensitive: loading assets, streaming video, handling real-time updates, or serving APIs with many small requests.

Establishing a QUIC Connection

To initiate a QUIC connection, a client sends an initial packet with a QUIC header containing a 64-bit connection ID, a version number, and a set of transport parameters. The server responds with its own QUIC header and a 0-RTT or 1-RTT handshake, depending on whether it can use cached session information.

The QUIC handshake is built on TLS 1.3 and includes the following handshake types:

A client can configure QUIC parameters using quic-transport-parameters in the TLS handshake. For example, in a ClientHello:

{
  "initial_max_streams_bidi": 100,
  "initial_max_streams_uni": 100,
  "max_idle_timeout_ms": 60000,
  "max_udp_payload_size": 1350,
  "ack_delay_exponent": 3,
  "max_ack_delay_ms": 25,
  "disable_active_migration": false
}

The server responds with its own parameters, which become the connection’s active settings.

Multiplexing HTTP/3 Streams Over QUIC

HTTP/3 uses QUIC as its underlying transport, enabling multiple independent streams within a single QUIC connection. Each stream carries one HTTP request or response, and streams are numbered and bidirectional by default.

To send an HTTP/3 request, a client sends a HEADERS frame followed by DATA frames:

HEADERS
  stream_id: 3
  flags: END_STREAM
  header_block: [
    ":method: GET",
    ":path: /api/users",
    ":scheme: https",
    ":authority: api.example.com"
  ]

DATA
  stream_id: 3
  flags: END_STREAM
  data: "[]"

The server responds with a HEADERS frame and one or more DATA frames, possibly interleaved with other stream data.

Key frame types in HTTP/3:

Streams are identified by a 64-bit integer and can be:

A client can create a new stream with a STREAM frame:

STREAM
  stream_id: 5
  flags: END_STREAM
  data: "GET /api/posts HTTP/3"

Configuring QUIC and HTTP/3 in Real Environments

In a production environment using nginx as an HTTP/3 server, the following configuration enables QUIC and HTTP/3 on port 443:

server {
    listen 443 http3 reuseport;
    listen 443 ssl reuseport;

    ssl_certificate_file /etc/ssl/certs/example.com.crt;
    ssl_certificate_key_file /etc/ssl/private/example.com.key;

    quic;
    quic_max_idle_timeout 60s;
    quic_max_udp_payload_size 1350;
    quic_initial_max_streams_bidi 100;
    quic_initial_max_streams_uni 100;
    quic_max_ack_delay 25ms;
    quic_ack_delay_exponent 3;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 3.0;
    }
}

The http3 directive enables HTTP/3 over QUIC. The reuseport directive allows multiple worker processes to bind to the same port, improving load distribution across QUIC connections.

A client using curl to test an HTTP/3 endpoint:

curl -v --http3 https://api.example.com

This command uses quic and http3 by default if curl is built with ngtcp2 support. The output includes QUIC handshake details:

*   Trying 203.0.113.1:443...
*   Connected to api.example.com (203.0.113.1) port 443 (#0)
*   ALPN, offering h3, h2, h1
*   QUIC: version: 0x00000001
*   QUIC: connection ID: 0x1a2b3c4d5e6f7a8b
*   QUIC: handshake completed in 123 ms
*   HTTP/3 200 OK
*   date: Mon, 01 Jan 2024 00:00:00 GMT
*   content-type: application/json
*   {"users": [...]}

Handling Connection Migration and Mobility

QUIC enables seamless connection migration when a client changes network interfaces—e.g., switching from Wi-Fi to LTE. The connection ID remains constant, so the server can route packets to the new IP address without re-establishing the connection.

To support migration, the client must:

A PATH_CHALLENGE frame includes a 64-bit challenge token. The server responds with the same token in a PATH_RESPONSE frame.

The server can detect a new path by:

When a client migrates, the server continues to send packets to the old IP address until the new path is confirmed. The client can send a NEW_CONNECTION_ID frame to advertise new connection IDs.

PATH_CHALLENGE
  length: 8
  data: 0x123456789abcdef0
PATH_RESPONSE
  length: 8
  data: 0x123456789abcdef0

Stream and Connection-Level Flow Control

QUIC implements both stream-level and connection-level flow control using MAX_DATA and MAX_STREAM_DATA frames.

Each stream maintains its own flow control window. The sender can adjust it with:

MAX_STREAM_DATA
  stream_id: 7
  max_data: 65536

The connection-wide flow control is managed by:

MAX_DATA
  max_data: 1048576

If a receiver sends a MAX_DATA frame with a value larger than the current window, the sender can send more data without waiting for acknowledgments.

Flow control is critical during high-throughput transfers. A common failure mode: a client sends large payloads on a stream but the server does not send MAX_STREAM_DATA updates, causing the client to stall after sending 64 KB of data.

Another subtle failure: a server sends a MAX_DATA frame, but the client does not update its flow control window until the next packet. This results in underutilized bandwidth and increased latency.

Error Handling and Recovery

QUIC defines a set of error codes and transport-level error frames.

A STOP_SENDING frame indicates that a receiver wants to stop receiving data on a stream:

STOP_SENDING
  stream_id: 11
  error_code: 0x00000001
  final_offset: 4096

A RESET_STREAM frame signals that a stream has been closed with an error:

RESET_STREAM
  stream_id: 11
  error_code: 0x00000002
  final_offset: 4096
  application_error_code: 0x0102

Common error codes:

A server may close a stream with:

RESET_STREAM
  stream_id: 7
  error_code: 0x00000004
  final_offset: 1024
  application_error_code: 0x1001

This tells the client that the stream was closed due to a timeout, and the client can resume or retry the request.

Detecting and Responding to Packet Loss

QUIC uses an adaptive loss detection mechanism based on RTT (round-trip time). The sender maintains a RTT value and detects loss when packets are not acknowledged within a window.

The loss detection algorithm uses:

A QUIC packet includes:

When a packet is lost, the sender can use:

The server sends ACK frames to confirm receipt:

ACK
  largest_acknowledged: 1024
  ack_delay: 15
  ack_range_count: 2
  ack_ranges: [
    { start: 1000, end: 1010 },
    { start: 950, end: 975 }
  ]

A common failure: a client sends a large request with multiple DATA frames, but the server fails to send an ACK frame after 25 ms. The client detects loss and retransmits the first 100 packets, but the server does not acknowledge them. The result is a chain of retransmissions and a backlog of unacknowledged data.

Managing HTTP/3 Settings and Parameters

HTTP/3 uses a SETTINGS frame to configure behavior at the connection level. The following SETTINGS frame configures several parameters:

SETTINGS
  settings: [
    { setting_id: 0x0001, value: 1000 },
    { setting_id: 0x0002, value: 500 },
    { setting_id: 0x0003, value: 8192 },
    { setting_id: 0x0004, value: 1048576 }
  ]

Common setting IDs:

A server can dynamically update settings during the connection:

SETTINGS
  settings: [
    { setting_id: 0x0001, value: 1 },
    { setting_id: 0x0003, value: 16384 }
  ]

This enables server push and increases the maximum header table size.

Troubleshooting QUIC and HTTP/3

Common issues and their solutions:

QUIC and HTTP/3 work best when the entire stack—from client to server to network—supports the full feature set. When they fail, it is often not due to missing features, but due to subtle misconfigurations in flow control, error handling, or migration.

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.