A rate limit is not an authorization decision
Throttling how often a client can call an endpoint protects capacity. It does not decide whether that client is allowed to read or change a specific object.
The bug pattern
A familiar stack:
- The gateway or middleware applies a rate limit keyed by API key, IP, or user id.
- Requests that stay under the quota reach the handler.
- The handler loads
resourceIdfrom the path or body and returns or mutates the object — with noauthorize(actor, action, resource)check, or with a check that only verifies “this key is valid.”
Operators then treat a green rate-limit dashboard as proof that access is under control. Attackers who stay under the quota still enumerate ids, scrape tenants, or mutate records they should never touch.
Why rate limits fail as authz
Rate limits answer how fast, not whether.
- Quota ≠ permission. A caller can be well under their limit and still be unauthorized for the object in the request.
- Shared keys hide principals. When many services share one API key, the limiter only sees the key. Per-object policy for the real actor never runs.
- IP and user buckets are coarse. Blocking abusive IPs does not replace membership, ownership, or relationship checks on each resource.
- Soft limits encourage “try again.” Retries that eventually succeed still bypass missing authorization if the handler never denies on policy.
- Internal tools skip both. Admin CLIs and batch jobs often disable rate limits “because trusted,” then also skip object checks — compounding the gap.
Capacity controls and access controls solve different threats. Collapsing them into one layer leaves object-level holes wide open.
What to enforce
Keep rate limiting — and put authorization next to every sensitive read or write:
- Authorize after (or with) identity, before side effects. For each
resourceId, decide allow/deny from current policy and relationships, independent of remaining quota. - Fail closed when policy is missing. A request that is under the rate limit but has no matching grant must be denied, not served “because traffic looks fine.”
- Separate budgets from grants. Do not encode “this role may call 100 times/min” as a substitute for “this role may update document 42.”
- Log both dimensions. Record rate-limit hits and authorization denials separately so a quiet limiter never masks a flood of 403s — or the absence of checks.
- Apply the same rule to bulk and streaming paths. List, export, and watch endpoints need per-object or per-query authorization even when throttled.
Tiny checklist
- If you disable rate limiting in staging, do unauthorized object ids still get 403/404 — or do they return data?
- For a valid API key under quota, can you read another tenant’s resource by changing only the id?
- Do 429 responses ever appear on paths that never emit 403, suggesting throttling is the only gate?
- When membership is revoked, does the next in-quota request fail authorize, or only slow down after many calls?
- Are internal jobs that skip rate limits still required to pass the same authorize helper as public traffic?
Rate limits protect the system. Authorization protects the data. You need both — and neither one can pretend to be the other.
