Skip to main content
CriticalProtected by PowerWAF

HTTP/2 Rapid Reset Attack

CategoryDDoSFirst seen2023Read time11 minVerified2026-03-11
DEFINITION

The HTTP/2 Rapid Reset attack (CVE-2023-44487) exploits the stream multiplexing and cancellation mechanism in HTTP/2 to launch unprecedented application-layer DDoS attacks. The attacker opens HTTP/2 streams by sending HEADERS frames and immediately cancels them with RST_STREAM frames, creating an asymmetric resource exhaustion cycle where the server allocates and tears down resources for each stream while the client bears virtually no cost β€” achieving request rates exceeding 200 million per second from relatively small botnets.

How HTTP/2 Rapid Reset Attack Works

HTTP/2 introduced stream multiplexing: multiple concurrent request/response exchanges over a single TCP connection, identified by stream IDs. The protocol includes RST_STREAM frames to cancel individual streams β€” a legitimate mechanism for canceling downloads or aborting unnecessary requests. The Rapid Reset attack weaponizes this by exploiting an asymmetry in the protocol design: sending a HEADERS frame (opening a stream) costs the client approximately 9 bytes of framing overhead, while the server must allocate memory for the stream state, parse headers (potentially decompressing HPACK-encoded headers), initialize request routing logic, allocate buffers, and begin processing the request. When RST_STREAM arrives milliseconds later, the server must then tear down all those allocations and clean up resources. This allocate-deallocate cycle, repeated millions of times per second across thousands of connections, overwhelms server CPU and memory without ever generating meaningful response traffic β€” the server is trapped in an infinite loop of setup and teardown.

1

Establish legitimate HTTP/2 connections to the target

The attacker completes normal TCP handshakes and TLS negotiations with the target server, establishing valid HTTP/2 connections. Each connection goes through the full TLS 1.2/1.3 handshake, making it indistinguishable from a legitimate client at the connection level. The attacker sends the HTTP/2 connection preface (the 'PRI * HTTP/2.0' magic string and initial SETTINGS frame) and negotiates connection parameters. A botnet of 20,000 nodes each maintaining 100 connections creates 2 million concurrent HTTP/2 connections to the target.

2

Rapidly open streams with HEADERS and cancel with RST_STREAM

On each connection, the attacker sends a HEADERS frame to open a new stream, immediately followed by a RST_STREAM frame (error code CANCEL, 0x8) to close it. The HEADERS frame contains a minimal but valid HTTP request (GET / HTTP/2), and the RST_STREAM arrives before the server completes processing. Stream IDs increment with each request (1, 3, 5, 7...), and the attacker can open and cancel streams as fast as the TCP connection allows β€” typically thousands per second per connection. The RST_STREAM arrives within the same TCP segment or the immediately following one.

3

Exploit server-side resource allocation asymmetry

For each HEADERS frame, the server's HTTP/2 implementation must: allocate memory for the stream state structure (typically 1-4 KB), decompress HPACK-encoded headers, create request context objects, initialize logging/metrics collection, and begin routing the request to the appropriate handler. When RST_STREAM arrives, the server must: abort any in-progress processing, deallocate all stream resources, update connection-level accounting, and potentially log the cancelled request. This allocate-then-immediately-deallocate cycle is far more expensive than simply ignoring the request would be, because the server has no mechanism to defer allocation until it confirms the stream won't be immediately cancelled.

4

Bypass concurrent stream limits using rapid cancellation

HTTP/2 implementations enforce a maximum concurrent streams limit (SETTINGS_MAX_CONCURRENT_STREAMS, typically 100-256). Under normal operation, this limits how many requests a client can have in-flight simultaneously. However, RST_STREAM closes a stream instantly, freeing its slot. By canceling each stream immediately, the attacker never reaches the concurrent limit β€” each stream exists for microseconds. A single connection can therefore generate thousands of stream open/close cycles per second despite the concurrent stream limit. This is the core protocol flaw: the rate of stream creation is unlimited because the concurrency limit only counts active streams, not total streams created.

5

Overwhelm the server while generating minimal network traffic

Unlike volumetric attacks that require massive bandwidth, HTTP/2 Rapid Reset is computationally asymmetric. The attacker sends approximately 20-30 bytes per stream cycle (9-byte HEADERS frame + 13-byte RST_STREAM frame), while the server performs thousands of bytes worth of memory allocation and CPU-intensive operations per cycle. A 20,000-node botnet each generating 10,000 stream cycles per second produces 200 million 'requests' per second β€” far exceeding any legitimate traffic pattern β€” while using relatively modest total bandwidth. The server's CPU saturates processing stream lifecycle events, and legitimate requests experience extreme latency or outright rejection.

Real-World Examples

2023

CVE-2023-44487 β€” zero-day exploitation across the internet

Between August and October 2023, threat actors exploited the HTTP/2 Rapid Reset vulnerability as a zero-day against targets across multiple industries before the CVE was publicly disclosed. The attacks achieved request rates of 201 million requests per second in some instances β€” 3x larger than any previously recorded HTTP DDoS attack β€” using botnets of only 20,000 compromised machines. The vulnerability affected virtually every HTTP/2 implementation: Nginx, Apache HTTP Server, Apache Tomcat, Microsoft IIS, HAProxy, Envoy, gRPC (Go, C++, Python), Node.js, Golang net/http, and .NET Kestrel all required patches.

2023

Internet-wide patching emergency

On October 10, 2023, the coordinated disclosure of CVE-2023-44487 triggered one of the largest simultaneous patching events in internet history. CISA issued advisory AA23-283A urging immediate action. Every major web server, reverse proxy, load balancer, and HTTP/2 library released patches within days: Nginx 1.25.3, Apache 2.4.58, Node.js 18.18.2/20.8.1, Go 1.21.3, HAProxy 2.8.4, and dozens more. The CVSS score of 7.5 (High) reflected the ease of exploitation and the universal impact across the internet's HTTP infrastructure.

2024

Sustained attacks against financial services sector

Throughout the first half of 2024, financial institutions and fintech companies became primary targets of HTTP/2 Rapid Reset attacks, even after patches were available. Many organizations ran unpatched reverse proxies, internal load balancers, or legacy middleware that still processed HTTP/2 streams without rate limiting. Attacks peaked at 50-100 million requests per second against individual banks and payment processors, causing API gateway failures, transaction timeouts, and mobile banking outages. The incidents demonstrated that the vulnerability's long tail β€” unpatched internal infrastructure behind patched edge proxies β€” remained exploitable months after disclosure.

Impact & Risk Assessment

HTTP/2 Rapid Reset represents a paradigm shift in application-layer DDoS because it exploits a protocol design flaw rather than overwhelming bandwidth or exploiting implementation bugs. The attack achieves request rates 10-100x higher than traditional HTTP floods while using a fraction of the bandwidth, making it exceptionally cost-effective for attackers. Impact is amplified by the near-universal deployment of HTTP/2: over 35% of all websites use HTTP/2 as of 2024, and virtually every modern web server, CDN edge node, API gateway, and load balancer implements the protocol. The server-side cost is dominated by CPU consumption (stream lifecycle management, HPACK decompression, request routing) rather than bandwidth, meaning traditional volumetric DDoS mitigations are ineffective. Cascading failures are common: overwhelmed front-end proxies pass partial requests to backends, saturating application servers and database connection pools. The attack's stealth is notable β€” because RST_STREAM cancels the request before a response is generated, access logs may not capture the attack traffic, and traditional request-based rate limiting doesn't trigger because no requests 'complete.'

How to Detect HTTP/2 Rapid Reset Attack

Monitor HTTP/2 frame-level metrics, not just request/response counts. The definitive indicator is a high ratio of RST_STREAM frames to completed responses β€” during normal operation, RST_STREAM represents less than 1% of stream closures; during a Rapid Reset attack, it approaches 100%. Track the rate of stream creation per connection: legitimate clients rarely exceed 10-20 concurrent streams, while attack connections cycle through thousands of stream IDs per second. Monitor the gap between HEADERS and RST_STREAM frames β€” during attacks, RST_STREAM arrives within the same TCP segment or within 1-2 RTTs of the HEADERS frame, a pattern never seen in legitimate browsing. Correlate CPU utilization spikes with HTTP/2 connection metrics: a CPU increase without a corresponding increase in completed requests or response bytes indicates processing overhead from stream lifecycle churn. Check web server error logs for connection reset patterns and high rates of status 499 (client closed connection) or stream cancellation entries. Implement HTTP/2 frame-level logging (available in Nginx debug builds and HAProxy at log-level traffic) to capture HEADERS/RST_STREAM frame sequences for forensic analysis.

How to Prevent HTTP/2 Rapid Reset Attack

Patch all HTTP/2 implementations immediately β€” this is the single most critical action. Apply vendor patches for: Nginx (http2_max_concurrent_streams directive + RST_STREAM rate limiting), Apache (mod_http2 with stream rate limiting), HAProxy (tune.h2.max-concurrent-streams + RST_STREAM accounting), Node.js, Go, .NET Kestrel, and all other HTTP/2 libraries in your stack. Configure stream rate limits per connection: limit the total number of streams created (not just concurrent) within a time window β€” for example, no more than 100 new streams per second per connection. Implement RST_STREAM rate limiting: drop connections that send RST_STREAM frames at a rate exceeding a configurable threshold (e.g., 50 RST_STREAMs per second). Reduce SETTINGS_MAX_CONCURRENT_STREAMS to the minimum required for your application (typically 64-128 instead of the default 256). Deploy a reverse proxy or load balancer as a front-end that absorbs HTTP/2 connection complexity and forwards only validated, complete requests to backend servers using HTTP/1.1. Enable connection-level rate limiting based on stream lifecycle patterns rather than just request count. For environments where HTTP/2 is not required, consider downgrading to HTTP/1.1 as a temporary mitigation until all infrastructure is patched. Deploy a WAF with HTTP/2 protocol-level inspection that can detect and block Rapid Reset patterns in real time.

Code Examples

Mitigation: Nginx HTTP/2 hardening against Rapid Reset
# Nginx configuration hardened against HTTP/2 Rapid Reset (CVE-2023-44487)
# Requires Nginx β‰₯ 1.25.3 (or backported security patch)

worker_processes auto;
worker_rlimit_nofile 65535;

events {
worker_connections 65535;
multi_accept on;
use epoll;
}

http {
# === HTTP/2 stream limits ===

# Maximum concurrent streams per connection (default 128)
# Lower values reduce Rapid Reset impact but may slow legitimate multiplexing
http2_max_concurrent_streams 64;

# Maximum number of requests per HTTP/2 connection before forcing reconnect
# Limits total stream IDs an attacker can cycle through
keepalive_requests 1000;

# Connection idle timeout β€” close unused connections quickly
keepalive_timeout 15s;

# === Rate limiting at the request level ===
limit_req_zone $binary_remote_addr zone=http2_rate:10m rate=50r/s;
limit_conn_zone $binary_remote_addr zone=http2_conn:10m;

server {
listen 443 ssl;
http2 on;

ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/private/server.key;

# TLS 1.2+ only (HTTP/2 requires TLS)
ssl_protocols TLSv1.2 TLSv1.3;

# Limit connections per IP
limit_conn http2_conn 20;

# Limit request rate per IP
limit_req zone=http2_rate burst=100 nodelay;

# Aggressive timeouts for request processing
client_header_timeout 5s;
client_body_timeout 10s;
send_timeout 10s;

location / {
proxy_pass http://backend;
proxy_http_version 1.1; # Forward to backends as HTTP/1.1
proxy_set_header Connection "";
proxy_connect_timeout 3s;
proxy_read_timeout 15s;
}
}
}
Detection: HTTP/2 Rapid Reset pattern analysis from access logs
import re
import time
import logging
from collections import defaultdict
from datetime import datetime, timedelta

logger = logging.getLogger('rapid_reset_detector')

def parse_nginx_log_line(line):
"""Parse Nginx combined log format with HTTP/2 info"""
# Pattern: IP - - [timestamp] "METHOD URI PROTO" status bytes "ref" "ua"
pattern = (
r'(\S+) \S+ \S+ \[([^\]]+)\] '
r'"(\S+) (\S+) (\S+)" (\d+) (\d+) '
r'"([^"]*)" "([^"]*)"'
)
match = re.match(pattern, line)
if not match:
return None
return {
'ip': match.group(1),
'timestamp': match.group(2),
'method': match.group(3),
'uri': match.group(4),
'protocol': match.group(5),
'status': int(match.group(6)),
'bytes': int(match.group(7)),
'user_agent': match.group(9)
}

def detect_rapid_reset_patterns(
log_file='/var/log/nginx/access.log',
window_seconds=10,
status_499_threshold=50,
zero_bytes_threshold=100
):
"""Detect HTTP/2 Rapid Reset indicators in Nginx access logs.

Key indicators:
- Status 499: client closed connection before response (RST_STREAM)
- Status 0: connection reset with no response sent
- 0-byte responses: request cancelled before body sent
- HTTP/2 protocol with high request rates from single IPs
"""
ip_windows = defaultdict(lambda: {
'total': 0, 'status_499': 0, 'status_0': 0,
'zero_bytes': 0, 'http2_requests': 0,
'user_agents': set()
})

with open(log_file, 'r') as f:
for line in f:
entry = parse_nginx_log_line(line.strip())
if not entry:
continue

ip = entry['ip']
stats = ip_windows[ip]
stats['total'] += 1

if entry['status'] == 499:
stats['status_499'] += 1
elif entry['status'] == 0:
stats['status_0'] += 1

if entry['bytes'] == 0:
stats['zero_bytes'] += 1

if 'HTTP/2' in entry['protocol']:
stats['http2_requests'] += 1

stats['user_agents'].add(entry['user_agent'])

# Analyze for Rapid Reset patterns
alerts = []
for ip, stats in ip_windows.items():
if stats['total'] < 10:
continue

cancelled_pct = (stats['status_499'] + stats['status_0']) / stats['total'] * 100
zero_bytes_pct = stats['zero_bytes'] / stats['total'] * 100

# Rapid Reset signature: high cancellation rate + HTTP/2 + high volume
is_suspicious = (
cancelled_pct > 80 and
stats['http2_requests'] > stats['total'] * 0.5 and
stats['total'] > status_499_threshold
)

if is_suspicious:
alerts.append({
'ip': ip,
'total_requests': stats['total'],
'cancelled_pct': round(cancelled_pct, 1),
'zero_bytes_pct': round(zero_bytes_pct, 1),
'http2_pct': round(stats['http2_requests'] / stats['total'] * 100, 1),
'user_agents': len(stats['user_agents']),
'severity': 'critical' if stats['total'] > 1000 else 'high'
})
logger.critical(
f'RAPID RESET PATTERN: {ip} β€” {stats["total"]} requests, '
f'{cancelled_pct:.0f}% cancelled, {stats["http2_requests"]} HTTP/2'
)

return alerts

if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
alerts = detect_rapid_reset_patterns()
print(f'Detected {len(alerts)} suspicious IPs')
for alert in alerts:
print(f' {alert["ip"]}: {alert["total_requests"]} req, '
f'{alert["cancelled_pct"]}% cancelled')
Audit: Check HTTP/2 implementation versions for CVE-2023-44487 patches
#!/bin/bash
# Audit script to verify HTTP/2 components are patched against
# CVE-2023-44487 (Rapid Reset). Run on each server in your infrastructure.

echo "=== HTTP/2 Rapid Reset (CVE-2023-44487) Patch Audit ==="
echo

# Check Nginx
if command -v nginx &>/dev/null; then
version=$(nginx -v 2>&1 | grep -oP '[\d.]+')
echo "Nginx: $version"
if [[ "$(printf '%s\n' "1.25.3" "$version" | sort -V | head -1)" == "1.25.3" ]]; then
echo " βœ“ Patched (β‰₯ 1.25.3)"
else
echo " βœ— VULNERABLE β€” update to β‰₯ 1.25.3"
fi
# Check if stream limits are configured
if nginx -T 2>/dev/null | grep -q 'http2_max_concurrent_streams'; then
echo " βœ“ http2_max_concurrent_streams configured"
else
echo " ! http2_max_concurrent_streams not set (using default)"
fi
fi

# Check Apache
if command -v httpd &>/dev/null || command -v apache2 &>/dev/null; then
version=$(httpd -v 2>/dev/null || apache2 -v 2>/dev/null | grep -oP '[\d.]+')
echo "Apache: $version"
if [[ "$(printf '%s\n' "2.4.58" "$version" | sort -V | head -1)" == "2.4.58" ]]; then
echo " βœ“ Patched (β‰₯ 2.4.58)"
else
echo " βœ— VULNERABLE β€” update to β‰₯ 2.4.58"
fi
fi

# Check HAProxy
if command -v haproxy &>/dev/null; then
version=$(haproxy -v 2>&1 | grep -oP '[\d.]+')
echo "HAProxy: $version"
fi

# Check Node.js
if command -v node &>/dev/null; then
version=$(node -v | tr -d 'v')
echo "Node.js: $version"
major=$(echo "$version" | cut -d. -f1)
if [[ $major -ge 21 ]]; then
echo " βœ“ Major version β‰₯ 21"
elif [[ $major -eq 20 ]]; then
if [[ "$(printf '%s\n' "20.8.1" "$version" | sort -V | head -1)" == "20.8.1" ]]; then
echo " βœ“ Patched (β‰₯ 20.8.1)"
else
echo " βœ— VULNERABLE β€” update to β‰₯ 20.8.1"
fi
elif [[ $major -eq 18 ]]; then
if [[ "$(printf '%s\n' "18.18.2" "$version" | sort -V | head -1)" == "18.18.2" ]]; then
echo " βœ“ Patched (β‰₯ 18.18.2)"
else
echo " βœ— VULNERABLE β€” update to β‰₯ 18.18.2"
fi
fi
fi

# Check Go
if command -v go &>/dev/null; then
version=$(go version | grep -oP '[\d.]+')
echo "Go: $version"
if [[ "$(printf '%s\n' "1.21.3" "$version" | sort -V | head -1)" == "1.21.3" ]]; then
echo " βœ“ Patched (β‰₯ 1.21.3)"
else
echo " βœ— VULNERABLE β€” update to β‰₯ 1.21.3"
fi
fi

# Check if HTTP/2 is enabled on common ports
echo
echo "=== HTTP/2 Exposure Check ==="
for port in 443 8443 8080; do
if ss -tlnp | grep -q ":${port} "; then
echo "Port $port: LISTENING"
fi
done

PowerWAF automatically blocks HTTP/2 Rapid Reset Attack at the edge.

Deploy in minutes. No code changes required. Free plan available.

Free plan spots are limited

Frequently Asked Questions

Traditional HTTP floods require completing TCP handshakes and sending full HTTP requests, generating response traffic that helps the server's rate limiting and the attacker's bandwidth costs. HTTP/2 Rapid Reset creates an asymmetry: the attacker sends minimal data (HEADERS + RST_STREAM, ~30 bytes per cycle) while the server performs expensive operations (memory allocation, header decompression, request routing, resource cleanup) for each cycle. Additionally, because the stream is cancelled before completion, it bypasses request-based rate limiting, access logging, and response-based metrics. A 20,000-node botnet using Rapid Reset achieves 200M+ requests/sec β€” an equivalent HTTP/1.1 flood would require millions of bots.
Yes, but at a significant performance cost. HTTP/2 provides header compression (HPACK), stream multiplexing, server push, and stream prioritization that improve page load times by 15-50% and reduce connection overhead. Disabling HTTP/2 forces fallback to HTTP/1.1, requiring more TCP connections (one per concurrent request), more bandwidth (uncompressed headers), and higher latency for complex pages. A better approach: patch your HTTP/2 implementation and configure stream rate limiting. If you must disable HTTP/2 as an emergency measure, do so only temporarily until patches are applied.
Check the version of every component in your HTTP/2 chain: Nginx β‰₯ 1.25.3 (or backported patch), Apache β‰₯ 2.4.58, HAProxy β‰₯ 2.8.4/2.7.6/2.6.16, Node.js β‰₯ 18.18.2 or β‰₯ 20.8.1, Go β‰₯ 1.21.3, .NET β‰₯ 6.0.23/7.0.12/8.0.0, Tomcat β‰₯ 8.5.94/9.0.81/10.1.14. Don't just check your edge proxy β€” internal load balancers, API gateways, service mesh sidecars (Envoy), and gRPC servers also need patching. Verify that stream rate limiting is actually configured, not just that the patched version is installed. Many patches add the capability but don't enable aggressive limits by default.
Yes, but only if the WAF has HTTP/2 frame-level visibility β€” traditional WAFs that inspect only HTTP request/response content cannot see the underlying HEADERS/RST_STREAM frame patterns. A WAF with protocol-level inspection can detect the attack signature: high RST_STREAM rates, HEADERS frames immediately followed by RST_STREAM without waiting for a response, and stream IDs incrementing rapidly without any completed request/response exchanges. PowerWAF provides HTTP/2 Rapid Reset protection through behavioral analysis of stream lifecycle patterns, automatically identifying and blocking connections exhibiting cancellation rates that indicate attack behavior rather than legitimate browsing.
HTTP/3 uses QUIC over UDP instead of TCP, and its stream cancellation mechanism (STOP_SENDING + RESET_STREAM frames) differs architecturally. QUIC includes built-in flow control and connection migration that may limit some rapid reset patterns. However, the fundamental asymmetry β€” opening a stream is cheap for the client, expensive for the server β€” exists in any request-based protocol. QUIC implementations should proactively implement stream creation rate limiting and cancellation rate monitoring based on the lessons learned from CVE-2023-44487. As of early 2026, no equivalent rapid reset vulnerability has been demonstrated in major QUIC implementations, but the protocol is still maturing and security researchers continue to analyze it.