# Rate limiting

> Limit how many requests a path accepts per IP, header, cookie or JSON field, per name or for the whole zone, with block, challenge or log actions.

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](#per-path-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](#rate-limit-rules) (`kind=rate_limit`) | Edge rule | Per IP, per minute | Simple per-path limits. |
| [Basic rate limit](#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`.

```json
{"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

1. 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.
2. **Every** rule that matches counts the request in its own counter, in list order.
3. The **first** rule over its limit whose action is `block` or `challenge` answers. `log` rules 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 to `requests` per `period`, counted over a sliding window.
- `gcra` (steady rate): a burst of `requests`, then one more every `period / requests` seconds. 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](https://developers.openwaf.dev/waf/block-page/); other clients get `{"error":"rate_limited","retry_after":N}`. |
| `challenge` | Browser navigations get the [browser challenge](https://developers.openwaf.dev/waf/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.

> **Caution: Shared IPs**
>
> Everyone behind one Wi-Fi (an office, a gym, a school) shares a public IP. Keep per-IP limits above what your application already allows per IP, and add a second rule keyed by `body_field` `email` if you need a per-account limit.


### Templates

The dashboard offers these starting points (**Start from a template**):

```json
[{"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}]
```

```json
{"name": "General API", "path_prefix": "/api", "requests": 600, "period": 60, "action": "log"}
```

### Add a rule

**Dashboard**

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.
**CLI**

```sh
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.
**API**

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`.
```sh
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}]}'
```
**MCP**

`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](https://developers.openwaf.dev/waf/rule-reference/#rate-limiting-rule-fields).

### 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](https://developers.openwaf.dev/rules/per-name-settings/).

### Events

Each action leaves a [security event](https://developers.openwaf.dev/analytics/security-events/) 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.

**Dashboard**

Domain → **Security → Firewall → Rules → Rate limits** → **Add rate limit**.
**CLI**

```sh
openwaf-cloud ctl rules add example.com kind=rate_limit path_pattern='/api/*' method=POST rpm=120 burst=20 --reason "API writes"
```
**API**

```sh
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}'
```
**MCP**

`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.

```sh
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)**.
