# Abuse Controls

Limits that keep one caller, script or bug from exhausting the service or its
budget. Covers OWASP API4 (unrestricted resource consumption) and API6
(unrestricted access to sensitive business flows).

## Contents

1. Layers of limits
2. Choosing a rate limit algorithm
3. Keys: what to limit by
4. A shared-store limiter
5. Telling clients about limits
6. Size, time and cost limits
7. Protecting business flows
8. Operating the limits

## 1. Layers of limits

| Layer | Stops | Tooling |
|---|---|---|
| Edge / CDN | Volumetric floods, obvious bots | Cloudflare, AWS WAF + Shield, Fastly |
| API gateway | Per-key quotas, plan tiers | Kong, Envoy, AWS API Gateway usage plans |
| Application | Per-user and per-route rules that need business context | Middleware backed by Redis or the database |
| Downstream | Spend on paid providers | Budget counters, provider-side caps |

Do not rely on one layer. The edge cannot tell which account a request belongs
to; the application cannot absorb a flood.

## 2. Choosing a rate limit algorithm

- **Fixed window** (N per minute, reset on the minute): simplest, allows a burst
  of 2N at window edges. Fine for coarse quotas.
- **Sliding window** (weighted count across the current and previous window):
  smooth, cheap, the usual choice for API limits.
- **Token bucket** (refill rate R, capacity B): allows short bursts up to B while
  enforcing an average of R. Good for interactive clients.
- **Concurrency limit** (at most K running at once): protects slow endpoints such as
  exports and report generation, where request rate is the wrong measure.

## 3. Keys: what to limit by

- Authenticated routes: the account or API key id, possibly per route group.
- Credential routes: the submitted identifier (email, username) **and** the
  source, so neither a spread-out attack on one account nor one source hitting
  many accounts gets through.
- Anonymous routes: client IP as seen by your trusted proxy. Configure the
  framework's trusted proxy list; a naively read `X-Forwarded-For` lets anyone
  pick their own key.
- IPv6: limit by /64 prefix, since one host can own a whole block.

## 4. A shared-store limiter

In-memory counters break as soon as there are two instances. Use Redis (or
Valkey) with an atomic script. A sliding-window sketch:

```lua
-- KEYS[1] = current window key, KEYS[2] = previous window key
-- ARGV[1] = limit, ARGV[2] = window seconds, ARGV[3] = elapsed seconds in current window
local current = tonumber(redis.call("GET", KEYS[1]) or "0")
local previous = tonumber(redis.call("GET", KEYS[2]) or "0")
local window = tonumber(ARGV[2])
local weight = (window - tonumber(ARGV[3])) / window
local estimated = previous * weight + current
if estimated >= tonumber(ARGV[1]) then
  return 0
end
redis.call("INCR", KEYS[1])
redis.call("EXPIRE", KEYS[1], window * 2)
return 1
```

In practice use a maintained implementation: `@fastify/rate-limit` or
`rate-limiter-flexible` on Node, `slowapi` or `limits` on Python, or the
gateway's built-in plugin. Decide explicitly what happens when Redis is down:
fail open for general traffic, fail closed for credential endpoints.

## 5. Telling clients about limits

Return `429 Too Many Requests` with a problem-details body and a `Retry-After`
header in seconds. Advertise the policy so well-behaved clients can pace
themselves; the IETF `RateLimit` and `RateLimit-Policy` header fields are the
emerging standard, and many APIs still send `X-RateLimit-Limit`,
`X-RateLimit-Remaining` and `X-RateLimit-Reset`. Pick one convention and
document it.

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 20
Content-Type: application/problem+json

{ "type": "https://api.example.com/problems/rate-limited", "title": "Too many requests", "status": 429 }
```

## 6. Size, time and cost limits

Rate limits count requests; these bound what a single request can cost.

- **Body size:** reject at the proxy (`client_max_body_size`) and in the
  framework (`bodyLimit`, `max_request_size`). JSON APIs rarely need more than
  1 MB; uploads get their own route and limit.
- **Collections:** cap array lengths in schemas and `limit` on list endpoints.
- **Time:** server-side request timeout, statement timeout on the database
  (`SET statement_timeout`), and deadlines on outbound calls.
- **GraphQL:** maximum depth, maximum aliases, a query cost budget per request
  and per minute, and persisted queries in production.
- **Regular expressions:** avoid catastrophic backtracking on user input; use
  linear-time engines (RE2) or bounded patterns.
- **Compression:** limit decompressed size to stop decompression bombs.
- **Paid side effects:** per-account daily caps on SMS, email, LLM tokens and
  payment attempts, with alerts well before the provider bill does.

## 7. Protecting business flows

Some abuse uses perfectly valid, correctly rate-limited requests. Identify the
flows where volume itself is the harm:

- buying limited-stock products or reserving limited slots;
- creating accounts, redeeming promo codes or referral rewards;
- posting reviews, comments or messages to other users;
- enumerating public identifiers (usernames, order numbers, gift card codes).

Controls, cheapest first:

1. Per-account and per-device limits specific to that flow (one redemption per
   account, five sign-ups per device per day).
2. Make identifiers unguessable and responses uniform so enumeration yields
   nothing.
3. Friction at the decisive step: email or phone verification, CAPTCHA or
   privacy-preserving attestation (Turnstile, App Attest, Play Integrity).
4. Velocity and anomaly scoring with a review queue for borderline cases.

## 8. Operating the limits

- Log every 429 with the key and route; dashboard the top limited keys.
- Start new limits in report-only mode, read the data, then enforce.
- Give trusted partners explicit higher tiers instead of exempting them silently.
- Load test with the limits on, so capacity planning reflects production
  behaviour.
- Review limits after incidents and when pricing plans change.
