> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dnzwithazee.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limits

> See who hit a route's rate limit, where and when, set limits per route with a suggested number, and let DNZ Proxy turn Under attack on by itself during a flood.

A route's **Rate limit** (requests a second per visitor, with a burst) and an **in-flight limit** (requests in
progress at once per visitor) answer `429` when one visitor sends too much. A [DNZ Proxy](https://docs.dnzwithazee.com/proxy/dnz-proxy.md) records
every refusal, so **Proxy → Rate limits** shows who hit which limit, from where and on which route, as the
[WAF](https://docs.dnzwithazee.com/proxy/waf.md) page does for attacks.

## See what was refused

Open **Proxy → Rate limits**. Pick a proxy (or all of them) and a span.

* **Totals**: refused by a rate limit, by an in-flight limit, the addresses, and addresses banned for hitting limits.
* **Over time**, the **map**, the **busiest limits** (with their setting, like `rate 5/s, burst 10`, and their
  routes), **addresses**, **paths**, **countries** and **hostnames**.
* **Refused requests**: one row per request, newest first. Filter by address, hostname, limit, rate or in-flight,
  or a path. Open a row for the user agent, referer, node and request ID.
* **Banned for limits**, with **Unban**.

**Block** and **Ban** work as on the WAF page. Before you use them, look for two marks:

* **Shared address**: a mobile carrier's network, or several browsers from one address. Many people may be
  behind it, and a block would refuse them all.
* **Forwarder warning**: most refusals come from a private address, one of your nodes, or a hosting or CDN
  network. The limit is probably counting the load balancer or CDN in front of DNZ Proxy, not your visitors. Add
  it to the entry point's **Trusted forwarders**, and the limit sees each visitor again.

**Proxy → Analytics** shows the same numbers beside the WAF section, and its request log has a **Rate limited**
filter. On the home page, **Overview → Proxy** says how many of the blocked requests a rate limit refused.

> [!NOTE]
>
> Blocked numbers go up after you upgrade wherever rate limits already refuse traffic: before, those refusals
> weren't counted anywhere.

## Set limits on routes

The **Settings** tab lists every route with its limits and what each refused in the last 24 hours.

* **Edit** or **Add** opens the route dialog.
* **Suggest** proposes a rate and burst from the busiest address on the route in any ten minutes of the last day:
  three times its average then. It is an average over ten minutes, not a peak.
* **Add a limit to routes**: pick routes and set one limit on all of them. Each route's history keeps a version.

Starting points: a login or password reset, a few requests a minute per address. A whole site, some tens a second,
with a burst of twice that.

In the route dialog, a rate limit on a DNZ Proxy also has:

* **Log only**: refuse nothing, and count and list what the limit would refuse. Try a number first.
* **Never limit these addresses**: your monitoring, your office, partners.
* **Count per**: what makes one visitor. See [What counts as one visitor](#what-counts-as-one-visitor).

```bash Terminal theme={null}
dnz proxy routes edit edge shop.example.com --rate-limit 10/20 --rate-limit-log-only on
dnz proxy limits suggest edge shop.example.com
dnz proxy limits show --since 1h
dnz proxy limits refused --client 203.0.113.9
```

## Ban addresses that keep hitting limits

Off by default. On, a limit's refusals count towards the proxy's **Bans that expire** (its DNZ Proxy settings)
(*N* blocks within *M* minutes), as the WAF's blocks do. Turn it on under **Proxy → Rate limits → Settings**, or
with `dnz proxy settings edge --ban-rate-limited on`.

Leave it off when a limit is keyed by a header, or when many people share one address (an office behind NAT):
it would ban real people.

## Soft and hard bans

A banned address is refused one of two ways. Pick it under **Proxy → Rate limits → Settings → Bans**, or in the proxy's
**Bans that expire**:

| | What the visitor gets | Use it when |
| - | - | - |
| **Soft ban** (default) | The *Access blocked* page with a request ID | Someone may be banned by mistake and should see why |
| **Hard during an attack** | The page on a quiet day; a closed connection while an attack or traffic spike is open | You want both |
| **Hard ban** | The connection is closed before TLS: no page, no request ID | An attacker should cost the proxy nothing |

A ban you make by hand (the block dialog on the WAF page or this page) is soft or hard by itself. Trusted forwarders
(a CDN, a load balancer) and private addresses are never hard-banned: they get the page.

### Drop hard bans in the kernel firewall

Off by default. On, each node also drops hard-banned addresses in its own nftables table, so their packets never reach
the proxy. It helps only against visitors who connect directly, not those behind a CDN.

Each node needs nftables and the right to change the firewall:

```bash systemd theme={null}
sudo apt-get install -y nftables
sudo mkdir -p /etc/systemd/system/dnz-proxy.service.d
sudo tee /etc/systemd/system/dnz-proxy.service.d/kernel-bans.conf >/dev/null <<'EOF'
[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
EOF
sudo systemctl daemon-reload && sudo systemctl restart dnz-proxy
```

In Docker the node can't write the firewall itself, so a small sidecar beside it does (bridge networking works, the
release image stays as it is). Add it beside the node's service; it touches only the node's own table and removes it when
stopped. The settings page fills in your service and volume names:

```yaml Docker Compose theme={null}
  dnz-proxy-firewall:
    image: nicolaka/netshoot@sha256:7f08c4aff13ff61a35d30e30c5c1ea8396eac6ab4ce19fd02d5a4b3b5d0d09a2
    network_mode: "service:dnz-proxy"
    cap_add: [NET_ADMIN]
    volumes:
      - dnz-proxy:/data
    restart: unless-stopped
    entrypoint: ["sh", "-c"]
    command:
      - |
        f=/data/kernel-bans.nft; last=
        trap 'nft delete table inet dnz_proxy 2>/dev/null; exit 0' TERM INT
        while :; do
          if [ -f $$f ] && s=$$(sha1sum $$f) && [ "$$s" != "$$last" ]; then
            n=$$(head -1 $$f | awk '{print $$5}')
            if grep -E '^(table|delete|flush|add|insert|create)' $$f | grep -qv 'inet dnz_proxy'; then ok=no; out='refused: the file names another table'
            elif out=$$(nft -f $$f 2>&1); then ok=yes; else ok=no; fi
            if [ $$ok = yes ]; then echo "ok $$n"; else printf 'error %s\n%s\n' "$$n" "$$out"; fi > /data/.kernel-bans.status
            mv /data/.kernel-bans.status /data/kernel-bans.status; last=$$s
          fi
          sleep 2 & wait $$!
        done
```

```bash docker run theme={null}
docker run -d --name dnz-proxy-firewall --restart unless-stopped \
  --network container:dnz-proxy --cap-add NET_ADMIN -v dnz-proxy:/data \
  --entrypoint sh nicolaka/netshoot@sha256:7f08c4aff13ff61a35d30e30c5c1ea8396eac6ab4ce19fd02d5a4b3b5d0d09a2 -c "$(cat <<'EOF'
f=/data/kernel-bans.nft; last=
trap 'nft delete table inet dnz_proxy 2>/dev/null; exit 0' TERM INT
while :; do
  if [ -f $f ] && s=$(sha1sum $f) && [ "$s" != "$last" ]; then
    n=$(head -1 $f | awk '{print $5}')
    if grep -E '^(table|delete|flush|add|insert|create)' $f | grep -qv 'inet dnz_proxy'; then ok=no; out='refused: the file names another table'
    elif out=$(nft -f $f 2>&1); then ok=yes; else ok=no; fi
    if [ $ok = yes ]; then echo "ok $n"; else printf 'error %s\n%s\n' "$n" "$out"; fi > /data/.kernel-bans.status
    mv /data/.kernel-bans.status /data/kernel-bans.status; last=$s
  fi
  sleep 2 & wait $!
done
EOF
)"
```

The image is `nicolaka/netshoot` v0.14, pinned by digest (about 850 MB, pulled once). The settings page lists each node:
how many addresses it drops, or why it can't.

## Connections per address

Each HTTP or HTTPS entry point can cap how many connections one address keeps open at once. Past it, a new connection
is closed before TLS. No limit by default. Set it on the proxy's **Ports** (the entry point dialog), or
`dnz proxy entrypoints edit edge websecure --max-connections-per-ip 50`. Trusted forwarders and private addresses are never
limited; a browser opens about 6 connections per site.

## Under attack during a spike

Off by default, per proxy. On, a hostname whose traffic spikes **and** whose origin struggles (5xx answers on at
least 5% of its requests) gets the browser check (**Under attack**) until the spike is over, then it goes off by
itself. A launch or a newsletter your origin handles changes nothing.

**1. Turn it on**

**Proxy → Rate limits → Settings → Under attack during a spike**, or `dnz proxy flood set edge --on`.

**2. Mark routes that must never be checked**

A browser check answers API clients and webhooks with `403`. In the route dialog, under **Stop attacks**, switch
on **Never turn on Under attack by itself**. The settings list routes that look like an API.

**Exempt** under the setting lists the addresses and user agents that never see this check: your monitoring, your office,
partners. DNZ's own uptime checks always go through. A route with its own **Under attack** keeps its own check and its own
exempt lists.

DNZ decides once a minute from every node's counts, so it acts one to two minutes after a flood starts. A hostname
stays checked at most 6 hours in a row. Each start and end is an incident and a notification
(**Under attack on by itself**), and shows in **What changed?**. End one early with **End** on the page, or
`dnz proxy flood end edge shop.example.com`: it stays off for that hostname for an hour.

## Alerts

Under **Settings → Notifications → Proxy alerts**:

* **Traffic spike** (on by default): requests on a hostname, blocked or not, rise sharply. The message says how
  many got a 5xx answer and whether they come from a few addresses or many.
* **Attack spike** counts what rate limits refuse as blocked, unless you switch **Count rate limits** off.

See [Notifications](https://docs.dnzwithazee.com/integrations/notifications.md).

## What it costs

A request no limit refuses costs nothing more: the counting happens only for a refused request, and the automatic
Under attack is in no route while it is off. Details in [Proxy benchmarks](https://docs.dnzwithazee.com/proxy/benchmarks.md).

## What counts as one visitor

Each address is one visitor unless you say otherwise. On a DNZ Proxy, on a full DNZ and on a
[reverse proxies only](https://docs.dnzwithazee.com/proxy/proxy-only.md) one, a limit can count per any of these, and up to four together:

| Part | One visitor is |
| - | - |
| `address` | one address |
| `cookie:NAME` | one value of that cookie; other cookies changing make no difference |
| `header:NAME` | one value of that request header |
| `query:NAME` | one value of that query parameter |
| `host` | one hostname |
| `path` | one path, without the query |
| `method` | one method |

For example, `cookie:session` with `path` gives each signed-in person a budget per page, and `header:X-Api-Key` gives
each API key its own.

Use **Count per** in the route dialog, **Or count per these together** on a rate limit or in-flight middleware, or the
terminal:

```bash Terminal theme={null}
dnz proxy routes edit edge shop.example.com --rate-limit 10/20 --rate-limit-key cookie:session --rate-limit-key path
```

* **A request without the cookie, header or parameter counts as its address**, for that part only. Visitors without
  a cookie never share one budget.
* **Visitors choose what they send**, and each new value is a new budget. To stop an attacker, keep a limit per address
  as well, with a higher number. A cookie or header is for treating people behind one address fairly.
* With several nodes, each counts for itself unless the limit shares **Redis**.
* A ban for hitting limits still bans the address.

## Traefik, Caddy and Nginx

They don't record refusals for this page. **Log only** and **Never limit these addresses** are DNZ Proxy only: on
Traefik and Nginx the limit holds for everyone, and the proxy's page says so. Caddy has no rate limit built in, so
the limit is left out there, with a warning.

**Count per**: Traefik takes one request header or the hostname. A request without that header doesn't fall back to
its address there. Anything else counts per address on Traefik and Nginx, with a warning.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.