OpenWAF has three kinds of rate limits. They are counted at the edge, before requests reach your origin.
| Kind | Where | Counts | Use it for |
|---|---|---|---|
Rate limiting rules (rate_rules) | Edge setting, up to 25 per list | Per IP, header, cookie or JSON body field, in any period | Logins, one-time codes, password changes, expensive APIs. Recommended. |
Rate limit rules (kind=rate_limit) | Edge rule | Per IP, per minute | Simple per-path limits. |
Basic rate limit (rate_limit_rpm) | Edge setting | Per IP, per minute, whole site | A ceiling for everything. |
None is set by default.
Rate limiting rules
Each rule counts the requests that match a path (and, optionally, methods, a header or a query parameter) per key. When a key goes over requests in period seconds, the rule's action applies to it, optionally for a mitigation_timeout.
{"name": "Login", "path_prefix": "/auth/login", "methods": ["POST"], "key": "ip",
"requests": 60, "period": 900, "algorithm": "gcra", "action": "block", "mitigation_timeout": 900}How rules are evaluated
- Rate limiting rules run after IP, threat feed and country rules and before the other rate limits, also for allow-listed IPs and in every WAF mode. They never apply to
/__waf/paths. - Every rule that matches counts the request in its own counter, in list order.
- The first rule over its limit whose action is
blockorchallengeanswers.logrules only record an event and the request continues.
Keys
key | Counted by | Notes |
|---|---|---|
ip (default) | Client IP | IPv6 addresses are grouped by /64. |
header | First value of the header key_name | E.g. x-api-key. |
cookie | Value of the cookie key_name | E.g. a session cookie. |
body_field | A field of the JSON body, key_name like email or user.email (up to 4 levels) | Compared in lower case. Read only when Content-Type is application/json (or application/*+json) and the declared Content-Length is 1 byte to 8 KiB; the body reaches the origin untouched. |
When the key has no value (missing header or cookie, a body that isn't small JSON), the request is counted by IP: removing the header never escapes the rule. Key values never leave the edge node in clear and never appear in events.
Algorithms
sliding_window(default): up torequestsperperiod, counted over a sliding window.gcra(steady rate): a burst ofrequests, then one more everyperiod / requestsseconds. Someone who keeps trying gets slowed down evenly instead of waiting for the window to end. Recommended for logins.
Actions
action | Answer |
|---|---|
block (default) | 429 Too Many Requests with Retry-After (seconds). Browsers get the site's block page; other clients get {"error":"rate_limited","retry_after":N}. |
challenge | Browser navigations get the browser challenge and, once solved, pass without being counted while the pass lasts. Anything else (a login POST, an API call) gets the 429 of block. |
log | Only an event; the request continues. Use it to tune a limit before enforcing it. |
With mitigation_timeout greater than 0, the request that crosses the limit starts a penalty for that key: for that many seconds its matching requests get the action directly, without being counted. With 0, the action applies only while the key is over the limit.
Several edge nodes
Each edge node counts on its own. When several nodes serve your site, a client could spread its requests across them. With per_node_divisor: true (the default) each node allows ceil(requests / entry points), so the limit holds for all of them together. A legitimate client that always reaches the same node then gets only its share.
Set per_node_divisor: false when the edge limit must stay above your application's own per-IP limit, so your application answers first with its own message and the edge is only the safety net.
Templates
The dashboard offers these starting points (Start from a template):
[{"name": "Send code", "path_prefix": "/auth/send-otp", "methods": ["POST"], "requests": 40, "period": 600,
"action": "block", "mitigation_timeout": 600},
{"name": "Verify code", "path_prefix": "/auth/verify-otp", "methods": ["POST"], "requests": 60, "period": 600,
"action": "block", "mitigation_timeout": 600}]{"name": "General API", "path_prefix": "/api", "requests": 600, "period": 60, "action": "log"}Add a rule
Domain → Security → Rate limiting → Add rule, or pick a template under Start from a template. Set Requests that count, Count per, Limit, How to count and Then, apply, order the rules with the arrows and Save changes. The Last 24 h column shows how many requests each rule limited.
openwaf-cloud ctl ratelimit add example.com id=login path_prefix=/auth/login methods=POST limit=60/15m \
algorithm=gcra action=block timeout=15m per_node_divisor=false --reason "Login limit"
openwaf-cloud ctl ratelimit list example.com
openwaf-cloud ctl ratelimit set example.com login requests=80 --reason "More headroom"
openwaf-cloud ctl ratelimit rm example.com login --reason "No longer needed"limit=N/period and timeout=duration are shortcuts for requests/period and mitigation_timeout. Each command reads the list, changes it and saves it with a version check.
The rules are part of the edge settings and a PATCH replaces the whole list ([] removes them all). Read the current list first with GET …/edge.
curl -sS -X PATCH "https://dash.openwaf.dev/v1/orgs/$ORG/zones/example.com/edge" \
-H "Authorization: Bearer $OPENWAF_TOKEN" \
-H "X-Change-Reason: Login limit" \
-H "Content-Type: application/json" \
-d '{"rate_rules": [{"name": "Login", "hosts": ["api.example.com"], "path_prefix": "/auth/login",
"methods": ["POST"], "requests": 60, "period": 900, "algorithm": "gcra", "action": "block",
"mitigation_timeout": 900, "per_node_divisor": false}]}'update_settings with settings.rate_rules (the whole list). Simulate, review the diff, then confirm with confirm=true and expected_version.
Every field is described in the rule reference.
One list per name
A name can have its own list in hosts.<name>.rate_rules: it replaces the zone's list for that name ([] = no rules for it; null = use the zone's list again). Use --host www.example.com with the ratelimit commands, or host with update_settings. The dashboard shows these names but doesn't edit their lists yet. See Per-name settings.
Events
Each action leaves a security event with rule_name rate_rule:<id>, attack_type rate_limit, status 429, 503 (challenge) or 200 (log) and a message such as Rate rule login: over 60 per 900 s (key ip), never the key's value.
Rate limit rules
An edge rule of kind rate_limit counts requests per IP and per calendar minute on a path_pattern (empty or * = every path, ending in * = prefix, otherwise exact) and optional method. A client may make rpm + burst requests per minute; the next ones get 429 with the block page. rpm: 0 blocks every matching request.
When several rules match, a rule with a specific path wins over a catch-all one.
Domain → Security → Firewall → Rules → Rate limits → Add rate limit.
openwaf-cloud ctl rules add example.com kind=rate_limit path_pattern='/api/*' method=POST rpm=120 burst=20 --reason "API writes"curl -sS -X POST "https://dash.openwaf.dev/v1/orgs/$ORG/zones/example.com/rules" \
-H "Authorization: Bearer $OPENWAF_TOKEN" \
-H "X-Change-Reason: API writes" \
-H "Content-Type: application/json" \
-d '{"kind": "rate_limit", "path_pattern": "/api/*", "method": "POST", "rpm": 120, "burst": 20}'add_rule with "kind": "rate_limit", path_pattern, method, rpm and burst.
Basic rate limit
rate_limit_rpm caps the requests per minute from one IP to one name (IPv6 by /64). 0 (the default) turns it off. It is off while the WAF is off (mode=off or waf_level=off). The answer is 429 with the block page.
openwaf-cloud ctl zones set example.com rate_limit_rpm=600 --reason "Site-wide ceiling"In the dashboard: Security → Firewall → Protection → Basic rate limit (requests per minute per IP).