⏱️ Lectura: 12 min

A browser using HTTP/1.1 opens up to six TCP connections per domain to load a single page; with HTTP/2 it opens just one and sends hundreds of requests over it at the same time, without blocking each other.

📑 En este artículo
  1. TL;DR
  2. What HTTP/2 Is and Why It Matters
    1. From SPDY to HTTP/2
  3. How Multiplexing Works in HTTP/2
    1. The Structure of a Frame
  4. HPACK: How Headers Get Compressed
  5. Practical Examples
  6. How to Enable It Step by Step
  7. Real-World Use Cases
  8. Common Mistakes and Best Practices
  9. Comparison with Alternatives
  10. Going Deeper: Priorities and Flow Control
  11. Frequently Asked Questions
    1. Does HTTP/2 Require HTTPS?
    2. Does HTTP/2 Automatically Make Any Site Faster?
    3. What’s the Difference Between HPACK and Compressing the Body with gzip?
    4. Why Does Head-of-Line Blocking Exist If HTTP/2 Multiplexes Requests?
    5. Can gRPC Work Without HTTP/2?
    6. How Do I Know If a Third-Party Site Uses HTTP/2 Without Access to Its Server?
  12. References

Published in 2015 as RFC 7540, HTTP/2 took Google’s SPDY protocol and turned it into an IETF standard. This article explains how its binary layer works, how it compresses headers with HPACK, and how to verify on your own server that HTTP/2 multiplexing is actually active.

TL;DR

  • You’ll understand how HTTP/2 multiplexes dozens of requests over a single TCP connection.
  • You’ll be able to read an HTTP/2 frame capture and distinguish HEADERS, DATA, and SETTINGS.
  • You’ll know how to configure Nginx and Node.js to serve content over HTTP/2 with TLS.
  • You’ll understand how HPACK compresses repeated headers using static and dynamic tables.
  • You’ll be able to verify with curl and openssl whether a server actually negotiated HTTP/2.
  • You’ll identify TCP-level head-of-line blocking, the reason behind HTTP/3.
  • You’ll know when migrating to HTTP/2 changes nothing and when performance actually matters.

What HTTP/2 Is and Why It Matters

HTTP/1.1 processes one request at a time per TCP connection. If you request ten files, the browser needs up to ten simultaneous connections or has to queue them. HTTP/2 solves this with a binary framing layer: instead of sending plain text line by line, it splits each message into binary frames tagged with a stream number, and all those frames travel interleaved over the same TCP connection.

This matters because the bottleneck of a modern website is almost never bandwidth: it’s the number of connections and the order in which resources arrive. A site with 80 JS, CSS, and image files benefits from sending those 80 requests in parallel over a single socket instead of taking turns across six queues.

From SPDY to HTTP/2

Google started experimenting with SPDY in Chrome before a standard existed. SPDY already introduced multiplexing and header compression, but it used the generic DEFLATE algorithm, the same compression family that ended up enabling the CRIME attack against TLS sessions. The IETF’s HTTPbis working group took SPDY as a starting point, fixed that design flaw with HPACK, and published the result as RFC 7540 in May 2015. Chrome dropped SPDY support shortly after, leaving HTTP/2 as its sole successor.

Comparison of TCP connections between HTTP/1.1 and HTTP/2
HTTP/1.1 opens several connections; HTTP/2 multiplexes everything into one. Foto de BoliviaInteligente en Unsplash

How Multiplexing Works in HTTP/2

Each request/response in HTTP/2 lives inside a stream: a logical flow identified by an odd number (if opened by the client) or an even number (if opened by the server through push). A stream is made up of frames: HEADERS for the headers, DATA for the body, and control frames like SETTINGS, WINDOW_UPDATE, or RST_STREAM.

The protocol defines ten frame types in total: DATA, HEADERS, PRIORITY, RST_STREAM, SETTINGS, PUSH_PROMISE, PING, GOAWAY, WINDOW_UPDATE, and CONTINUATION. Each one carries a stream identifier that lets any endpoint reassemble the messages even if they arrive interleaved.

The Structure of a Frame

Every frame starts with a fixed 9-byte header: 3 bytes for the payload length (up to 16,777,215 bytes), 1 byte for the frame type, 1 byte of flags, and 4 bytes for the stream identifier. That fixed format lets a parser read the byte stream without looking for text delimiters like the line breaks in HTTP/1.1, and it’s one of the reasons decoding HTTP/2 is cheaper on CPU than parsing plain text.

sequenceDiagram
    participant N as Browser
    participant S as Server
    N->>S: HEADERS stream 1 (index.html)
    N->>S: HEADERS stream 3 (estilo.css)
    N->>S: HEADERS stream 5 (app.js)
    S-->>N: DATA stream 3 (estilo.css)
    S-->>N: DATA stream 1 (index.html)
    S-->>N: DATA stream 5 (app.js)
    Note over N,S: the streams are interleaved over the same connection

Each stream has its own flow control through WINDOW_UPDATE frames, so a heavy resource doesn’t hog the entire connection’s bandwidth. The original delivery order is recovered on the receiver’s side using each frame’s stream identifier, not the order of arrival on the wire.

HPACK: How Headers Get Compressed

HTTP headers repeat a lot between requests: user-agent, accept-encoding, cookie. Sending them in full on every request wastes bytes. HPACK (RFC 7541) solves this with two tables: a static table with 61 entries predefined by the standard, and a dynamic table that client and server fill in with headers already sent earlier in that same connection.

Some fixed entries in the static table: index 2 represents :method: GET, index 4 represents :path: /, and index 8 represents :status: 200. If your request uses exactly those values, HPACK sends a single byte per header instead of the full text string. If a header is new, it’s encoded with Huffman coding and added to the dynamic table for reuse on the next request.

flowchart LR
    A["Header: method GET"] --> B{"Exists in static table?"}
    B -->|"yes"| C["Send 1-byte index"]
    B -->|"no"| D{"Exists in dynamic table?"}
    D -->|"yes"| E["Send dynamic index"]
    D -->|"no"| F["Encode with Huffman and add to dynamic table"]
📌 Note: HPACK deliberately avoids the generic DEFLATE algorithm that SPDY used, because combining compression with secret data (like session cookies) in the same stream enabled the CRIME attack against TLS. HPACK separates indices from literals precisely to avoid reopening that problem.

Practical Examples

The simplest way to confirm a server speaks HTTP/2 is to request the response with curl, specifying the protocol:

curl -I --http2 -s https://www.google.com | head -n 5

If the ALPN negotiation worked, the first line of the response reads HTTP/2 200 instead of HTTP/1.1 200 OK. If your server doesn’t support HTTP/2 yet, curl silently falls back to HTTP/1.1 without showing any error.

To spin up a real HTTP/2 server, Node.js has native support in the http2 module. This example responds with an exact content header and closes the stream:

const http2 = require('node:http2');
const fs = require('node:fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem'),
});

server.on('stream', (stream, headers) => {
  console.log('Request to:', headers[':path']);
  stream.respond({
    ':status': 200,
    'content-type': 'text/plain; charset=utf-8',
  });
  stream.end('Hello from HTTP/2');
});

server.listen(8443);

The headers object uses colon-prefixed pseudo-headers (:path, :method, :status) because HTTP/2 splits the traditional status line into individual fields within the same HEADERS block compressed by HPACK.

On the client side, the Python httpx library negotiates HTTP/2 automatically if the server supports it:

import httpx

with httpx.Client(http2=True) as client:
    response = client.get('https://programacion.example/api/status')
    print(response.http_version)

That example requires installing the h2 extra with pip install httpx[http2]. If the server doesn’t support HTTP/2, httpx falls back to HTTP/1.1 without raising an exception.

How to Enable It Step by Step

In Nginx, the modern approach (1.25.1 onward) separates the http2 directive from listen:

server {
    listen 443 ssl;
    http2 on;
    ssl_certificate     /etc/nginx/certs/programacion.crt;
    ssl_certificate_key /etc/nginx/certs/programacion.key;
    server_name programacion.example;
}

In earlier versions of Nginx, the syntax was listen 443 ssl http2; on a single line. After reloading with nginx -s reload, confirm the binary has the module compiled in:

nginx -V 2>&1 | grep -o http_v2_module

And confirm the actual ALPN negotiation against the production server:

openssl s_client -alpn h2 -connect programacion.example:443 </dev/null 2>/dev/null | grep ALPN

If the output shows ALPN protocol: h2, the browser will use HTTP/2 on that connection. In Chrome DevTools, the Network tab has a “Protocol” column that shows h2 per request when it’s active.

Server processing multiple HTTP/2 streams in parallel
Each stream maintains its own flow control within the connection. Foto de Jainath Ponnala en Unsplash

Real-World Use Cases

gRPC, Google’s RPC framework, natively depends on HTTP/2: it uses its bidirectional streams to support data streaming in both directions over a single connection, something HTTP/1.1 can’t do without websockets. APIs with many small JSON responses and sites with dozens of static assets notice the biggest difference compared to HTTP/1.1.

Modern CDNs and API gateways have served HTTP/2 by default for years, so most web traffic already travels multiplexed without the developer configuring anything on the client side. Where configuration does matter is on the origin server: if your backend speaks plain HTTP/1.1 behind the CDN, you lose the multiplexing benefit on the final leg to the application server, even though the end user still benefits on the leg to the CDN.

On the other hand, an API that serves a single large response per connection, or an internal service with very few concurrent clients, gains little from migrating: the benefit of HTTP/2 is proportional to how many small, parallel resources need to be fetched.

Common Mistakes and Best Practices

  • Misused Server Push: HTTP/2 allowed the server to push resources without the client requesting them (PUSH_PROMISE frames). In practice, guessing wrong about which resources to push wasted bandwidth, and browsers gradually dropped support for it. Don’t design it as a central piece of your architecture.
  • TCP-level head-of-line blocking: even though HTTP/2 multiplexes at the application layer, it still travels over a single TCP connection. If a packet is lost, TCP blocks delivery of all streams until it’s retransmitted, even if other streams’ data has already arrived. This is exactly the problem HTTP/3 over QUIC solves by giving each stream its own loss control.
  • Ignored concurrent stream limit: the SETTINGS frame negotiates a maximum number of simultaneous streams per connection. If your client opens more requests than the server allows, they sit queued and waiting even though the connection is technically multiplexing: check the SETTINGS_MAX_CONCURRENT_STREAMS value if you notice unexpected bottlenecks.
  • Concatenating and sprite-ing assets is still useful, but less critical: with HTTP/1.1, combining files to save connections was almost mandatory; with HTTP/2, multiplexing makes serving many small files reasonable, though the overhead of each individual request doesn’t disappear entirely.

Comparison with Alternatives

ProtocolTransportMultiplexingHeader CompressionWhen to Use It
HTTP/1.1TCPNo (one request per connection)NoneCompatibility with very old clients
HTTP/2TCP + TLS (h2)Yes, at the application layerHPACKDefault standard today for most sites
HTTP/3QUIC over UDPYes, with no head-of-line blocking between streamsQPACKMobile networks or frequent packet loss

Going Deeper: Priorities and Flow Control

The original HTTP/2 specification included a dependency tree between streams to indicate priority (PRIORITY frames), but in practice browsers implemented it inconsistently. The IETF replaced it with a simpler scheme in RFC 9218, which expresses priority through a human-readable HTTP header (priority: u=1, i) instead of a complex binary tree.

Flow control works at two levels: per stream and for the entire connection. Each side announces how many bytes it’s willing to receive through a WINDOW_UPDATE frame, and the sender can’t send more DATA than the window allows until the receiver updates it. This prevents a single slow stream from saturating the buffer and blocking the others.

flowchart TD
    subgraph HTTP1["HTTP 1.1"]
    A1["Browser"] --> B1["TCP Connection 1"]
    A1 --> B2["TCP Connection 2"]
    A1 --> B3["TCP Connection 3"]
    B1 --> C1["Server"]
    B2 --> C1
    B3 --> C1
    end
    subgraph HTTP2["HTTP 2"]
    A2["Browser"] --> B4["Single TCP Connection"]
    B4 --> C2["Server"]
    end
💡 Tip: if you want to inspect raw HTTP/2 frames, Wireshark decodes them automatically when it has the TLS session key (export SSLKEYLOGFILE before running the browser).

📖 Summary on Telegram: View summary

Your next step: run openssl s_client -alpn h2 -connect your-domain:443 </dev/null 2>/dev/null | grep ALPN against your own site and confirm whether it’s already serving HTTP/2.

Frequently Asked Questions

Does HTTP/2 Require HTTPS?

The standard defines a plain-text variant called h2c, but in practice no major browser implements it. All of them require TLS (h2), so if you want HTTP/2 in the browser you need a certificate.

Does HTTP/2 Automatically Make Any Site Faster?

No. The benefit depends on how many small, parallel resources the page loads. A site with few large assets gains little compared to a well-configured HTTP/1.1 setup.

What’s the Difference Between HPACK and Compressing the Body with gzip?

They’re different things: gzip or brotli compress the response body (HTML, JSON, images). HPACK compresses only the headers, using reference tables instead of generic compression, precisely to avoid the vulnerability that affected SPDY.

Why Does Head-of-Line Blocking Exist If HTTP/2 Multiplexes Requests?

Because multiplexing happens at the application layer, but everything still travels over a single TCP connection. If TCP loses a packet, it holds back delivery of all streams until it’s retransmitted, regardless of whether other streams depend on that packet.

Can gRPC Work Without HTTP/2?

Not in a standard way: gRPC is designed on top of HTTP/2’s bidirectional streaming capabilities and uses them as a core part of its protocol.

How Do I Know If a Third-Party Site Uses HTTP/2 Without Access to Its Server?

Open Chrome DevTools, go to the Network tab, enable the “Protocol” column (right-click on the column header), and reload the page: you’ll see h2 or http/1.1 for each request.

References

  • RFC 7540: official specification of the HTTP/2 protocol.
  • RFC 7541: specification of HPACK, HTTP/2’s header compression.
  • MDN: HTTP: general documentation on the HTTP protocol and its evolution.
  • Wikipedia: CRIME: the attack that drove the design of HPACK instead of generic DEFLATE.
  • RFC 9218: extensible priority scheme that replaced HTTP/2’s original dependency tree.

📱 Enjoying this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.

Imagen destacada: Foto de Ivan N en Unsplash


Andrés Morales

Developer and AI researcher. Writes about language models, frameworks, developer tooling, and open source releases. Covers ML papers, the tech startup ecosystem, and programming trends.

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.