Know What Your Bytes Ride On Before You Optimize It
Network Protocols From HTTP/1.1 to HTTP/3
How HTTP/1.1, HTTP/2, and HTTP/3 actually move bytes — keep-alive, multiplexing, head-of-line blocking, the TLS handshake tax, and where gRPC fits.
What you'll learn
- Compare HTTP/1.1, HTTP/2, and HTTP/3 connection models
- Explain head-of-line blocking at the TCP and HTTP layers
- Reason about TLS handshake cost and when gRPC's HTTP/2 transport pays off
Every request in your system rides on a transport protocol, and the protocol quietly decides how many round trips you pay, how requests interleave, and what happens when one packet is lost. You can’t tune what you don’t understand — so before we talk APIs, let’s get concrete about what HTTP actually does on the wire, version by version.
HTTP/1.1: one request at a time per connection
HTTP/1.1 is a text protocol over a single TCP connection, and its defining limitation is that a connection handles one request/response at a time. You send a request, you wait for the full response, then you send the next.
The big win 1.1 added over 1.0 was keep-alive: reuse the same TCP connection for many requests instead of opening a fresh one each time. That matters because opening a connection isn’t free — it costs a TCP handshake (one round trip) plus, for HTTPS, a TLS handshake (one or two more).
But keep-alive doesn’t make a single connection concurrent. Browsers worked around this by opening ~6 parallel connections per origin. Six connections means six handshakes, six congestion-control state machines, and a hard ceiling on parallelism. Worse, it suffers head-of-line (HOL) blocking: a slow response at the front of a connection holds up everything queued behind it.
HTTP/2: multiplexing over one connection
HTTP/2 keeps the same semantics (methods, headers, status codes) but changes the wire format to binary frames. The key feature is multiplexing: many independent streams share one TCP connection, and their frames interleave.
Notice /b can come back before /a — streams are independent at the HTTP
layer. HTTP/2 also adds header compression (HPACK) to avoid re-sending bulky
cookie/header sets on every request, and lets the server prioritize streams.
The catch: HTTP/2 still runs over TCP, and TCP delivers bytes strictly in order. If one packet is lost, the kernel holds all streams’ data until that packet is retransmitted. So HTTP/2 fixed application-layer HOL blocking but inherited transport-layer HOL blocking from TCP.
HTTP/3: HTTP over QUIC
HTTP/3 swaps the transport entirely. Instead of TCP it uses QUIC, a protocol built on UDP that implements its own streams, reliability, and congestion control in user space. Because QUIC understands streams natively, a lost packet only stalls its own stream — the others keep flowing. That kills transport-layer HOL blocking, the one thing HTTP/2 couldn’t fix.
QUIC also folds TLS into the transport handshake, so connection setup is faster (often 1 round trip, and 0-RTT on resumption), and it supports connection migration — a phone moving from Wi-Fi to cellular keeps the same connection instead of starting over.
| Property | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| Transport | TCP | TCP | QUIC (UDP) |
| Concurrency | ~6 conns/origin | multiplexed streams | multiplexed streams |
| App-layer HOL blocking | Yes | No | No |
| Transport HOL blocking | Yes | Yes | No |
| Header compression | No | HPACK | QPACK |
| Handshake | TCP + TLS separately | TCP + TLS separately | combined, 0-RTT capable |
The TLS handshake tax
Every HTTPS connection pays a setup cost before any application byte moves: a TCP handshake (1 RTT), then a TLS handshake (1 RTT in TLS 1.3, 2 in TLS 1.2). On a cross-region link where one RTT is ~150ms, that’s 150–450ms of pure setup before your first byte. This is exactly why connection reuse matters so much — and why HTTP/3’s combined, resumable handshake is a real latency win for chatty clients.
gRPC rides on HTTP/2
gRPC is an RPC framework that uses HTTP/2 as its transport and Protocol Buffers as its binary serialization. It leans directly on HTTP/2’s streaming to offer four call styles: unary, server-streaming, client-streaming, and bidirectional streaming. The compact binary framing and multiplexing make it a strong fit for internal service-to-service traffic, where you control both ends and want low overhead. We compare it head-to-head with REST and GraphQL in the next lesson.
The JavaScript angle
Node’s http2 module speaks HTTP/2 natively, and you opt into protocol
negotiation rather than getting it for free. With ALPN (Application-Layer
Protocol Negotiation) baked into TLS, the client and server agree on the highest
version both support during the handshake.
import http2 from 'node:http2';
import { readFileSync } from 'node:fs';
// Server: one secure HTTP/2 endpoint.
const server = http2.createSecureServer({
key: readFileSync('key.pem'),
cert: readFileSync('cert.pem'),
});
server.on('stream', (stream, headers) => {
// Each request is a stream on the SAME connection.
stream.respond({ ':status': 200, 'content-type': 'application/json' });
stream.end(JSON.stringify({ path: headers[':path'] }));
});
server.listen(8443);
// Client: open ONE session, fire many concurrent requests over it.
const client = http2.connect('https://localhost:8443');
for (const path of ['/a', '/b', '/c']) {
const req = client.request({ ':path': path }); // multiplexed stream
let body = '';
req.on('data', (chunk) => (body += chunk));
req.on('end', () => console.log(path, body));
} Most Node apps still terminate HTTP/2 and HTTP/3 at a reverse proxy (nginx, Caddy, or a cloud load balancer) and speak plain HTTP/1.1 to the Node process behind it. That’s a perfectly good pattern — the proxy gets you modern transport and TLS termination without rewriting your app servers.
With the transport understood, the next question is what shape of API you build on top of it: REST, GraphQL, or gRPC.