A rate-based rule looks like the simplest control in AWS WAF. A number of requests, a span of time, an action when the count is exceeded. That apparent simplicity is exactly what makes it easy to deploy wrong and hard to notice you did.
Three decisions hide inside that one threshold, and each is a separate way to be quietly ineffective. You choose what a "client" is, by picking the aggregation key. You choose whether crossing the line does anything, by setting the action to Count or Block. And you choose what the threshold means, by pointing it at a trailing window that estimates rather than counts. Get any one of those wrong and traffic flows to your origin while the console shows a rule that is present, associated, and, on every metric it exposes, working.
The whole AWS WAF stack, enrollment through origin exposure, is walked in AWS DDoS testing, and the method under all of it in the complete guide to DDoS testing. Those treat rate-rule calibration at overview level: pick a threshold, sweep traffic across it, confirm it engages. This post drills into one control on that stack and the mechanics the parent only points at, because the threshold is the least interesting of the three decisions, and the two more interesting ones are where a rule that reads as active turns out to be counting the wrong thing. On AWS this is the same L7-edge question raised for Azure in testing Front Door and WAF policies.
A rate-based rule is three decisions, not one number
Before the threshold ever matters, a request passes through a small pipeline inside the rule. It is worth tracing, because every failure in this post lives at one of its stages.
| Decision | What it controls | The failure it produces | What a test measures |
|---|---|---|---|
| Scope-down statement | Which requests are counted at all | A flood that falls outside the scope is never counted | Whether the real attack traffic is inside the scope |
| Aggregation key | What counts as "one client" | Distributed traffic under the per-key line; false positives on shared IPs | Whether the key matches how real and hostile clients present |
| Action (Count / Block) | Whether crossing the line drops anything | A rule that logs floods and forwards them | Origin response, not the metric count |
| Threshold and window | When the estimated rate trips | A boundary burst completes before the estimate crosses | Time from onset to actual drop; the true knee |
Read left to right, the pipeline explains why a threshold can be perfectly calibrated and still protect nothing. If the scope-down excludes the attack, the count never rises. If the key is wrong, the count is spread across a thousand buckets that each stay low. If the action is Count, the count rises, the metric climbs, and every request is forwarded anyway. The threshold is the last gate, and by the time traffic reaches it, three earlier decisions have already determined whether it can do its job.
Count mode: the rule that logs the flood and forwards it
Every rule in AWS WAF carries an action, and a rate-based rule is no exception. When a key's estimated rate crosses the threshold, the rule takes that action against the offending requests. In Block, they get a 403 at the edge. In Count, the rule increments a CloudWatch metric, attaches a label the request carries downstream, and forwards it to the origin unchanged.
Nothing before the action differs between the two. The same requests are weighed, the same CloudWatch metric ticks up, the same sampled request lands in the console. What diverges is the one property that matters, whether the offending request lives or dies, and everything the rule exists to do rides on it.
That split between spotting an over-rate client and actually shedding its traffic is the block versus count distinction, and it is the most portable finding in edge security. It shows up on AWS as a managed rule frozen in Count and has a sibling on every other platform. A rate-based rule in Count has done all of the work except the part that helps.
Why the rule sits in Count, and why it stays there
Count is not a blunder at the start. A freshly written threshold will trip on some legitimate burst before it is proven, so teams park the rule in Count to watch what it would have shed, tune the threshold and the scope against live traffic, and switch the action to Block only once the false positives are gone. That sequencing is sound.
What goes wrong is that the switch never gets flipped. A validation meant to run for a sprint settles in as the resting state of the rule. Months later it is still diligently tallying every burst and handing every one to the application, and during an incident the line of counted requests climbing on the dashboard looks, to anyone who has not opened the rule to read its action, precisely like a defense at work.
What the test has to measure
The action is the single property the telemetry conceals, since Count and Block drive an identical metric. The only surface that renders the difference legible is the origin.
The test pushes a rate past the threshold from enough distinct keys to trip the rule, then checks who answered: a 403 returned at the edge, or a 200 served by the application. If the counter climbed while the origin kept returning 200s, the action is Count, whatever the graph looks like. Count has a legitimate advanced use as a signal feeding a downstream rule, or a label a later rule keys on, and where that is the design the test validates the pair rather than the single rule. What it never does is read a rising counter as proof that anything was stopped.
Scope-down: the traffic the rule was told to ignore
A rate-based rule can carry a scope-down statement, a filter that decides which requests are counted toward the rate at all. It is a genuinely useful control: rate-limit only POST /login, or only requests missing a known good header, or only a specific URI prefix, and leave everything else uncounted so the rule stays focused.
The failure mode is that the scope-down is narrower than the person reading the rule believes. Anything outside it is not merely allowed, it is invisible to this rule, outside the universe the counter can see.
Scope the rule to URI starts-with /api/ and a flood against / is never counted. Scope it to exclude requests bearing a verified-bot header and an attacker who sets that header exempts himself. The rule is working precisely as written; the trouble is that what it was written to watch is not where the attack landed.
A test does not stop at "is a rate-based rule present." It confirms the attack traffic actually falls inside the scope-down, and it does the more revealing thing on purpose: it drives traffic deliberately positioned just outside the scope and confirms the rule is blind to it, so the boundary of what the rule even considers is a measured fact rather than an assumption buried in a nested statement.
The aggregation key decides what "a client" means
Here is the decision the parent post does not reach, and the one that determines more than the threshold ever will. A rate-based rule counts per key, and you choose the key. The default is source IP, but AWS WAF can aggregate on a forwarded IP address, a custom header, a cookie, a query-string argument, the HTTP method, the URI path, or a combination of up to five of these.
Every choice of key is a definition of identity. And identity is the one thing the attacker fully controls in how he presents it.
Source IP is the obvious key and the one with two opposite blind spots. It cannot see a spread-out attack, where every participating address stays quiet enough on its own to clear the limit and only the sum of thousands of them, the carpet-bombing shape, is large. And it lands hard on the shared address: a corporate egress, a mobile carrier's CGNAT, a VPN concentrator each fold hundreds of real users behind one IP, so a limit calibrated to a single user's rate fires on the whole crowd and becomes a false-positive engine.
A forwarded IP address, read from X-Forwarded-For or a header you name, is the usual fix for the CGNAT problem, because it recovers the real client behind a proxy that overwrites the header. The catch is trust. Unless a hop you control rewrites that header on every request, its value is client-supplied, and keying on client-supplied input against an adversary is a trap: he sets a fresh value per request, every request looks like a new client, and the counter never accumulates. You have asked the attacker to choose which bucket to fall in. This is the IP spoofing problem moved up to L7, where the address is just a string in a header.
A custom header, cookie, or query argument keys on something the application assigns, a session cookie or an API token, which is exactly right for rate-limiting authenticated abuse where each account should carry its own budget. It is blind, by construction, to any request that omits the value. An attacker who simply does not send the cookie, or rotates a query argument each request, is never counted as a repeat client.
A combination key narrows the bucket, which raises precision on legitimate traffic, and multiplies the ways to land in a fresh, empty bucket. Change any one component of the tuple and the counter resets to zero. The tighter the identity you demand, the easier it is for an attacker to present a slightly different one.
The through-line is uncomfortable and worth stating plainly: the aggregation key is an assumption about how clients present identity, and the attack that beats it is not more powerful, it just declines to present identity the way the key assumed. The rate limiting is only as good as the aggregation key, and a test earns its keep by driving each rule against traffic engineered to manipulate its specific key: distributed under a per-IP rule, header-rotated under a forwarded-IP rule, cookie-absent under a cookie rule.
Thresholds and the window: the burst that finishes first
The threshold is evaluated against a trailing window. AWS WAF estimates the request rate for each key over the configured window, which can be one, two, five, or ten minutes, and takes the action when that estimate exceeds the limit. It is an estimate over a span, not an instantaneous count, and that distinction produces the behavior a single fixed-rate probe never reveals.
A sharp, short burst is averaged into the window. A spike that is high enough to hurt but brief enough to complete before the windowed estimate catches up reaches the origin, and there is propagation delay on top: the interval between the estimate crossing the limit and the block actually taking effect at the edge. Traffic that arrives inside that interval is forwarded. The boundary burst, timed against the window rather than sustained through it, is the traffic that slips under a threshold that is, on paper, set correctly.
The chart is a simulated shape, not measured data, drawn to show the interval rather than to quantify it. The number that matters is empirical and specific to your rule: drive a burst tuned to the window, record when attack traffic starts, when the rule begins returning 403s, and when origin-observed volume actually falls, and the gap between the first and last is the time-to-mitigation the console does not display. The same window has a tail on the way out. A blocked key stays blocked until its estimated rate falls back under the limit, which is the mitigation you want against a real flood and the penalty box a NAT egress sits in after one false trip.
Finding the true threshold by sweeping the rate is the calibration the parent post already covers, so it is enough to say here that one static rate answers a single yes-or-no, is this rate over the limit, where a sweep traces the whole curve and locates the knee. What is specific to this control is that the knee moves with the window and the key, so a threshold validated at one window length and one aggregation key is not evidence about another.
What the console cannot tell you
The AWS WAF console is honest about what it knows, and what it knows is narrow. It shows the rule is present, its action, a sample of matched requests, and the CloudWatch counts. It cannot show whether the aggregation key matches how your real clients present identity, whether the scope-down excludes the traffic an attacker will actually send, how wide the boundary-burst window is against a realistic vector, or what the rule costs legitimate users sharing an address.
Each is an outcome, and outcomes only appear at the origin, with a realistic legitimate-traffic model driven alongside the attack so the false-positive rate is counted rather than hoped for. A rule that turns away an HTTP flood and a batch of real customers on a shared egress in the same motion has not protected the service. It has traded one outage for another.
One more interaction is worth driving directly, because it puts two products in the same place. A rate-based rule and any Shield Advanced automatic mitigation both write into the same web ACL, share its capacity budget, and share an evaluation order, so which one fires first is a property of your priority configuration and not something to settle on a whiteboard. That two-control interplay is worked through in Shield Advanced testing.
Authorization, in one line
One AWS rule sits above all of this: high-volume simulated attacks run on their own authorization track, apart from routine penetration testing, and that clearance has to exist before the first packet. The current procedure lives in the authorization section of AWS DDoS testing, and keeping the run non-destructive follows testing without disrupting production.
FAQ
What is AWS WAF rate-based rule testing?
Controlled, authorized generation of traffic against a rate-based rule to measure what it actually does, rather than that it exists. It confirms the rule's action is Block and not Count, that its scope-down includes the real attack, that its aggregation key matches how clients present, and how wide the window is between a key crossing the threshold and the rule enforcing. All of that is measured at the origin, because the console shows configuration, not outcome.
What is the difference between Count and Block on a rate-based rule?
When a key exceeds the threshold, both actions record the event to the same CloudWatch metric. What diverges is the fate of the request: Block answers it with a 403 at the edge, Count tags it and lets it through to the origin. So a Count rule can log an entire flood and shed none of it, while its metric graph climbs like a rule that is enforcing. The response the origin sends, not the counter, is what separates the two.
Which aggregation key should a rate-based rule use?
It depends on the traffic and the threat, and each key has a distinct blind spot rather than a ranking. Source IP misses distributed attacks and penalizes shared NAT egresses. A forwarded IP recovers real clients behind a proxy but must not be attacker-settable. A custom header or cookie fits authenticated abuse but is blind to requests that omit the value. The right choice is the one whose blind spot your test has confirmed the attack cannot exploit.
Can an attacker bypass an AWS WAF rate-based rule?
Yes, in three ways that a test should try. Distribute the load so each key stays under the threshold; rotate the value of an attacker-controllable aggregation key so no bucket accumulates; or send traffic outside the rule's scope-down so it is never counted. Each is traffic engineered to sit under the line the rule draws, which is why the line has to be drawn against how a real adversary presents traffic.
How is the rate-based rule threshold evaluated?
Against a trailing window of one, two, five, or ten minutes, as an estimate of each key's rate rather than an instantaneous count. Because it is a windowed estimate with propagation delay before enforcement, a short burst can complete before the rule begins blocking. The exposure that matters is the interval from the rate crossing the threshold to origin volume actually falling, and it is measured under load, not read from the configuration.
The rule counts perfectly; the question is what it counts
A rate-based rule does its arithmetic flawlessly. It sorts each request into a bucket by the key you named, tracks each bucket's estimated rate across the window, and fires the configured action the moment an estimate clears the line. Nothing in it is broken. The rule is a faithful implementation of an assumption, and the assumption is where the exposure lives.
The assumption is about identity: that "a client" is whatever you decided to count by, that clients present that identity honestly, and that the traffic you care about falls inside the scope you drew. An attacker keeps all three assumptions in view and violates whichever is cheapest. He spreads across addresses under a per-IP rule, rotates a header under a forwarded-IP rule, drops a cookie under a cookie rule, or aims a step outside the scope. None of that defeats the counting. It simply refuses to be the thing being counted.
That is the durable half of this, the part still true in five years: the control is the key and the scope, not the number, because they decide what the number even measures. The threshold is the perishable half, a value tuned to a traffic shape the application will outgrow. A test that only sweeps the threshold validates the easy decision and leaves the two that matter untouched. The harder and more useful question is not whether the rule counts to the right number. It is whether it is counting the right requests at all.
