Rate limits are for traffic that is too much rather than bad. A login form being guessed at, a search endpoint being scraped, an API being used as a database — none of it looks like an attack to a rule, because each individual request is perfectly legitimate.

| Column | Meaning |
|---|---|
| PRIORITY | evaluation order, highest first |
| NAME | and what it is for |
| PATH | the pattern it applies to |
| LIMITS | the allowance and the window |
| ACTION | what happens when the allowance is spent |
| TENANT | scope: global, a group, or one site |
| STATUS | enabled or disabled |
Each rule counts per client address, per rule, per site. The counter lives in the runtime store (Valkey) with a lifetime equal to the rule's window, so it disappears on its own rather than needing to be cleaned.
The allowance is counted over the window, not per second. With Requests per window 100 and Window 60, the policy is "100 requests per minute". The console spells the two out together as you type them.
The field was labelled Requests/sec until central 2.4.35, and the rule list rendered it as "100 req/s" — the opposite of what the engine does. If you sized a limit against that label, read it again: it was never a per-second rate.
Burst is what the X-RateLimit-Remaining header counts down from, so a client that reads the headers can slow down before it is refused.
If the runtime store is unreachable, rate limit rules are skipped. The engine does not hold up a request waiting for a counter — it has 100 milliseconds to answer and the request proceeds without the limit if it does not. Rate limiting is therefore a best-effort control, not a guarantee; do not use it as the only thing standing between an attacker and something expensive.
After the firewall rules, the behavioural analysis and the bot checks; before the custom rules and the managed rulesets. So an address already blocked by a list or a firewall rule never reaches a rate limit, and a request that passes the limit still meets every detection rule afterwards.

Click Add Rule.
Login attempts.* for everything, /api/* for a prefix, or an exact path. A limit on * applies to assets too, which is rarely what anybody wants.X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset so well-behaved clients can back off.Burst does not change the decision. It is stored and shown, and the engine refuses a request when the count inside the window passes the allowance above — nothing consults Burst on the way.
Until engine 3.0.39 its one use was computing X-RateLimit-Remaining, which made that header lie in both directions: a burst below the limit told a well-behaved client it had nothing left while requests were still being served, and one above it promised room that did not exist. The header is now counted against the allowance that decides.
The field remains because it is somebody's stored configuration, and the console says what it does. A real token bucket — a short spike allowed above the average — is a change to how traffic is refused, not a rename, and it is not shipped yet.
| Action | What the client gets |
|---|---|
| Rate Limit (429) | the standard answer: Too Many Requests |
| Block (403) | a refusal that reads as a security decision rather than a quota |
| Slider Puzzle | an interactive challenge — a person continues, a script does not |
| JS Fingerprint | a silent challenge a real browser passes |
| Throttle | the response is delayed instead of refused, which slows a scraper without breaking a user |
| Queue (503 + Retry-After) | asks the client to come back later, and says when |
Throttle and Queue are the interesting ones: they degrade rather than deny, which is usually the right answer for a customer who is merely enthusiastic, and an effective one against automation that does not implement waiting.
Send more requests than the allowance and watch the answer change:
for i in $(seq 1 30); do
curl -s -o /dev/null -w '%{http_code} ' \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/140.0.0.0' \
"https://site.example.com/api/search?q=test"
done; echo
You should see a run of 200 and then the rule's code — 429 by default.
Run against a rule of 5 per 60 seconds, on a live installation, that reads:
request 1 → HTTP 404
request 2 → HTTP 404
request 3 → HTTP 404
request 4 → HTTP 404
request 5 → HTTP 404
request 6 → HTTP 429 limit=5 remaining=0 reset=60
request 7 → HTTP 429 limit=5 remaining=0 reset=60
(The 404s are the origin answering for a path that does not exist — the rule was
placed on one deliberately, so that proving it could not refuse a real visitor.
What matters is where the answer changes.)
The headers travel on the refusal, not on every response. X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset appear when the rule acts; the requests it let through carry whatever the origin sent. A client that expects a countdown on every response will not get one here.
That rule was written with an allowance of 5 and a burst of 100, deliberately: before engine 3.0.39 the remaining count was computed from the burst, so this refusal would have carried remaining=95 while the engine was refusing the request.
Each refusal is recorded in Security Events with the reason rate_limit_rule and the rule's name, so you can tell a limit from a block at a glance.
10 per 300s stops guessing without troubling anybody who typed their password wrong twice.*) — only as a very high ceiling against floods, and prefer DDoS Protection → Adaptive Rate Limiting, which moves its thresholds with the traffic instead of holding a fixed number.Start with the action set to Throttle or with a generous allowance, watch Security Events for a day, and tighten. A limit that is too strict produces support tickets from customers, not from attackers.
| Situation | Better tool |
|---|---|
| The requests are individually malicious | the rulesets — a limit would let the first ones through |
| One address is responsible for all of it | an IP list entry with an expiry |
| A crawler is polite but relentless | Bot Management: allow it and cap it there |
| A flood rather than a stream | DDoS Protection, whose adaptive limiting and slow-request defences are built for it |