Technical Deep Dives

HTTP/2 Monitoring: Understanding and Observing Modern Protocol Performance

Learn how HTTP/2 features like multiplexing, server push, and header compression affect monitoring, and how to properly observe HTTP/2 connections in production.

AzMonitor TeamOctober 1, 20259 min read · 1,461 wordsUpdated January 20, 2026
HTTP/2protocol monitoringweb performancenetwork

HTTP/2 changed the rules for web performance monitoring. The protocol improvements — multiplexing, header compression, server push, stream prioritization — also changed how failures manifest and how you observe them. An HTTP/2 connection failure is different from an HTTP/1.1 failure, and monitoring tools that don't understand the protocol can give you misleading data about what's actually happening.

HTTP/2 vs HTTP/1.1: What Changes for Monitoring

HTTP/1.1 creates a new TCP connection for each request (or reuses connections serially). HTTP/2 multiplexes multiple streams over a single TCP connection. This has significant monitoring implications:

| Aspect | HTTP/1.1 | HTTP/2 | |---|---|---| | Connections | Multiple per domain | Single per domain | | Request blocking | Head-of-line blocking at HTTP level | No HTTP head-of-line blocking | | Connection failures | Affect one request | Can affect all concurrent requests | | Latency measurement | Per-connection TCP setup | Single connection amortized | | TLS | Optional, per-connection | Required (in practice), amortized | | Header cost | Full headers repeated | HPACK compressed, state tracked |

A single HTTP/2 TCP connection failure drops all in-flight requests simultaneously — this looks different in monitoring than individual HTTP/1.1 request failures.

Verifying HTTP/2 Is Active

Before monitoring HTTP/2 performance, confirm the protocol is actually being used:

# Check HTTP version with curl
curl -I --http2 https://example.com -v 2>&1 | grep "HTTP/"
# Output should include: < HTTP/2 200

# Check protocol negotiation
curl -I --http2 https://example.com -v 2>&1 | grep "ALPN"
# Output: * ALPN, server accepted to use h2

# Test server push (if implemented)
curl --http2 https://example.com -v 2>&1 | grep "PUSH"

# HTTP/2 statistics
curl --http2 https://example.com -w "
http_version: %{http_version}
response_code: %{http_code}
time_namelookup: %{time_namelookup}
time_connect: %{time_connect}
time_appconnect: %{time_appconnect}
time_pretransfer: %{time_pretransfer}
time_starttransfer: %{time_starttransfer}
time_total: %{time_total}
" -o /dev/null -s
# Programmatic HTTP/2 detection
import httpx

def check_http2_support(url):
    """Check if a URL supports HTTP/2"""
    with httpx.Client(http2=True) as client:
        response = client.get(url)
        
        return {
            "url": url,
            "http_version": response.http_version,
            "is_http2": response.http_version == "HTTP/2",
            "status_code": response.status_code,
            "headers": dict(response.headers)
        }

result = check_http2_support("https://api.example.com")
print(f"Protocol: {result['http_version']}")
print(f"HTTP/2: {result['is_http2']}")

HTTP/2 Stream and Frame Monitoring

HTTP/2 introduces streams and frames as internal protocol constructs. Monitoring these reveals protocol-level issues:

# h2 library for detailed HTTP/2 inspection
import h2.connection
import h2.config
import h2.events
import socket
import ssl

class HTTP2Inspector:
    """Inspect HTTP/2 framing and stream behavior"""
    
    def __init__(self, host, port=443):
        self.host = host
        self.port = port
    
    def connect(self):
        sock = socket.create_connection((self.host, self.port))
        ctx = ssl.create_default_context()
        ctx.set_alpn_protocols(['h2', 'http/1.1'])
        
        self.ssl_sock = ctx.wrap_socket(sock, server_hostname=self.host)
        negotiated = self.ssl_sock.selected_alpn_protocol()
        
        if negotiated != 'h2':
            raise ValueError(f"Server negotiated {negotiated}, not h2")
        
        config = h2.config.H2Configuration(client_side=True)
        self.conn = h2.connection.H2Connection(config=config)
        self.conn.initiate_connection()
        self.ssl_sock.sendall(self.conn.data_to_send(65535))
        
        return negotiated
    
    def send_request(self, path, headers=None):
        stream_id = self.conn.get_next_available_stream_id()
        
        request_headers = [
            (':method', 'GET'),
            (':path', path),
            (':scheme', 'https'),
            (':authority', self.host),
        ]
        if headers:
            request_headers.extend(headers)
        
        self.conn.send_headers(stream_id, request_headers, end_stream=True)
        self.ssl_sock.sendall(self.conn.data_to_send(65535))
        
        return stream_id
    
    def check_server_settings(self):
        """Get HTTP/2 server settings for analysis"""
        raw_data = self.ssl_sock.recv(65535)
        events = self.conn.receive_data(raw_data)
        
        settings = {}
        for event in events:
            if isinstance(event, h2.events.RemoteSettingsChanged):
                settings = dict(event.changed_settings)
        
        return settings

Monitoring HTTP/2 Connection Reuse

One of HTTP/2's key benefits is connection reuse. Monitor whether connections are actually being reused:

// Browser-side: measure connection reuse via Resource Timing API
function analyzeConnectionReuse() {
  const resources = performance.getEntriesByType('resource');
  
  const connectionStats = {};
  
  resources.forEach(resource => {
    // Same connectStart as previous request = connection was reused
    const connectionReused = resource.connectStart === resource.connectEnd;
    const domain = new URL(resource.name).hostname;
    
    if (!connectionStats[domain]) {
      connectionStats[domain] = { reused: 0, new: 0 };
    }
    
    if (connectionReused) {
      connectionStats[domain].reused++;
    } else {
      connectionStats[domain].new++;
    }
  });
  
  // Calculate connection reuse efficiency
  const report = {};
  for (const [domain, stats] of Object.entries(connectionStats)) {
    const total = stats.reused + stats.new;
    report[domain] = {
      ...stats,
      reuse_rate: (stats.reused / total * 100).toFixed(1) + '%',
      efficiency: stats.reused > stats.new ? 'good' : 'poor'
    };
  }
  
  return report;
}

If connection reuse is low (many new connections being established), investigate:

  • Is your server supporting Keep-Alive properly?
  • Are clients hitting different servers that don't share TLS session state?
  • Is a load balancer breaking persistent connections?

Server Push Monitoring

HTTP/2 server push allows servers to proactively send resources clients will need. Monitor its effectiveness:

// Track pushed resources vs fetched resources
const pushedResources = new Set();

// Listen for pushed resources (service worker context)
self.addEventListener('fetch', event => {
  const request = event.request;
  
  // If served from push cache, it won't have a network request
  if (request.mode === 'navigate') {
    const start = performance.now();
    
    event.respondWith(
      caches.match(request).then(cachedResponse => {
        if (cachedResponse) {
          // This resource was server-pushed
          pushedResources.add(request.url);
          return cachedResponse;
        }
        // Otherwise, fetch normally
        return fetch(request);
      })
    );
  }
});

// Report push effectiveness
function reportPushEfficiency(navigations, pushed) {
  const pushHitRate = pushed.size / navigations;
  return {
    total_navigations: navigations,
    push_hits: pushed.size,
    hit_rate: pushHitRate,
    pushed_resources: Array.from(pushed)
  };
}

Server push can actually hurt performance if:

  • Pushing resources the client already has in cache
  • Pushing large resources that delay the initial response
  • Push is not canceled when the client sends RST_STREAM

HPACK Header Compression Monitoring

HPACK compresses HTTP/2 headers using a shared compression table. Monitor compression effectiveness:

def analyze_header_compression(requests):
    """
    Analyze HTTP/2 header compression efficiency.
    
    In HTTP/1.1, headers are repeated in full every request.
    In HTTP/2 with HPACK, repeated headers are compressed dramatically.
    """
    analysis = {
        "requests": len(requests),
        "total_raw_header_bytes": 0,
        "estimated_compressed_bytes": 0,
        "common_headers": {}
    }
    
    # Track header occurrence
    header_counts = {}
    for req in requests:
        analysis["total_raw_header_bytes"] += sum(
            len(k) + len(v) for k, v in req.headers.items()
        )
        
        for key, value in req.headers.items():
            header_str = f"{key}:{value}"
            header_counts[header_str] = header_counts.get(header_str, 0) + 1
    
    # Common headers that benefit most from HPACK
    for header, count in sorted(header_counts.items(), key=lambda x: -x[1])[:10]:
        analysis["common_headers"][header] = count
    
    # Estimate compression (repeated headers are tiny after first occurrence)
    first_seen = set()
    estimated_compressed = 0
    
    for req in requests:
        for key, value in req.headers.items():
            header_str = f"{key}:{value}"
            if header_str in first_seen:
                # Compressed to index reference: ~1-3 bytes
                estimated_compressed += 2
            else:
                # First occurrence: full header
                estimated_compressed += len(key) + len(value)
                first_seen.add(header_str)
    
    analysis["estimated_compressed_bytes"] = estimated_compressed
    analysis["compression_ratio"] = (
        1 - estimated_compressed / analysis["total_raw_header_bytes"]
        if analysis["total_raw_header_bytes"] > 0 else 0
    )
    
    return analysis

HTTP/2 in Load Balancers

HTTP/2 and load balancers have a complex relationship. Many load balancers terminate HTTP/2 from clients and use HTTP/1.1 to backend servers. Monitor this translation:

# Check protocol at each layer
# Client → Load Balancer
curl -I --http2 https://api.example.com -v 2>&1 | grep "HTTP/"

# Load Balancer → Backend (from load balancer logs)
# Look for X-Forwarded-Proto header and connection type

# Check load balancer configuration
# Example: AWS ALB HTTP/2 support
aws elbv2 describe-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:... \
  --query 'Attributes[?Key==`routing.http2.enabled`]'

| Configuration | Client | LB→Backend | Impact | |---|---|---|---| | H2 end-to-end | HTTP/2 | HTTP/2 | Best for gRPC, preserves multiplexing | | H2 termination | HTTP/2 | HTTP/1.1 | Common, H2 benefits at edge | | H1 everywhere | HTTP/1.1 | HTTP/1.1 | Missing protocol benefits |

Testing HTTP/2 Specific Failure Scenarios

HTTP/2 has protocol-specific failure modes to test:

# Test GOAWAY frame handling
# Server can send GOAWAY to gracefully close connection
# Clients should retry affected streams

# Simulate with h2load (HTTP/2 load testing)
h2load -n1000 -c10 -m10 https://api.example.com/health
# -n: number of requests
# -c: concurrent clients
# -m: max concurrent streams per client

# Output includes stream and connection statistics:
# finished in 1.23s, 812.36 req/s, 1.23MB/s
# requests: 1000 total, 1000 started, 1000 done, 1000 succeeded
# connections: 10 total, 1 started, 10 done, 0 failed

# Test with curl's HTTP/2 verbose output
curl --http2 -v https://api.example.com 2>&1 | grep -E "SETTINGS|WINDOW|STREAM|PUSH"

Monitoring HTTP/2 with Synthetic Checks

Configure your synthetic monitoring to use HTTP/2:

monitor:
  name: "API - HTTP/2 Performance"
  url: "https://api.example.com/health"
  protocol: "http2"  # Force HTTP/2
  assertions:
    - type: status_code
      value: 200
    - type: http_version
      value: "2"  # Confirm HTTP/2 was negotiated
    - type: response_time
      operator: less_than
      value: 500
    - type: response_header
      header: "content-encoding"
      operator: exists  # Check compression is working

Alert if HTTP/2 negotiation fails (server degraded to HTTP/1.1):

alert:
  name: "HTTP/2 Downgrade Detected"
  condition: "http_version != '2' for any region"
  severity: warning
  message: "Server is serving HTTP/1.1 instead of HTTP/2 — possible TLS or configuration issue"

Conclusion

HTTP/2 monitoring requires understanding how the protocol changes performance characteristics and failure modes. Key points: verify HTTP/2 negotiation is actually happening via ALPN, monitor connection reuse efficiency, track HPACK compression benefits, and watch for the all-streams-affected failure mode unique to HTTP/2's multiplexing. Your external monitoring tools (like AzMonitor) should explicitly support HTTP/2 for accurate protocol-level testing, and your internal monitoring should track connection-level metrics that don't exist in HTTP/1.1. The protocol improvements of HTTP/2 are only valuable if your stack is actually using them correctly — monitoring confirms this.

Tags:HTTP/2protocol monitoringweb performancenetwork
Back to blog
A
AzMonitor Team
The AzMonitor team writes guides based on experience monitoring millions of endpoints daily across 10,000+ customer environments. Our expertise covers uptime monitoring, SRE practices, and reliability engineering.
Try AzMonitor free

3 monitors free forever · No credit card needed · Set up in 2 minutes

Start monitoring free →