Artica Wiki

EdgeGuard — Rate Limits

Limiting how often one client may ask: what the allowance actually counts, why burst does not decide anything, and how to prove a limit works.

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.

console-waf-04-rate-limits.png

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

How the counting works

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.

  1. A request arrives whose path matches the rule.
  2. The counter for that address, that rule and that site is incremented.
  3. If the count has passed the allowance, the rule's action is taken.
  4. The counter expires when the window does, and the client starts again.

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.

Where they sit in the decision

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.

Creating a rule

console-dlg-06-rate-limit.png

Click Add Rule.

  1. Name — what it protects, e.g. Login attempts.
  2. Path Pattern — * for everything, /api/* for a prefix, or an exact path. A limit on * applies to assets too, which is rarely what anybody wants.
  3. Requests per window — the allowance inside the window, and the number that decides.
  4. Burst — what the remaining-header counts down from.
  5. Window (sec) — the period the allowance is counted over.
  6. Action — see below.
  7. Priority — highest first; the first matching rule decides.
  8. Response Code — default 429.
  9. Include rate limit headers — adds X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset so well-behaved clients can back off.
  10. Scope — Global, Group or Tenant → Create.

Burst

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.

The actions

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.

Verifying

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.

Choosing limits

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.

When a rate limit is the wrong tool

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