Let’s talk about impact. In the first post we met MadeYouReset and how it bypasses Rapid Reset mitigations. In the second, we went down the rabbit hole of stream states, primitives, and why work keeps happening after a stream is reset. Here we’ll make it practical: what the attack is actually bounded by, how to measure it properly (and how not to), and where to find the “significant” impact in real systems.
MadeYouReset vs Regular HTTP/2 Flood Attacks
What Bounds a Regular Flood
A classic HTTP/2 request flood is limited by MAX_CONCURRENT_STREAMS
. Once the client hits the max number of active streams, it has to wait for responses to complete before opening new ones. That means the achievable request rate is bounded by how quickly the server computes responses - which is the time it takes to compute a resource. If this resource makes DB queries or accesses other services - it will be longer.
For example, with the common default of MAX_CONCURRENT_STREAMS = 100
and a resource that takes 50 milliseconds to compute, the flood attacker tops out around 2000 req/s (requests per second).
What Bounds MadeYouReset
MadeYouReset doesn’t wait for responses at all. Each request starts as a valid HTTP/2 stream, the server begins computing the response, and then the client triggers a stream error that makes the server send RST_STREAM
for us. From the protocol’s perspective, the stream becomes closed immediately and no longer counts toward MAX_CONCURRENT_STREAMS
. From the backend’s perspective, work keeps going.
Practically, the attack rate is bounded by two things:
- The attacker’s ability to generate traffic and frames (outgoing bandwidth and client CPU).
- The server’s HTTP/2 front‑end throughput: how fast it can parse HEADERS and swallow the small, reset‑triggering control frames, plus how quickly it can enqueue/dispatch backend work.
On the wire, the incremental cost to trigger the reset is tiny. For example, a WINDOW_UPDATE
frame’s payload is 13 bytes. The exact size depends on the primitive you pick, but the idea holds: compared to the backend work you’ve already caused, your marginal cost is negligible.
So while a regular flood is capped by response latency, MadeYouReset is capped by throughput (yours and the server’s front‑end) and the server’s request‑processing rate.
How Not To Measure Impact
It’s important to test MadeYouReset the right way, because if you benchmark the wrong thing you won’t realize the attack’s potential.
It’s natural to start with a simple server that returns “hello world”. That’s perfectly fine to check which primitives work on the server, but it’s a terrible way to measure impact. There are three problems with that. First, the work per request is trivially easy: the server hardly does any computation, so the “unbounded” concurrency we create doesn’t translate into meaningful backend load. Second, the responses are very fast: requests complete so quickly that we can’t build up a large number of in‑flight operations in the backend. And the third and most important problem - real web services don’t look like that - they do complex operations that take time.
It’s worth separating “easy” from “fast”. A request can be easy on the CPU and still take a long time because it’s waiting on something - an I/O operation, a mutex, a rate‑limit token, a remote service. Those slow paths are exactly where MadeYouReset shines: they stretch the lifetime of work inside the server while you keep creating more of it.
This is why a “hello world” benchmark underestimates impact. Minimal computation keeps CPU usage low, short lifetimes keep memory and queues calm, and you walk away thinking MadeYouReset behaves like a regular flood. It doesn’t. Returning a static string or a tiny JSON without touching anything else gives you very little backend work per request. Streams reset fast, work finishes fast, memory pressure stays low. But that’s not what we are aiming for.
Where To Find Significant Impact
Solid Impact
We’re looking for the exact opposite of the hello‑world trap: a route that kicks off real work and keeps the backend busy. Two qualities matter most. First, the work should be hard enough to meaningfully consume resources - CPU‑heavy parsing, rendering, compression, complex queries, anything that actually makes your server sweat. Second, it should take time. Latency stretches the lifetime of each in‑flight request so you can accumulate many of them while continuing to create more.
This is where the asymmetry with a regular flood becomes obvious. In a classic flood, higher latency hurts the attacker because MAX_CONCURRENT_STREAMS
caps active requests - the slower each one is, the fewer requests per second you can sustain. With MadeYouReset, added latency doesn’t reduce the rate - it amplifies impact by letting more backend work pile up concurrently.
In practice, pick endpoints that start doing substantial computation or I/O before writing any response bytes. A worst‑case regex on user input, a cache‑miss that hits a remote database, a report that renders templates and queries storage - anything that both burns cycles and takes long enough to sit “in flight” is a prime candidate. The heavier and longer‑lived the work, the more significant the impact you’ll measure with MadeYouReset.
Significant Impact
Solid impact is nice. Significant impact is where things tip over.
Consider what happens as the server gets overloaded. In a regular flood, higher latency feeds back into the attacker’s rate: with MAX_CONCURRENT_STREAMS
capping active requests, slower responses directly reduce how many new requests per second the attacker can issue. It’s a kind of natural built‑in self‑throttling.
MadeYouReset doesn’t play by that rule, because its ability to create new work isn’t tied to response time. It keeps injecting requests while the backend’s latency stretches. There are practical ceilings, of course - eventually the HTTP/2 front‑end and the network become the bottleneck, and the TCP window can fill - but the sustainable rate remains far above what a regular flood can maintain under the same load.
Think about it this way: the longer each request lingers inside the server, the bigger the pile gets. If you keep the arrival rate high while that “linger time” grows, the number of in‑flight requests climbs - and keeps climbing. Let it run and you either run out of memory or the server starts slamming the door on real users with 503s “Service Unavailable”. MadeYouReset keeps the arrival rate high even as the server slows down. That’s the whole trick.
The endpoints we are looking for are those whose latency grows significantly as load rises. If latency settles to a steady value, impact plateaus. If it keeps climbing with load, the effect compounds and turns a strong attack into a significant one.
How can we make latency grow significantly in modern, highly concurrent systems?
We don’t have to beat them at their own game - let’s pick operations that are naturally non‑concurrent: sequential operations.
Significant Impact #1 - SQL UPDATE Operation
Consider a web server with an endpoint (resource) that updates a value in the logged-in user’s database row. For example, the number of pages the user has seen. To make it concrete, let’s say the database is MySQL on another server.
Two updates to the same row can’t happen at once - it wouldn’t make sense. One finishes, then the next starts.
What happens under load?
A queue of database update queries forms quickly (as fast as our request rate). Because every request hits the same row, they run one by one, so the time per request grows.
More requests → more update queries → a larger queue → longer waits per query → higher per-request latency → even more requests pile up concurrently.
And this situation just keeps getting worse and worse.
A flood attacker can’t push it this far: the concurrency limit of HTTP/2 keeps only MAX_CONCURRENT_STREAMS
(usually 100) requests in flight.
Significant Impact Everywhere
I chose the UPDATE
operation because it’s the clearest one‑at‑a‑time example. It turns out that every SQL operation we will choose will have sequential “properties” - because of the way the MySQL protocol works.
A MySQL server runs many things in parallel across connections, but on a single connection it handles one statement at a time - queries on that connection run in order!
Most web servers use a small database connection pool. On any single connection, work is handled in order. Across the pool, plenty can run in parallel. Since the number of connections is relatively small, MadeYouReset can easily outpace them by an order of magnitude - which will cause requests to queue up - the same effect we saw with UPDATE
.
Side note: Even with multi‑statement features (similar to HTTP/1.1 pipelining), the server executes them in order on that connection.
So the bottom line is that if an HTTP/2 server exposes endpoints that query a database (even with a simple SELECT
query) then it is very possible that MadeYouReset can drive unbounded in‑flight work and memory growth, which can result in either a DoS and/or OOM crash.
Before we see it in practice, let’s talk about a very natural way to avoid it - which really doesn’t help.
Trying To Prevent Significant Impact
While making significant impact we relied on the following observation: MadeYouReset thrives when attacking endpoints that run one‑at‑a‑time so latency keeps growing and the number of active requests gets bigger.
In practice, servers have timeouts. After a few seconds you’ll get a 503 Service Unavailable.
But that won’t stop the impact - timeouts cap response time, but they don’t necessarily stop the work.
It basically behaves like HTTP/2 stream cancellation: the HTTP request ends, but the operations it started keep running. The SQL update may already be sitting in the pool’s queue, or already sent to the database. Memory and work in other components persist - so timeouts won’t prevent the significant impact.
What if the DB driver actively cancels in‑flight queries or drops queued work when the originating HTTP request is cancelled? That can reduce - or even prevent - the impact. However, doing this correctly end‑to‑end is hard, and many stacks don’t wire it through. We’ll dive into this in the next post.
Demo
The demo shows a Hyper server written in Rust that uses sqlx with a pool of connections to query a MySQL database.
The code is on my GitHub.
In the next post, we will see why this specific example is actually really interesting.
Summary
We saw why regular floods are capped by concurrency and latency, while MadeYouReset rides on throughput and keeps the backend busy - especially on endpoints that do real work and make requests wait their turn. We also saw how MadeYouReset can exploit sequential operations which can cause complete DoS and OOM.
Next up: request cancellation. We’ll look at why implementing request cancellation in the backend might not be the silver bullet against MadeYouReset, and how in some scenarios it can be bypassed.