HTTP/3’s multiplexing isn’t about sending more data at once, it’s about preventing one slow request from blocking all the others.

Let’s watch this happen with a simple curl command. We’ll simulate a slow response from the server for one of our requests.

First, set up a quick HTTP/3 server. If you don’t have nghttp installed, grab it.

# On macOS with Homebrew
brew install nghttp2

# On Ubuntu
sudo apt update && sudo update nghttp2

Now, start a basic HTTP/3 server. This one will serve a simple HTML file.

nghttpd -v -p 8443 --ssl-cert=cert.pem --ssl-key=key.pem 8443

You’ll need cert.pem and key.pem. You can generate self-signed ones for testing:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes -subj "/CN=localhost"

Now, let’s hit this server with two curl requests. The first one will be normal, the second will simulate a delay. We’ll use curl’s --parallel option to send them concurrently.

curl --http3 -s https://localhost:8443/ -o /dev/null &
curl --http3 -s "https://localhost:8443/delay/5" -o /dev/null &
wait

The /delay/5 endpoint needs to be added to nghttpd. You can’t do that directly with nghttpd. For a real demonstration, we’d use a framework that supports custom handlers, like hyper in Rust or aioquic in Python. But the principle remains: one slow request.

Let’s imagine our server does have a /delay/5 endpoint that takes 5 seconds to respond.

With HTTP/1.1, if you run those two curl commands sequentially:

# HTTP/1.1 - Sequential
curl -s https://localhost:8443/ -o /dev/null
curl -s https://localhost:8443/delay/5 -o /dev/null

The second request won’t even start until the first one finishes. If they were on separate connections, they could run in parallel, but a single connection would be blocked.

Now, with HTTP/3 and its multiplexing over QUIC, here’s what happens:

# HTTP/3 - Parallel (conceptually)
curl --http3 -s https://localhost:8443/ -o /dev/null &
curl --http3 -s "https://localhost:8443/delay/5" -o /dev/null &
wait

Both requests are sent over the same QUIC connection. Even though the second request is taking 5 seconds to process on the server, the first request’s response can still be delivered immediately. The underlying QUIC transport handles interleaving the data streams. This is the magic of multiplexing.

The core problem HTTP/3 multiplexing solves is "Head-of-Line (HOL) blocking." In HTTP/1.1, if a request on a single connection is slow, it blocks all subsequent requests on that same connection. Even with HTTP/2, HOL blocking could occur at the TCP level: if a TCP packet for one stream was lost, all other streams on that same TCP connection would also stall until the lost packet was retransmitted, even if their data was ready.

HTTP/3, built on QUIC, solves this by performing multiplexing at the application layer over UDP. Each HTTP/3 stream is independently tracked. If a UDP packet carrying data for multiple streams is lost, only the stream(s) whose data was in that specific packet will pause. Other streams on the same QUIC connection can continue to make progress.

Here’s the mental model: Think of a highway.

  • HTTP/1.1 (single lane): One car breaks down, the whole road is blocked.
  • HTTP/1.1 (multiple lanes, but shared traffic lights): Each car on a separate lane might be fine, but if they all have to pass through the same set of traffic lights, one slow car can still cause a backup for everyone behind it.
  • HTTP/2 (multiple lanes, shared traffic lights still problematic): Even with multiple lanes, if one lane has a major accident (packet loss at TCP level), all lanes might have to stop and wait for cleanup.
  • HTTP/3 (multiple lanes, independent traffic lights): Each lane has its own independent traffic light. If one car has an issue, it only affects its lane. Other lanes keep moving.

The actual levers you control are largely on the server side configuration for your web server or application framework. For clients like curl, it’s just about enabling --http3.

For a server, enabling HTTP/3 means configuring it to listen on UDP ports, handle QUIC handshake, and manage multiple streams. Many modern servers (Nginx, Caddy, etc.) support this. When you configure them, you’re essentially telling them to set up QUIC listeners. The application logic then just deals with individual requests without worrying about how they are being interleaved at the transport layer.

One thing most people don’t realize is that HTTP/3’s multiplexing is stream-level, not connection-level in the same way TCP works. While QUIC establishes a connection, the multiplexing happens on logical streams within that connection. So, if you send 10 requests over a single HTTP/3 connection, they are processed as 10 independent streams. A lost packet affecting stream 3 doesn’t stop streams 1, 2, 4, 5, etc., from delivering their data as soon as it’s ready. This is a fundamental shift from TCP’s byte-stream orientation.

The next concept you’ll likely run into is how HTTP/3 handles connection migration, allowing clients to change IP addresses or ports without breaking the connection.

Want structured learning?

Take the full Http3 course →