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.
3 monitors free forever · No credit card needed · Set up in 2 minutes
Start monitoring free →