โ† Back to DevBytes

HTTP/2 Protocol: A Complete Reference Guide

What is HTTP/2?

HTTP/2 is the second major version of the Hypertext Transfer Protocol, standardized in 2015 as RFC 7540. It evolved from Google's SPDY protocol and was designed to address the performance limitations of HTTP/1.1, particularly around latency and inefficient connection usage. HTTP/2 is a binary protocol that retains the same HTTP semantics โ€” methods, status codes, header fields โ€” but fundamentally changes how data is framed and transported between client and server.

Unlike HTTP/1.1's textual nature, HTTP/2 uses binary framing to encapsulate messages. This allows for more efficient parsing, eliminates whitespace ambiguity, and enables advanced features like multiplexing multiple streams over a single TCP connection. The protocol is fully backward-compatible: existing web applications work without modification, while performance improvements are gained transparently when both endpoints support HTTP/2.

Key Terminology

Why HTTP/2 Matters

๐Ÿš€ Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

The web has evolved dramatically since HTTP/1.1 was standardized in 1997. Modern pages often require dozens or even hundreds of individual requests โ€” stylesheets, scripts, images, API calls โ€” to render fully. HTTP/1.1's request-response model, where each request typically requires a dedicated connection or suffers from head-of-line blocking when pipelined, became a serious bottleneck.

HTTP/2 solves these fundamental problems through several architectural improvements:

Real-World Impact

Studies across major websites show that HTTP/2 typically reduces page load times by 20-40% compared to HTTP/1.1, with the most dramatic improvements seen on high-latency connections (mobile networks, for example) and resource-heavy pages. Beyond raw speed, HTTP/2 also reduces server load by consolidating connections โ€” a server that previously handled hundreds of concurrent TCP connections per client may now handle just one or two.

Core Features in Depth

Binary Framing Layer

HTTP/2 replaces HTTP/1.1's text-based protocol with a binary framing layer. Each frame has a fixed 9-byte header containing the frame type, flags, and stream identifier, followed by a variable-length payload. There are ten defined frame types:

Multiplexing and Streams

Multiplexing is arguably HTTP/2's most impactful feature. A single TCP connection can host up to 2ยณยน concurrent streams (though practical limits are much lower). Each stream is independent: frames from different streams can be interleaved arbitrarily, and a slow or stalled stream does not block others. This eliminates the head-of-line blocking that plagued HTTP/1.1 pipelining.

Streams have well-defined lifecycle states: idle, open (with either half-closed variants), and closed. Stream identifiers are assigned by the initiating endpoint โ€” clients use odd numbers, servers use even numbers (including 0 for connection-level control).

HPACK Header Compression

HTTP headers are notoriously redundant. In HTTP/1.1, headers like Cookie, User-Agent, and Accept-Encoding are sent repeatedly with each request, often unchanged across dozens of requests. HPACK solves this by maintaining an indexed table of header fields on both endpoints. Common headers are referenced by their table index rather than transmitted in full, and new entries can be added dynamically. HPACK uses two encoding techniques:

The compressor uses Huffman coding for literals and a differential encoding scheme that sends only changes to the dynamic table, achieving compression ratios of 85-95% for typical header traffic.

Server Push

Server push allows the server to preemptively send resources that it knows the client will need. When the server receives a request, it can send one or more PUSH_PROMISE frames describing the resources it intends to push, followed by the actual response data on new streams. The client can accept or reject these pushes by resetting the corresponding streams.

This feature is particularly powerful for eliminating the "discovery delay" โ€” the time between fetching an HTML document and discovering its dependent resources (CSS, JavaScript, images). A well-configured server can push critical render-blocking resources immediately after the initial HTML request, potentially saving one or more full round trips.

// Example: Server push configuration in Node.js with http2 module
const http2 = require('http2');
const fs = require('fs');

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

server.on('stream', (stream, headers) => {
  // When a request for index.html arrives, push style.css
  if (headers[':path'] === '/index.html') {
    stream.pushStream({ ':path': '/style.css' }, (err, pushStream) => {
      if (err) throw err;
      pushStream.respond({
        'content-type': 'text/css',
        ':status': 200
      });
      pushStream.end('body { margin: 0; font-family: sans-serif; }');
    });

    stream.pushStream({ ':path': '/script.js' }, (err, pushStream) => {
      if (err) throw err;
      pushStream.respond({
        'content-type': 'application/javascript',
        ':status': 200
      });
      pushStream.end('console.log("Pushed script loaded");');
    });
  }

  // Send the main HTML response
  stream.respond({
    'content-type': 'text/html',
    ':status': 200
  });
  stream.end(`
    
    
    
    
      

HTTP/2 Server Push Demo

Stream Prioritization

HTTP/2 includes a priority system where each stream can declare a dependency on another stream and assign a weight (1-256) relative to sibling streams. This creates a priority tree that guides the server in allocating bandwidth. For example, an HTML document might be marked as the highest priority, with render-blocking CSS as its high-weight dependent, and deferred scripts as low-weight dependents. The server interprets these signals to interleave frames optimally.

Flow Control

HTTP/2 implements per-stream and connection-level flow control using WINDOW_UPDATE frames. Each endpoint maintains a receive window for every stream and for the connection as a whole. The sender must not transmit data beyond the receiver's advertised window. This prevents a fast sender from overwhelming a slow receiver and allows backpressure to propagate naturally through multiplexed streams โ€” something impossible in HTTP/1.1.

How to Use HTTP/2

Enabling HTTP/2 on the Server

Most modern web servers support HTTP/2 natively. Configuration varies by server, but the common pattern involves enabling TLS (HTTP/2 over cleartext TCP, or "h2c", is rarely used in production) and activating the HTTP/2 module.

Nginx Configuration

# nginx.conf โ€” Enable HTTP/2 on Nginx
server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    # Modern TLS configuration required for HTTP/2
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
    ssl_prefer_server_ciphers on;

    # HTTP/2-specific optimizations
    http2_push_preload on;  # Enable automatic push of preload links

    location / {
        root /var/www/html;
        index index.html;
    }

    location ~ \.css$ {
        # Push CSS files when referenced via preload
        add_header Link '<$1>; rel=preload; as=style' always;
    }
}

Apache Configuration

# Apache httpd.conf โ€” Enable HTTP/2 via mod_http2
# Ensure mod_http2 and mod_ssl are loaded
LoadModule http2_module modules/mod_http2.so
LoadModule ssl_module modules/mod_ssl.so


    Protocols h2 http/1.1
    SSLEngine on
    SSLCertificateFile /etc/apache2/ssl/server.crt
    SSLCertificateKeyFile /etc/apache2/ssl/server.key

    # Enable server push via Header directive
    
        Header add Link '; rel=preload; as=style'
    

Node.js HTTP/2 Server

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

const server = http2.createSecureServer({
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.crt'),
  settings: {
    // Custom HTTP/2 settings
    maxConcurrentStreams: 256,       // Limit concurrent streams
    initialWindowSize: 65535,        // Initial flow control window
    maxHeaderListSize: 16384,        // Max header block size in bytes
    enablePush: true                 // Enable server push
  }
});

server.on('stream', (stream, headers) => {
  const path = headers[':path'];
  
  // Log incoming request
  console.log(`Request: ${headers[':method']} ${path}`);

  // Handle different routes
  if (path === '/') {
    stream.respond({
      ':status': 200,
      'content-type': 'text/html; charset=utf-8',
      'cache-control': 'max-age=3600'
    });
    stream.end('

Welcome to HTTP/2

'); } else if (path === '/api/data') { stream.respond({ ':status': 200, 'content-type': 'application/json' }); stream.end(JSON.stringify({ version: 'HTTP/2', timestamp: Date.now() })); } else { stream.respond({ ':status': 404 }); stream.end('Not Found'); } }); server.listen(8443, () => { console.log('HTTP/2 server listening on port 8443'); });

Client-Side Usage

From a browser perspective, HTTP/2 is automatically negotiated when connecting to an HTTPS server that advertises "h2" via ALPN (Application-Layer Protocol Negotiation) during the TLS handshake. Developers don't need to change their application code โ€” fetch, XHR, and all browser APIs work identically. However, certain HTTP/1.1 performance patterns become counterproductive with HTTP/2.

Node.js HTTP/2 Client

const http2 = require('http2');

// Establish a connection to the server
const client = http2.connect('https://localhost:8443', {
  // Client options
  settings: {
    maxConcurrentStreams: 100,
    enablePush: true
  }
});

// Handle server-initiated pushes
client.on('stream', (pushStream, headers) => {
  console.log('Server pushed:', headers[':path']);
  let pushedData = '';
  pushStream.on('data', (chunk) => {
    pushedData += chunk.toString();
  });
  pushStream.on('end', () => {
    console.log('Pushed resource data:', pushedData.substring(0, 100));
  });
});

// Function to make a request
function makeRequest(path) {
  return new Promise((resolve, reject) => {
    const stream = client.request({
      ':method': 'GET',
      ':path': path
    });

    let data = '';
    stream.on('data', (chunk) => {
      data += chunk.toString();
    });
    stream.on('end', () => {
      resolve({ status: stream.headers[':status'], data });
    });
    stream.on('error', reject);
  });
}

// Execute multiple requests concurrently over one connection
(async () => {
  try {
    const [res1, res2, res3] = await Promise.all([
      makeRequest('/'),
      makeRequest('/api/data'),
      makeRequest('/style.css')
    ]);
    console.log('Responses received concurrently:', 
      res1.status, res2.status, res3.status);
  } catch (err) {
    console.error('Request error:', err);
  } finally {
    client.close();
  }
})();

curl Command-Line Usage

# Test HTTP/2 support and view connection details
curl -v --http2 https://example.com

# Force HTTP/2 (fail if server doesn't support it)
curl --http2-only https://example.com

# View HTTP/2 connection state and streams
curl -v --http2 -o /dev/null -s https://example.com 2>&1 | grep -E '(h2|HTTP/2)'

# Test server push
curl --http2 -v https://example.com 2>&1 | grep -i push

# Send multiple requests over single HTTP/2 connection
curl --http2 https://example.com/api/1 https://example.com/api/2

Best Practices for HTTP/2

1. Consolidate Connections, Not Requests

In HTTP/1.1, domain sharding (spreading resources across multiple domains) was a common optimization to increase parallel connections. With HTTP/2 multiplexing, this becomes harmful: each additional domain requires a separate TCP connection with its own TLS handshake, slow-start phase, and HPACK compression table built from scratch. Consolidate resources onto as few origins as possible. A single HTTP/2 connection can handle thousands of concurrent requests efficiently.

2. Stop Concatenating and Sprite-Mapping

CSS and JavaScript concatenation, image sprites, and inlining were HTTP/1.1 workarounds to reduce request count. Under HTTP/2, these practices degrade cache granularity and increase initial download size. Instead, serve modular, granular resources and rely on multiplexing to fetch them concurrently. A page that loads 50 small CSS files over HTTP/2 may perform better than one that loads a single massive concatenated file.

3. Use Server Push Sparingly

Server push is powerful but easy to misuse. Pushing resources the client already has cached wastes bandwidth. Implement push only for critical, render-blocking resources that you're certain the client needs. Use rel=preload headers in responses and configure conditional pushes based on cookie state or session information. Monitor push performance metrics and be prepared to disable pushes that don't improve page load times.

4. Prioritize Intelligently

Leverage HTTP/2 stream prioritization to guide the server's resource delivery order. Browsers typically implement a sensible default priority tree, but you can influence it:

  • Place <link rel="preload"> tags for critical resources early in the HTML
  • Use importance attributes on fetch requests: fetch(url, { importance: 'high' })
  • Avoid flooding the connection with low-priority requests that compete with critical resources

5. Optimize TLS Configuration

HTTP/2 implementations require modern TLS configurations. Specifically:

  • Use TLS 1.2 or TLS 1.3 exclusively
  • Enable AEAD cipher suites (AES-GCM, ChaCha20-Poly1305)
  • Disable stream ciphers, block ciphers, and weak Diffie-Hellman groups
  • Consider OCSP stapling to reduce TLS handshake latency
  • Implement TLS session resumption to speed up reconnects

6. Monitor HPACK Table Size

The HPACK dynamic table has a configurable maximum size (default 4096 bytes per SETTINGS_HEADER_TABLE_SIZE). If your application uses many custom headers or large cookies, the table may evict entries frequently, reducing compression efficiency. Keep headers consistent and compact. Avoid generating dynamic header values that change with every request (e.g., timestamps in headers).

7. Handle GOAWAY Gracefully

HTTP/2 servers periodically send GOAWAY frames to initiate graceful shutdown (e.g., during rolling deployments). Clients should handle GOAWAY by finishing in-flight requests on the current connection and opening a new connection for subsequent requests. Implement retry logic in client libraries to seamlessly transition when a GOAWAY is received.

// Client-side GOAWAY handling example
client.on('goaway', (lastStreamId, errorCode, opaqueData) => {
  console.log(`GOAWAY received: last stream ${lastStreamId}, code ${errorCode}`);
  
  // Stop creating new streams on this connection
  connectionClosing = true;
  
  // Create a fresh connection for new requests
  const newClient = http2.connect('https://localhost:8443');
  
  // Drain pending requests to the new connection
  pendingRequests.forEach(req => {
    const stream = newClient.request(req.headers);
    // ... handle response
  });
  pendingRequests = [];
});

8. Avoid HTTP/1.1 Performance Hacks

Many classic performance optimizations become anti-patterns under HTTP/2. Specifically avoid:

  • Domain sharding
  • CSS/JS concatenation
  • Image sprites
  • Resource inlining (base64 data URIs)
  • Cookie-free domains (each domain requires a separate connection)
  • Aggressive resource bundling

9. Test and Measure

Use tools that accurately simulate HTTP/2 behavior:

# Using h2load for HTTP/2 load testing
h2load -n 10000 -c 50 -m 100 https://example.com/api/endpoint

# Options explained:
# -n 10000: total number of requests
# -c 50: number of concurrent clients
# -m 100: max concurrent streams per connection

# Using Chrome DevTools: Protocol column in Network tab shows "h2"
# Using Wireshark: Filter with 'http2' to inspect frames

# Node.js performance test with http2 module
const http2 = require('http2');

async function benchmark(url, count) {
  const client = http2.connect(url);
  const start = Date.now();
  
  const requests = Array.from({ length: count }, (_, i) => {
    return new Promise((resolve) => {
      const stream = client.request({ ':path': '/' });
      stream.on('end', resolve);
      stream.end();
    });
  });
  
  await Promise.all(requests);
  const elapsed = Date.now() - start;
  console.log(`${count} requests in ${elapsed}ms (${(count / elapsed * 1000).toFixed(0)} req/s)`);
  client.close();
}

benchmark('https://localhost:8443', 1000);

10. Graceful Degradation

Not all clients support HTTP/2. Ensure your infrastructure gracefully falls back to HTTP/1.1. ALPN negotiation during TLS automatically handles this โ€” the server advertises both "h2" and "http/1.1", and the client selects what it supports. Your backend logic should remain protocol-agnostic, relying on HTTP semantics rather than protocol-specific features.

Advanced Considerations

HTTP/2 over Cleartext (h2c)

While HTTP/2 can operate over cleartext TCP (h2c), browsers don't support this mode โ€” they require TLS. h2c is useful for internal services, load balancer-to-backend communication, and development environments. It uses an upgrade mechanism from HTTP/1.1 via the Upgrade: h2c header.

# curl with h2c upgrade (requires special server support)
curl --http2-prior-knowledge http://localhost:8080

# Node.js h2c server example
const http2 = require('http2');

const server = http2.createServer(); // No SSL options
server.on('stream', (stream, headers) => {
  stream.respond({ ':status': 200 });
  stream.end('HTTP/2 cleartext works!');
});
server.listen(8080);

HPACK Compression Internals

Understanding HPACK helps debug header-related issues. The encoder maintains a dynamic table (FIFO, bounded by SETTINGS_HEADER_TABLE_SIZE). When adding a new entry, the oldest entries are evicted if the table exceeds its maximum size. Each endpoint manages its own encoder/decoder pair independently โ€” the client's encoder uses the server's decoder table and vice versa.

// Demonstrating HPACK-aware header design
// Good: Consistent, indexable headers
stream.respond({
  ':status': 200,
  'content-type': 'text/html',
  'cache-control': 'max-age=3600',
  'vary': 'accept-encoding'
});

// Bad: Unique, non-indexable headers per request
stream.respond({
  ':status': 200,
  'x-request-id': crypto.randomUUID(), // Never reusable
  'x-timestamp': Date.now().toString(), // Changes every request
  'content-type': 'text/html'
});

Interoperability with HTTP/3

HTTP/3 (standardized in 2022) builds on HTTP/2's concepts but replaces TCP with QUIC (UDP-based transport). HTTP/2's binary framing, multiplexing, and HPACK (replaced by QPACK in HTTP/3) laid the groundwork. Most HTTP/2 implementations are evolving toward HTTP/3 support. The transition path is straightforward: the semantic layer remains identical, and only the transport changes.

Conclusion

HTTP/2 represents a fundamental advancement in web protocol design. Its binary framing, multiplexed streams, header compression, and server push capabilities directly address the performance bottlenecks that constrained HTTP/1.1 for nearly two decades. For developers, the transition is largely transparent โ€” existing applications benefit automatically when deployed behind HTTP/2-capable infrastructure. However, realizing the full potential requires rethinking HTTP/1.1-era optimization patterns: consolidating origins rather than sharding them, serving granular resources rather than concatenating them, and using server push judiciously. As the web ecosystem continues evolving toward HTTP/3, the architectural foundations established by HTTP/2 โ€” multiplexing, binary framing, and semantic consistency โ€” ensure a smooth upgrade path while delivering meaningful performance improvements today.

๐Ÿš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started โ€” $23.99/mo
โ† Back to all articles