HTTP/2 Rapid Reset makes stream limits a security control
A review of HTTP/2 stream accounting, cancellation cleanup, and edge-to-backend capacity limits.
TTP/2 multiplexes many requests over one connection. That efficiency also gives a client a way to make a server allocate work and then cancel it almost immediately. The Rapid Reset pattern sends request streams and RST_STREAM frames in quick succession. Implementations that account for the work before enforcing a useful connection limit can spend capacity on requests that no longer have a client waiting for a response.
CVE-2023-44487 recorded the pattern as an HTTP/2 denial-of-service weakness. The lesson applies across implementations. A stream limit controls how much concurrent state and upstream work a peer can cause the server to create.
What the protocol permits
RFC 9113 defines SETTINGS_MAX_CONCURRENT_STREAMS as a directional limit. A server tells the client how many streams it will allow, and the client tells the server how many server-initiated streams it will accept. The setting bounds concurrent streams, but implementations still have to process connection and stream state quickly and consistently.
The reset is the difficult part. A reverse proxy may receive a request, reserve a worker, open an upstream connection, and then learn that the client cancelled the stream. If cleanup is delayed while new streams continue arriving, the proxy can accumulate state faster than it releases it. A blunt limit on inbound reset frames may not be enough because the attack can vary its timing.
Cloudflare's analysis describes the attack as opening many streams and cancelling each request without waiting for a response. Google's analysis notes that mitigation based only on an inbound reset rate can be bypassed by variants that spread the resets differently. Capacity has to be bounded at each layer, including the path before a request reaches an upstream service.
Where the control belongs
Start with the edge server and every proxy that accepts HTTP/2. Record the configured maximum concurrent streams, connection lifetime, request rate, upstream connection pool, and timeout behavior. Then verify what happens after a reset. The worker should stop work that has no remaining requester, release the upstream slot, and close a connection when its request budget is exhausted.
The application also needs a budget. A proxy can pass a request upstream before the client resets it, so an application endpoint that starts expensive work immediately remains exposed. Per-identity and per-connection limits should apply before database queries, report generation, large downloads, or fan-out to other services. Global concurrency limits protect the backend when many connections arrive together.
keepalive_requests is one useful control in NGINX because it limits requests served over a connection. It is not a complete HTTP/2 policy. The right value depends on traffic, upstream behavior, and load-balancer capacity. Raising it far above the default for convenience extends the period in which a client can create and abandon work.
Tests that reveal the failure
Use an isolated test environment and send a bounded pattern of headers followed by resets. Measure live streams, upstream requests, worker occupancy, temporary memory, and time to cleanup. Confirm that the client cannot cause the upstream request count or connection state to grow without a matching decrement.
Test through the production topology: CDN, load balancer, reverse proxy, application server, and any service mesh. A limit that appears correct in one component may be bypassed when another component multiplexes requests into a new connection or retries a cancelled request. Record whether retries inherit the client's cancellation signal.
Instrument reset ratios, concurrent stream counts, requests per connection, upstream cancellations, and queue depth. Alert on sustained divergence between accepted requests and completed or cancelled work. A short spike can be legitimate, but a growing gap with stable client demand deserves investigation.
Defensive baseline
Set explicit concurrent-stream and request-per-connection limits at the edge. Keep upstream pools smaller than the maximum work the backend can tolerate. Enforce application concurrency and timeouts before expensive work begins. Propagate cancellation to downstream calls, and make cleanup observable. Apply vendor patches as part of the baseline, but do not treat a version check as proof that the whole request path can shed abandoned work.
Rapid Reset is a protocol-level capacity problem. Defenders reduce its effect by making work admission, cancellation, and connection turnover explicit, then testing those controls across the actual proxy chain.
References
Did you enjoy this article?
Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.