CrackedRuby CrackedRuby

Overview

HTTP/2 and HTTP/3 represent significant protocol evolution from HTTP/1.1, addressing performance bottlenecks that emerged as web applications became more complex. HTTP/2, published as RFC 7540 in 2015, introduced binary framing, multiplexing, and header compression while maintaining compatibility with HTTP/1.1 semantics. HTTP/3, standardized as RFC 9114 in 2022, builds on HTTP/2's improvements but replaces TCP with QUIC, a UDP-based transport protocol that eliminates head-of-line blocking at the transport layer.

The shift to HTTP/2 addressed fundamental limitations in HTTP/1.1, where browsers opened multiple TCP connections to achieve parallelism, leading to connection overhead and inefficient resource usage. HTTP/2 multiplexes multiple requests over a single connection, eliminating the need for connection pooling and domain sharding techniques that web developers previously relied on.

HTTP/3 tackles remaining performance issues in HTTP/2, particularly head-of-line blocking at the TCP layer. When a TCP packet is lost, all streams on that connection must wait for retransmission, even if their data has already arrived. By moving to QUIC, HTTP/3 implements stream-level flow control and independent loss recovery, ensuring packet loss affects only the specific stream that experienced it.

HTTP/1.1 Flow:
Request 1 ──────> Response 1
Request 2 ──────> Response 2  (waits for Request 1)
Request 3 ──────> Response 3  (waits for Request 2)

HTTP/2 Flow (single connection):
Request 1 ──┐
Request 2 ──┼──> Multiplexed ──> Responses 1,2,3 interleaved
Request 3 ──┘

HTTP/3 Flow (QUIC streams):
Request 1 ──> Stream 1 (independent recovery)
Request 2 ──> Stream 2 (independent recovery)
Request 3 ──> Stream 3 (independent recovery)

Both protocols maintain HTTP semantics, meaning request methods, status codes, and headers function identically to HTTP/1.1 from an application perspective. The differences lie in how data is framed, transmitted, and managed at the protocol layer.

Key Principles

Binary Framing Layer

HTTP/2 and HTTP/3 use binary framing instead of HTTP/1.1's text-based protocol. In HTTP/1.1, messages consist of readable text with headers separated by newlines and terminated by double newlines. Binary framing encodes messages into discrete frames with specific types and flags.

HTTP/2 defines frame types including HEADERS, DATA, SETTINGS, WINDOW_UPDATE, PRIORITY, RST_STREAM, PUSH_PROMISE, PING, and GOAWAY. Each frame contains a length field, frame type, flags, stream identifier, and payload. The stream identifier associates frames with specific request-response pairs.

HTTP/1.1 Message:
GET /api/users HTTP/1.1\r\n
Host: example.com\r\n
Accept: application/json\r\n
\r\n

HTTP/2 Frame Structure:
+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+-------------------------------+
|R|                 Stream ID (31)              |
+=+=============+===============================
|                   Payload                     |
+-----------------------------------------------+

Binary framing enables efficient parsing without complex string manipulation or vulnerability to ambiguous message formats. Parsers know exactly where each frame begins and ends based on length fields, eliminating delimiter scanning.

Stream Multiplexing

Multiplexing allows multiple concurrent exchanges on a single connection. Each HTTP request-response pair occurs on a distinct stream, identified by a stream ID. Streams are independent, meaning frames from different streams can be interleaved without blocking.

In HTTP/2, stream IDs are 31-bit integers. Client-initiated streams use odd numbers, server-initiated streams (server push) use even numbers. Stream 0 is reserved for connection-level control frames. Stream IDs must increase monotonically for each endpoint.

# Conceptual stream interleaving
# Stream 1: Request for /page.html
# Stream 3: Request for /style.css
# Stream 5: Request for /script.js

# Frames sent in this order:
HEADERS (stream 1) - /page.html request
HEADERS (stream 3) - /style.css request
HEADERS (stream 5) - /script.js request
DATA (stream 3) - CSS content chunk 1
DATA (stream 1) - HTML content chunk 1
DATA (stream 5) - JS content chunk 1
DATA (stream 3) - CSS content chunk 2
DATA (stream 1) - HTML content chunk 2
# Responses complete independently

Stream prioritization allows clients to indicate resource importance through dependency trees and weights. A stream can depend on another stream, and assigned weights determine bandwidth allocation among siblings. Browsers use prioritization to request critical resources like HTML and CSS before images or analytics scripts.

Header Compression

HPACK (HTTP/2) and QPACK (HTTP/3) compress headers to reduce overhead. HTTP headers are repetitive across requests - headers like User-Agent, Accept-Encoding, and Cookie often remain identical. HPACK uses static and dynamic tables to represent common headers with indexes rather than sending full header strings.

The static table contains 61 common header name-value pairs defined in the specification. The dynamic table stores headers seen during the connection, allowing subsequent references using small integers. Literal headers can use Huffman encoding for additional compression.

First Request:
:method: GET
:path: /index.html
:scheme: https
:authority: example.com
user-agent: Mozilla/5.0...
accept-encoding: gzip, deflate

Encoded with indexes from static table:
:method (2), :path (literal), :scheme (7), :authority (literal), 
user-agent (literal, added to dynamic table index 62)
accept-encoding (16)

Second Request (same connection):
:method: GET
:path: /style.css
user-agent: Mozilla/5.0... (same as before)

Encoded:
:method (2), :path (literal), user-agent (62 from dynamic table)

QPACK extends HPACK for HTTP/3, addressing HTTP/2's head-of-line blocking in header decompression. HPACK requires strict ordering - if a frame with dynamic table updates is lost, subsequent frames cannot be decoded. QPACK uses separate unidirectional streams for dynamic table updates, allowing header blocks to reference future table entries with explicit acknowledgment tracking.

Connection Management

HTTP/2 uses a single TCP connection per origin. Connection preface begins with the string "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" followed by a SETTINGS frame. This preface ensures clear HTTP/2 negotiation and prevents HTTP/1.1 intermediaries from misinterpreting the binary protocol.

HTTP/3 connections establish over QUIC, which provides built-in connection migration. When a client's network changes (mobile device switching from WiFi to cellular), the QUIC connection continues using connection IDs rather than 4-tuple (source IP, source port, destination IP, destination port) identification. This eliminates connection re-establishment delays.

Flow control operates at both connection and stream levels. WINDOW_UPDATE frames grant additional transmission credits. Initial window sizes are negotiated via SETTINGS frames. This prevents fast senders from overwhelming slow receivers and allows per-stream bandwidth allocation.

# HTTP/2 flow control example
# Initial window size: 65,535 bytes

# Stream 1 sends DATA frame: 20,000 bytes
# Stream 1 remaining window: 45,535 bytes

# Client sends WINDOW_UPDATE (stream 1): 20,000
# Stream 1 remaining window: 65,535 bytes

# Connection-level window also needs updates
# Both stream and connection windows must have available credits

QUIC Transport Protocol

HTTP/3 operates over QUIC, a multiplexed transport protocol built on UDP. QUIC combines transport and cryptographic handshakes, establishing connections faster than TCP+TLS. The initial handshake typically completes in one round trip for new connections and zero round trips for resumed connections with cached parameters.

QUIC implements streams natively at the transport layer. Each stream has independent flow control and reliability. Lost packets only affect streams carrying that data, unlike TCP where packet loss blocks all streams on the connection. QUIC retransmits lost data on a new packet number, simplifying loss detection and avoiding retransmission ambiguity present in TCP.

Connection migration allows clients to continue connections after network changes without application-level interruption. QUIC connections use connection IDs negotiated during handshake. When the client's IP address changes, it sends packets with the same connection ID from the new address, and the server migrates the connection.

QUIC Connection Establishment (1-RTT):
Client                                Server
  |                                      |
  |-- Initial [ClientHello] ----------->|
  |                                      |
  |<- Initial [ServerHello]              |
  |   Handshake [Certificate]            |
  |   1-RTT [HTTP Response] -------------|
  |                                      |
  |-- Handshake [Finished] ------------->|
  |   1-RTT [HTTP Request] -------------->|

Compare to TCP+TLS (2-RTT):
Client                                Server
  |                                      |
  |-- TCP SYN ----------------------->  |
  |<- TCP SYN-ACK -------------------|  |
  |-- TCP ACK ----------------------->  |
  |-- TLS ClientHello --------------->  |
  |<- TLS ServerHello, Certificate --|  |
  |-- TLS Finished ------------------>  |
  |-- HTTP Request ------------------>  |

Implementation Approaches

Protocol Negotiation

Servers must support protocol negotiation to enable HTTP/2 and HTTP/3 clients while maintaining backward compatibility. HTTP/2 negotiation uses Application-Layer Protocol Negotiation (ALPN), a TLS extension where clients indicate supported protocols during the handshake. Servers select a protocol and inform the client in the ServerHello message.

For unencrypted connections, HTTP/2 supports an upgrade mechanism where clients send HTTP/1.1 requests with an "Upgrade: h2c" header. The server responds with 101 Switching Protocols to accept or continues with HTTP/1.1. However, browsers only support HTTP/2 over TLS, making the upgrade mechanism primarily relevant for server-to-server communication.

HTTP/3 discovery typically uses Alternative Services (Alt-Svc). Servers advertise HTTP/3 availability through an Alt-Svc response header or ALTSVC frame in HTTP/2. Clients then establish a QUIC connection to the advertised endpoint while maintaining the existing HTTP/1.1 or HTTP/2 connection as a fallback.

HTTP/2 ALPN Negotiation:
ClientHello:
  Supported protocols: h2, http/1.1

ServerHello:
  Selected protocol: h2

HTTP/3 Discovery via Alt-Svc:
HTTP/1.1 Response:
Alt-Svc: h3=":443"; ma=3600

Client attempts QUIC connection to example.com:443
If successful, subsequent requests use HTTP/3
If QUIC blocked, falls back to HTTP/1.1 or HTTP/2

Server Push

HTTP/2 introduced server push, allowing servers to send resources before clients request them. Servers send PUSH_PROMISE frames to notify clients of pushed resources, followed by response headers and data on new streams. Clients can reject pushes with RST_STREAM if resources are already cached.

Server push aims to eliminate round trips for predictable resource dependencies. When serving HTML that references CSS and JavaScript files, servers can push those resources immediately rather than waiting for the client to parse HTML and request them. However, push requires careful cache awareness - pushing already-cached resources wastes bandwidth.

HTTP/3 initially included server push but removed it in RFC 9114 due to limited adoption and implementation complexity. Servers can achieve similar results using Link headers with rel=preload, informing browsers of critical resources without actually pushing data.

# HTTP/2 server push sequence
# Client requests /index.html

# Server sends:
HEADERS (stream 1) - response to /index.html request
PUSH_PROMISE (stream 1, promised stream 2) - /style.css
PUSH_PROMISE (stream 1, promised stream 4) - /script.js
DATA (stream 1) - HTML content
HEADERS (stream 2) - response headers for /style.css
DATA (stream 2) - CSS content
HEADERS (stream 4) - response headers for /script.js
DATA (stream 4) - JS content

# Client receives all resources without additional requests
# If client already had /style.css cached:
RST_STREAM (stream 2) - reject the push

Prioritization Strategies

HTTP/2 prioritization uses dependency trees where streams can depend on others. Each stream has a weight (1-256) determining bandwidth share among siblings. Browsers construct prioritization trees reflecting resource importance - critical rendering path resources receive higher priority.

Stream dependencies form a tree structure. When creating a stream, clients specify a parent stream ID and whether the new stream is exclusive. Exclusive dependencies insert the new stream between parent and children, making existing children depend on the new stream. This allows dynamic priority adjustments as new requests are made.

HTTP/3 abandoned HTTP/2's dependency-based prioritization due to implementation complexity and limited server support. RFC 9218 defines an extensible priority scheme using PRIORITY_UPDATE frames and Priority header fields. Clients indicate urgency (0-7) and incremental (boolean) parameters, providing simpler priority semantics.

HTTP/2 Priority Tree Example:
         Stream 0 (connection)
              |
         Stream 1 (HTML) weight: 16
              |
         +----+----+
         |         |
    Stream 3   Stream 5
    (CSS)      (JS)
    weight:16  weight:16

Bandwidth allocation:
HTML receives 100% until complete
Then CSS and JS split equally (50% each)

Adding exclusive dependency:
Request Stream 7 (critical image)
  exclusive dependency on Stream 1

New tree:
         Stream 0
              |
         Stream 1
              |
         Stream 7
              |
         +----+----+
    Stream 3   Stream 5

Connection Coalescing

HTTP/2 and HTTP/3 allow connection coalescing, where a single connection serves multiple origins sharing a certificate. When a client has a connection to example.com, it can reuse that connection for www.example.com if the server's certificate covers both domains and the server announces support via ORIGIN frames.

Connection coalescing reduces connection overhead when organizations serve multiple domains from the same infrastructure. Instead of establishing separate connections for cdn.example.com, api.example.com, and www.example.com, clients use one connection if the server's certificate and ORIGIN frames permit.

DNS resolution influences coalescing. Clients check that origins resolve to the same IP address before coalescing. This prevents connection hijacking where attackers redirect DNS to malicious servers presenting valid certificates for other domains.

# Connection coalescing scenario
# Client connects to https://example.com
# Server certificate covers: example.com, www.example.com, cdn.example.com

# Server sends ORIGIN frame:
ORIGIN
  Origin: https://www.example.com
  Origin: https://cdn.example.com

# Client can now send requests to all three origins on this connection
# Request to www.example.com/page.html → uses existing connection
# Request to cdn.example.com/image.png → uses existing connection
# Request to other.com → requires new connection

Performance Considerations

Latency Reduction

HTTP/2 reduces latency through multiplexing, eliminating the round-trip delays caused by serial HTTP/1.1 requests. In HTTP/1.1, browsers open multiple connections (typically 6 per origin) to achieve parallelism, but this approach has limits and creates connection management overhead. HTTP/2 allows unlimited concurrent streams on a single connection.

Header compression significantly reduces request and response sizes. Typical web pages include large Cookie headers and repetitive request headers across resources. HPACK compression achieves 70-85% header size reduction, particularly impactful on high-latency mobile networks where every byte transmitted adds measurable delay.

HTTP/3's QUIC foundation eliminates head-of-line blocking at the transport layer. In HTTP/2 over TCP, a single lost packet blocks all streams on that connection until the packet is retransmitted and acknowledged. With QUIC, lost packets only affect the specific stream, allowing other streams to continue delivering data.

# Latency comparison (100ms RTT network)

# HTTP/1.1 (6 connections, 10 resources)
# Resource 1-6: 100ms (first round trip)
# Resource 7-10: 200ms (wait for connection availability)
# Total: ~200ms for all resources

# HTTP/2 (1 connection, 10 resources)
# All resources: 100ms (single round trip)
# Total: ~100ms for all resources

# HTTP/3 with 1% packet loss
# HTTP/2: Lost packet blocks all streams
# Recovery time: 100ms (RTT for retransmit) affects all streams
# HTTP/3: Lost packet blocks only affected stream
# Other 9 streams continue without delay

Connection Establishment

QUIC's integrated cryptographic handshake reduces connection establishment time. TCP requires a 3-way handshake before TLS negotiation begins. TLS 1.2 needs two round trips, while TLS 1.3 optimizes to one round trip. QUIC combines transport and cryptographic handshakes into a single round trip for new connections.

Resumed connections achieve zero-round-trip (0-RTT) handshakes using pre-shared keys from previous connections. Clients send encrypted application data in the first packet, allowing immediate request transmission. This dramatically improves performance for repeat visitors, particularly on high-latency mobile networks.

HTTP/2 connection reuse amortizes establishment costs across multiple requests. Applications maintaining persistent connections see improved performance compared to HTTP/1.1's connection churn. Connection pooling becomes simpler since a single connection per origin suffices.

Connection Establishment Timing:
HTTP/1.1 + TLS 1.2: 3 RTTs
  - TCP handshake: 1 RTT
  - TLS handshake: 2 RTTs

HTTP/1.1 + TLS 1.3: 2 RTTs
  - TCP handshake: 1 RTT
  - TLS handshake: 1 RTT

HTTP/2 + TLS 1.3: 2 RTTs
  (Same as HTTP/1.1 + TLS 1.3)

HTTP/3 (QUIC) new connection: 1 RTT
  - Combined handshake: 1 RTT

HTTP/3 (QUIC) resumed: 0 RTT
  - Encrypted data in first packet

Bandwidth Utilization

HTTP/2 improves bandwidth utilization through stream prioritization and flow control. Servers can allocate bandwidth according to resource priority, sending critical CSS before optional analytics. Flow control prevents buffer bloat where fast servers overwhelm slow clients.

Header compression reduces bandwidth consumption, particularly important for mobile connections with limited bandwidth and metered data. Compressing a 2KB header down to 300 bytes saves bandwidth across hundreds of requests per page load.

HTTP/3 handles loss recovery more efficiently than TCP. QUIC retransmits lost data with new packet numbers, eliminating retransmission ambiguity. When TCP retransmits, it cannot distinguish ACKs for original packets from ACKs for retransmissions, complicating congestion control. QUIC always knows which transmission was acknowledged.

# Bandwidth utilization example
# Page with 1 HTML (50KB), 5 CSS (100KB total), 20 images (2MB total)

# HTTP/1.1 strategy:
# Send all CSS first (critical rendering path)
# Then send images
# 6 parallel connections partially helps

# HTTP/2 strategy with prioritization:
# HTML: highest priority (16)
# CSS: high priority (12)
# Images: lower priority (8)
# Server allocates bandwidth proportionally
# Critical resources complete faster

# Flow control prevents issues:
# Slow client with 1Mbps connection
# Fast server with 1Gbps connection
# Without flow control: server buffers millions of bytes
# With flow control: server sends only what client can receive

Resource Overhead

HTTP/2 reduces connection overhead by using fewer TCP connections. Each TCP connection consumes memory for send/receive buffers, congestion control state, and connection tracking. Reducing from 6-8 connections per origin to 1 connection decreases memory consumption on both client and server.

Server push can waste bandwidth if pushed resources are already cached. Browsers cannot inform servers about cache state before receiving PUSH_PROMISE frames. Some implementations mitigate this by tracking pushed resources using cookies or cache digests, but these approaches add complexity.

QUIC requires more CPU for cryptographic operations since all packets are encrypted, not just the handshake. However, hardware acceleration for AES-GCM and ChaCha20-Poly1305 makes this overhead manageable on modern systems. The CPU cost is offset by reduced connection establishment overhead and improved loss recovery.

# Memory comparison (approximate)
# Per TCP connection: 100-200KB buffers + tracking data

# HTTP/1.1 approach:
# 6 connections to cdn.example.com
# 6 connections to api.example.com
# 6 connections to www.example.com
# Total: 18 connections = 1.8-3.6MB

# HTTP/2 approach (with coalescing):
# 1 connection serving all three origins
# Total: 1 connection = 100-200KB
# Savings: 1.7-3.4MB per client

Ruby Implementation

HTTP/2 Client Libraries

Ruby provides several HTTP/2 client libraries. The http-2 gem offers a pure Ruby implementation of the HTTP/2 protocol. The httpx gem supports both HTTP/2 and HTTP/3 with a focus on performance and modern Ruby features.

require 'http/2'

client = HTTP2::Client.new
client.on(:frame) do |bytes|
  # Send binary frame data over socket
  socket.write(bytes)
end

stream = client.new_stream
stream.on(:close) do
  puts "Stream closed"
end

stream.on(:headers) do |headers|
  puts "Response headers: #{headers}"
end

stream.on(:data) do |data|
  puts "Response data: #{data}"
end

# Send request
headers = {
  ':scheme' => 'https',
  ':method' => 'GET',
  ':path' => '/api/users',
  ':authority' => 'example.com'
}
stream.headers(headers, end_stream: true)

# Receive frames from socket
loop do
  data = socket.read(1024)
  client << data
end

The httpx library provides a higher-level interface with automatic connection management:

require 'httpx'

# HTTP/2 request (automatically negotiates protocol)
response = HTTPX.get("https://example.com/api/data")
puts response.status
puts response.body.to_s

# Concurrent requests over single HTTP/2 connection
responses = HTTPX.get(
  "https://example.com/api/users",
  "https://example.com/api/posts",
  "https://example.com/api/comments"
)

responses.each do |response|
  puts "#{response.uri}: #{response.status}"
end

HTTP/3 Support

HTTP/3 support in Ruby is emerging. The httpx gem includes experimental HTTP/3 support built on the quic-go library through FFI bindings. Applications must explicitly enable HTTP/3 and handle fallback scenarios.

require 'httpx'

# Enable HTTP/3 with fallback
http = HTTPX.plugin(:retries).with(http3: true)

begin
  response = http.get("https://example.com/api/data")
  
  # Check negotiated protocol
  if response.headers['alt-svc']
    puts "Server supports HTTP/3: #{response.headers['alt-svc']}"
  end
  
  puts "Protocol used: #{response.version}"
  puts "Response: #{response.body.to_s}"
rescue HTTPX::Error => e
  # Handle connection failures, potentially retrying over HTTP/2
  puts "Request failed: #{e.message}"
end

Server Implementation

Puma, a popular Ruby web server, supports HTTP/2 when running with TLS. Applications must configure TLS certificates and enable HTTP/2 in server settings. Puma handles protocol negotiation via ALPN automatically.

# config/puma.rb
port ENV.fetch('PORT', 3000)

# Enable TLS with HTTP/2
if ENV['ENABLE_HTTP2']
  ssl_bind '0.0.0.0', '3001',
    key: 'config/ssl/server.key',
    cert: 'config/ssl/server.crt',
    verify_mode: 'none'
  
  # HTTP/2 enabled automatically with TLS
end

workers ENV.fetch('WORKERS', 4)
threads 1, 5

Rails applications work transparently over HTTP/2. The application code remains unchanged since HTTP/2 maintains HTTP semantics. Response headers and body generation work identically:

# app/controllers/api_controller.rb
class ApiController < ApplicationController
  def index
    # This code works identically over HTTP/1.1, HTTP/2, and HTTP/3
    @users = User.includes(:posts).limit(20)
    
    # Set cache headers for HTTP/2 push hints
    response.headers['Link'] = '</assets/application.css>; rel=preload; as=style'
    
    render json: @users
  end
end

Connection Management

Ruby HTTP clients must manage connection pooling differently for HTTP/2. Traditional connection pools maintain multiple connections per host, but HTTP/2 requires only one connection. The httpx gem handles this automatically:

require 'httpx'

# Create session with connection reuse
session = HTTPX.plugin(:persistent)

# Multiple requests reuse same HTTP/2 connection
10.times do |i|
  session.get("https://example.com/api/resource/#{i}") do |response|
    puts "Request #{i}: #{response.status}"
  end
end

# Explicitly close connections when done
session.close

Applications must handle connection failures gracefully. HTTP/2 connections can fail due to network issues, server restarts, or protocol errors. Retry logic should re-establish connections and resend failed requests:

require 'httpx'

def fetch_with_retry(url, max_retries: 3)
  retries = 0
  
  begin
    HTTPX.get(url, timeout: { operation_timeout: 5 })
  rescue HTTPX::Error => e
    retries += 1
    if retries <= max_retries
      sleep(2 ** retries) # Exponential backoff
      retry
    else
      raise
    end
  end
end

response = fetch_with_retry("https://example.com/api/data")
puts response.body.to_s

Header Manipulation

Ruby applications can set custom headers that influence HTTP/2 behavior. Server push hints use Link headers with rel=preload:

class ApplicationController < ActionController::Base
  before_action :set_push_headers
  
  private
  
  def set_push_headers
    # Hint server to push critical resources
    push_headers = [
      '</assets/application.css>; rel=preload; as=style',
      '</assets/application.js>; rel=preload; as=script',
      '</assets/logo.png>; rel=preload; as=image'
    ].join(', ')
    
    response.headers['Link'] = push_headers
  end
end

Setting Alt-Svc headers advertises HTTP/3 availability:

class ApplicationController < ActionController::Base
  before_action :advertise_http3
  
  private
  
  def advertise_http3
    if ENV['HTTP3_ENABLED']
      # Advertise HTTP/3 on same host
      response.headers['Alt-Svc'] = 'h3=":443"; ma=3600'
    end
  end
end

Performance Monitoring

Applications should monitor protocol usage and performance metrics. Logging the negotiated protocol helps track adoption:

require 'httpx'

class MetricsMiddleware
  def initialize(app)
    @app = app
  end
  
  def call(env)
    start_time = Time.now
    protocol = env['SERVER_PROTOCOL'] # "HTTP/2.0" or "HTTP/1.1"
    
    status, headers, body = @app.call(env)
    
    duration = Time.now - start_time
    
    # Log metrics
    Rails.logger.info({
      protocol: protocol,
      method: env['REQUEST_METHOD'],
      path: env['PATH_INFO'],
      duration: duration,
      status: status
    }.to_json)
    
    [status, headers, body]
  end
end

Tracking multiplexed stream counts in HTTP/2 connections:

require 'http/2'

class HTTP2ConnectionManager
  attr_reader :active_streams
  
  def initialize
    @client = HTTP2::Client.new
    @active_streams = 0
    
    @client.on(:stream) do |stream|
      @active_streams += 1
      
      stream.on(:close) do
        @active_streams -= 1
        update_metrics
      end
    end
  end
  
  def new_request(headers)
    stream = @client.new_stream
    stream.headers(headers, end_stream: true)
    stream
  end
  
  private
  
  def update_metrics
    # Report to monitoring system
    Metrics.gauge('http2.active_streams', @active_streams)
  end
end

Integration & Interoperability

Reverse Proxy Configuration

Applications commonly deploy behind reverse proxies that handle HTTP/2 and HTTP/3 termination. Nginx supports both protocols with appropriate configuration:

server {
    listen 443 ssl http2;
    listen 443 quic reuseport;
    
    http2_max_concurrent_streams 128;
    http2_max_field_size 16k;
    http2_max_header_size 32k;
    
    # HTTP/3 support
    http3 on;
    http3_max_concurrent_streams 128;
    
    # Advertise HTTP/3
    add_header Alt-Svc 'h3=":443"; ma=86400';
    
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    
    location / {
        proxy_pass http://ruby_backend;
        proxy_http_version 1.1;
        
        # Preserve original protocol information
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

upstream ruby_backend {
    server 127.0.0.1:3000;
    keepalive 32;
}

Ruby applications receive HTTP/1.1 requests from the proxy but can detect the original protocol through custom headers:

class ApplicationController < ActionController::Base
  def client_protocol
    # Nginx sets custom header indicating frontend protocol
    request.headers['X-Protocol-Version'] || 'HTTP/1.1'
  end
  
  def using_http2?
    client_protocol == 'HTTP/2.0'
  end
  
  def using_http3?
    client_protocol == 'HTTP/3.0'
  end
end

CDN Integration

Content Delivery Networks provide HTTP/2 and HTTP/3 support at edge locations. Applications must consider how protocol features interact with CDN caching:

class AssetsController < ApplicationController
  def show
    asset = Asset.find(params[:id])
    
    # Set aggressive caching for CDN
    expires_in 1.year, public: true
    
    # Add push hints for HTTP/2
    if using_http2?
      related_assets = asset.related_assets.limit(5)
      push_links = related_assets.map do |a|
        "</assets/#{a.id}>; rel=preload; as=#{a.content_type}"
      end
      response.headers['Link'] = push_links.join(', ')
    end
    
    send_file asset.file_path,
              type: asset.content_type,
              disposition: 'inline'
  end
end

Browser Compatibility

All modern browsers support HTTP/2. HTTP/3 support varies but is available in Chrome, Edge, Firefox, and Safari. Applications should provide fallback mechanisms:

class ApiController < ApplicationController
  def index
    # Detect protocol capabilities
    protocol = request.headers['X-Protocol-Version']
    
    # Adjust response based on protocol
    case protocol
    when 'HTTP/3.0', 'HTTP/2.0'
      # Can rely on multiplexing, send all resources
      render json: {
        users: User.includes(:posts, :comments).all,
        metadata: fetch_metadata
      }
    else
      # HTTP/1.1, send minimal response
      render json: {
        users: User.limit(20).all,
        metadata_url: api_metadata_path
      }
    end
  end
end

Load Balancing Considerations

Load balancers must handle HTTP/2 and HTTP/3 connection persistence. Since HTTP/2 uses single connections with multiple streams, connection-based load balancing can create imbalanced load distribution:

# HAProxy configuration for HTTP/2
frontend https_frontend
    bind *:443 ssl crt /path/to/cert.pem alpn h2,http/1.1
    
    # Use request-based load balancing, not connection-based
    default_backend ruby_servers

backend ruby_servers
    balance roundrobin
    
    # Enable HTTP/2 to backend if supported
    server ruby1 127.0.0.1:3000 check
    server ruby2 127.0.0.1:3001 check
    server ruby3 127.0.0.1:3002 check

Applications can implement health checks that report stream load:

class HealthController < ApplicationController
  def show
    metrics = {
      status: 'healthy',
      active_connections: $connection_pool.active_connections,
      http2_streams: $http2_manager&.active_streams || 0,
      memory_usage: memory_usage_mb
    }
    
    render json: metrics
  end
  
  private
  
  def memory_usage_mb
    `ps -o rss= -p #{Process.pid}`.to_i / 1024
  end
end

WebSocket Compatibility

WebSocket connections cannot multiplex over HTTP/2 or HTTP/3. The WebSocket protocol uses its own framing and operates independently. Applications using WebSocket maintain separate connections:

# Action Cable configuration
class Connection < ActionCable::Connection::Base
  identified_by :current_user
  
  def connect
    # WebSocket connection established separately from HTTP/2
    # Protocol: HTTP/1.1 → WebSocket upgrade
    self.current_user = find_verified_user
  end
  
  private
  
  def find_verified_user
    verified_user = User.find_by(id: cookies.signed[:user_id])
    reject_unauthorized_connection unless verified_user
    verified_user
  end
end

WebTransport, built on HTTP/3, provides an alternative to WebSocket with better multiplexing. However, Ruby support is currently limited.

Reference

Protocol Comparison

Feature HTTP/1.1 HTTP/2 HTTP/3
Transport TCP TCP QUIC (UDP)
Message Format Text Binary Binary
Multiplexing No Yes Yes
Header Compression No HPACK QPACK
Server Push No Yes No
Connection Per Origin 6-8 1 1
Head-of-Line Blocking N/A Transport Layer None
Connection Migration No No Yes
0-RTT Handshake No No Yes

Frame Types (HTTP/2)

Frame Type Code Purpose
DATA 0x0 Conveys request/response body data
HEADERS 0x1 Transmits header fields
PRIORITY 0x2 Specifies stream priority
RST_STREAM 0x3 Terminates a stream
SETTINGS 0x4 Negotiates connection parameters
PUSH_PROMISE 0x5 Notifies peer of pushed resources
PING 0x6 Measures RTT and check connection
GOAWAY 0x7 Initiates connection shutdown
WINDOW_UPDATE 0x8 Updates flow control window
CONTINUATION 0x9 Continues header block fragments

SETTINGS Parameters

Setting ID Default Description
HEADER_TABLE_SIZE 0x1 4096 Maximum header compression table size
ENABLE_PUSH 0x2 1 Enables server push
MAX_CONCURRENT_STREAMS 0x3 unlimited Maximum concurrent streams
INITIAL_WINDOW_SIZE 0x4 65535 Initial flow control window size
MAX_FRAME_SIZE 0x5 16384 Maximum frame payload size
MAX_HEADER_LIST_SIZE 0x6 unlimited Maximum header list size

Error Codes

Error Code Description
NO_ERROR 0x0 Graceful shutdown
PROTOCOL_ERROR 0x1 Protocol violation detected
INTERNAL_ERROR 0x2 Internal implementation error
FLOW_CONTROL_ERROR 0x3 Flow control limits exceeded
SETTINGS_TIMEOUT 0x4 Settings ACK not received
STREAM_CLOSED 0x5 Frame on closed stream
FRAME_SIZE_ERROR 0x6 Frame size incorrect
REFUSED_STREAM 0x7 Stream not processed
CANCEL 0x8 Stream cancelled by peer
COMPRESSION_ERROR 0x9 Compression state error
CONNECT_ERROR 0xa TCP connection error
ENHANCE_YOUR_CALM 0xb Excessive resource usage
INADEQUATE_SECURITY 0xc TLS requirements not met
HTTP_1_1_REQUIRED 0xd HTTP/1.1 must be used

Ruby HTTP/2 Client Configuration

require 'httpx'

# Comprehensive HTTP/2 client configuration
http = HTTPX.plugin(:persistent)
          .plugin(:retries, max_retries: 3, retry_after: 1)
          .plugin(:follow_redirects, max_redirects: 3)
          .with(
            http2_settings: {
              max_concurrent_streams: 100,
              initial_window_size: 65_535,
              max_frame_size: 16_384
            },
            timeout: {
              connect_timeout: 5,
              operation_timeout: 30,
              read_timeout: 10
            }
          )

ALPN Protocol Identifiers

Protocol Identifier Description
HTTP/1.1 http/1.1 Plaintext HTTP/1.1
HTTP/2 over TLS h2 Binary HTTP/2 with TLS
HTTP/2 cleartext h2c Binary HTTP/2 without TLS
HTTP/3 h3 HTTP/3 over QUIC

Nginx HTTP/2 Directives

# Connection-level settings
http2_max_concurrent_streams 128;
http2_max_field_size 16k;
http2_max_header_size 32k;
http2_recv_buffer_size 256k;

# Push settings
http2_push /assets/application.css;
http2_push /assets/application.js;
http2_push_preload on;

# Timeouts
http2_idle_timeout 3m;
http2_recv_timeout 30s;

QUIC Connection ID Length

Type Length Range Description
Short Header 0-20 bytes Connection IDs in short header packets
Long Header 0-20 bytes Connection IDs in long header packets
Initial 8-20 bytes Connection IDs in initial packets
Retry 0-20 bytes Server-chosen connection IDs

HTTP/3 Frame Types

Frame Type Purpose
DATA Application data
HEADERS HTTP headers
CANCEL_PUSH Cancel server push
SETTINGS Connection configuration
PUSH_PROMISE Announce pushed resource
GOAWAY Graceful shutdown
MAX_PUSH_ID Limit push stream IDs

Performance Benchmarks

Typical latency improvements observed with HTTP/2 and HTTP/3 compared to HTTP/1.1:

Scenario HTTP/2 Improvement HTTP/3 Improvement
High latency (200ms RTT) 30-50% faster 40-60% faster
Packet loss (1-2%) 10-20% faster 30-50% faster
Mobile connections 20-40% faster 35-55% faster
Concurrent requests (20+) 40-60% faster 45-65% faster
Frequent reconnections 15-25% faster 40-70% faster