HTTP/3 isn’t just a faster HTTP; it’s fundamentally a different protocol that trades TCP’s reliable, ordered delivery for UDP’s speed and flexibility, using QUIC to bring back only what’s needed.

Let’s see it in action. Imagine two microservices, service-a and service-b, communicating.

First, service-a needs to initiate a connection to service-b. Instead of a TCP handshake, service-a sends a QUIC "initial" packet to service-b’s IP address and port. This packet contains a unique Connection ID. service-b receives this and, if it’s willing to accept the connection, responds with its own QUIC packet, also containing a Connection ID. This establishes a secure, encrypted QUIC connection using TLS 1.3, all before any application data is sent. This is the magic of QUIC’s "0-RTT" or "1-RTT" handshake – significantly faster than TCP’s three-way handshake plus TLS.

Now, service-a wants to send a request to service-b. It constructs an HTTP/3 request, which is then framed within QUIC. QUIC packets are sent over UDP. The key here is that QUIC streams are independent. If service-a sends two requests, Request 1 and Request 2, and Request 1’s data packet gets delayed or lost, Request 2’s data packets can still be delivered and processed by service-b without waiting for Request 1 to be fully retransmitted. This eliminates the "head-of-line blocking" problem that plagues TCP.

Here’s a simplified conceptual view of how service-b might handle incoming QUIC streams from service-a:

{
  "connection_id_from_a": "0xabc123",
  "streams": {
    "stream_0": { // Request 1
      "header": {
        "method": "GET",
        "path": "/resource/1",
        "headers": {
          "host": "service-b.local",
          "user-agent": "service-a/1.0"
        }
      },
      "data_chunks": [
        {"offset": 0, "payload": "..."}, // May arrive out of order
        {"offset": 1024, "payload": "..."}
      ],
      "state": "receiving_data" // or "complete", "processing"
    },
    "stream_1": { // Request 2
      "header": {
        "method": "POST",
        "path": "/resource/2",
        "headers": {
          "host": "service-b.local",
          "content-type": "application/json"
        }
      },
      "data_chunks": [
        {"offset": 0, "payload": "{\"key\": \"value\"}"}
      ],
      "state": "receiving_data"
    }
  }
}

service-b’s QUIC implementation manages these independent streams. When a stream’s data is complete and in order (QUIC handles reordering and retransmission per stream), service-b can then process the HTTP/3 request. The HTTP/3 framing itself is built on HPACK for header compression (similar to HTTP/2 but adapted for QUIC) and uses different frame types for headers, data, and control messages, all multiplexed over the QUIC streams.

The problem this solves is the inherent latency and blocking in traditional TCP-based communication, especially in high-latency or lossy network environments common in distributed systems. TCP’s single, ordered byte stream means a single lost packet can stall all subsequent data, even if it belongs to a different logical request. HTTP/3, via QUIC, decouples these logical requests into independent streams, allowing them to make progress even if others are encountering network issues.

The levers you control are primarily at the application and infrastructure levels:

  1. Service Configuration: Your microservices need to be built with HTTP/3 and QUIC support. This means using libraries like nghttp3/ngtcp2 (C/C++), quic-go (Go), or neqo (Rust), or framework integrations like those emerging in Spring WebFlux, Netty, or Envoy. You’ll configure your services to listen on UDP ports for QUIC, specify TLS certificates, and potentially tune QUIC-specific parameters like initial congestion window sizes or idle timeouts.
  2. Load Balancer/Gateway: If you’re using an API Gateway or Load Balancer (e.g., Envoy, Nginx, HAProxy, cloud provider LBs), it must support HTTP/3. This is where the UDP traffic will be terminated and potentially re-originated as HTTP/3 or even downgraded to HTTP/1.1 or HTTP/2 if backend services don’t yet support it. You’ll configure your LB to expose UDP ports and handle TLS termination for QUIC.
  3. Network Policy/Firewalls: Since HTTP/3 uses UDP, you must ensure your firewalls and network policies allow UDP traffic on the ports your services are listening on. This is often overlooked, as most policies are TCP-centric. You’ll need to explicitly open UDP ports, e.g., sudo ufw allow 443/udp.
  4. TLS Certificates: QUIC relies on TLS 1.3 for security. You’ll need valid TLS certificates configured for your services or load balancers.
  5. Application Code: While the protocol handles much of the complexity, your application code might need minor adjustments to leverage stream prioritization or handle potentially out-of-order application-level data if you’re building custom protocols on top of HTTP/3.

The most surprising thing about HTTP/3’s performance gains is that they aren’t solely due to faster handshakes or better multiplexing; they’re largely a consequence of how QUIC’s stream-level error handling and congestion control prevent head-of-line blocking. In TCP, a single lost packet on a connection means all data on that connection stops until retransmission. QUIC, however, retransmits lost packets only for the specific stream they belong to, allowing other streams on the same QUIC connection to continue flowing unimpeded. This makes it far more resilient to packet loss, a common issue in real-world networks.

The next hurdle you’ll face is understanding how to effectively manage connection IDs and their lifecycle, especially in scenarios involving NAT rebinding or client IP changes.

Want structured learning?

Take the full Http3 course →