# System Design: [Rate limiter name]

> Reference design for a distributed rate limiter that protects APIs from
> overload and abuse and enforces per-customer quotas. It compares token
> bucket, leaky bucket, fixed window and sliding window algorithms, specifies
> a Redis-based implementation with atomic scripts, discusses multi-region
> limits, response headers (`Retry-After` and the IETF RateLimit header fields
> draft) and the fail-open versus fail-closed decision. It follows the
> structure of the System Design Document template and is pre-filled with a
> worked design to adapt. Values marked *Example* are assumptions.

<!--
Guidance: How to use this template
- Decide first what the limiter is for: protecting capacity, enforcing a
  paid quota, or stopping abuse. Each purpose wants different accuracy and a
  different failure behaviour (section 7.6).
- Fill the policy table in 7.1 with real tiers before sizing anything; the
  number of distinct keys drives memory and the request rate drives shards.
- Keep the algorithm comparison in 7.2 even if you only use one algorithm;
  reviewers will ask why you did not pick another.
- Test the limiter's own failure (Redis unavailable) before launch.
-->

## Document control

<!-- Guidance: A design is a proposal until it is approved; record who decided and when. -->

| Field | Value |
|---|---|
| Document ID | [SDD-RL-001] |
| System / feature | [API rate limiting] |
| Version | [0.1] |
| Status | [Draft / In review / Approved / Implemented / Superseded] |
| Author(s) | [@author] |
| Reviewers | [@reviewer-1], [@reviewer-2] |
| Last updated | YYYY-MM-DD |
| Related | [API gateway design], [Pricing tiers], [ADR: fail-open policy] |

### Approvals

| Role | Name | Decision | Date |
|---|---|---|---|
| Tech lead | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| Staff / principal engineer | [@name] | [Decision] | YYYY-MM-DD |
| SRE / operations | [@name] | [Decision] | YYYY-MM-DD |
| Security | [@name] | [Decision] | YYYY-MM-DD |
| Product (quota tiers) | [@name] | [Decision] | YYYY-MM-DD |

## Revision history

<!-- Guidance: Note what changed in the design, not the wording. -->

| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft for design review |
| 0.2 | YYYY-MM-DD | [@name] | *Example:* Switched from fixed window to token bucket after burst analysis |
| 0.3 | YYYY-MM-DD | [@name] | *Example:* Added local pre-check layer to cut Redis calls for abusive keys |
| 1.0 | YYYY-MM-DD | [@name] | *Example:* Approved |

## Contents

<!-- Guidance: Keep aligned with the headings. -->

1. [Overview](#1-overview)
2. [Requirements](#2-requirements)
3. [Capacity estimation](#3-capacity-estimation)
4. [API design](#4-api-design)
5. [Data model](#5-data-model)
6. [High-level design](#6-high-level-design)
7. [Detailed design](#7-detailed-design)
8. [Scalability and performance](#8-scalability-and-performance)
9. [Reliability, security and operations](#9-reliability-security-and-operations)
10. [Trade-offs and alternatives](#10-trade-offs-and-alternatives)
11. [Delivery plan and risks](#11-delivery-plan-and-risks)
12. [Appendices](#12-appendices)

## 1. Overview

<!-- Guidance: One page that a busy reviewer can read alone: problem, proposed solution in a paragraph, and the decisions that need their attention. -->

### 1.1 Problem statement

[*Example:* Each API instance limits clients in local memory, so a client
spread across 40 instances gets 40 times its quota. A single customer's
batch job caused a 25-minute brownout last month, and paid tiers cannot be
enforced reliably.]

### 1.2 Proposed solution

[*Example:* The API gateway calls a rate-limit module on every request. The
module identifies the client (API key, user, or IP for anonymous traffic),
looks up the policy, and runs an atomic token-bucket script in a sharded Redis
cluster in the same region. Rejected requests get HTTP 429 with `Retry-After`
and RateLimit headers. A small in-process cache blocks keys that are already
exhausted without calling Redis. If Redis is unavailable, the gateway fails
open for quota limits and falls back to coarse local limits for protection.]

### 1.3 Decisions requested from reviewers

| ID | Question | Recommended option | Section |
|---|---|---|---|
| Q-01 | *Example:* Which algorithm? | Token bucket for API quotas; sliding window counter for login attempts | 7.2, 10 |
| Q-02 | *Example:* Global or per-region limits? | Per-region share of the global limit, rebalanced every 10 s | 7.5 |
| Q-03 | *Example:* Behaviour when Redis is down? | Fail open with local fallback limits; fail closed for login | 7.6 |
| Q-04 | *Example:* Where does the limiter run? | In the gateway process, not a separate network hop | 6, 10 |
| [Q-nn] | [Question] | [Option] | [Section] |

## 2. Requirements

<!-- Guidance: Functional requirements describe behaviour; non-functional requirements describe qualities with numbers. Mark what is out of scope so nobody designs for it. -->

### 2.1 Functional requirements

| ID | Requirement | Priority |
|---|---|---|
| FR-01 | *Example:* Limit requests per API key per policy (rate and burst) | Must |
| FR-02 | *Example:* Different limits per plan tier and per endpoint group | Must |
| FR-03 | *Example:* Limit anonymous traffic per IP or /64 IPv6 prefix | Must |
| FR-04 | *Example:* Return 429 with `Retry-After` and RateLimit headers | Must |
| FR-05 | *Example:* Change policies without redeploying (effective within 60 s) | Must |
| FR-06 | *Example:* Support weighted cost (an export costs 10 units) | Should |
| FR-07 | *Example:* Shadow mode: evaluate and log without rejecting | Should |
| [FR-nn] | [Requirement] | [Must / Should / Could] |

### 2.2 Non-functional requirements

| ID | Quality | Target | Measured as |
|---|---|---|---|
| NFR-01 | Added latency | *Example:* p99 at or below 2 ms per request | Gateway timing |
| NFR-02 | Accuracy | *Example:* Within 5% of the configured limit in one region; within 15% globally | Load test with synthetic clients |
| NFR-03 | Availability | *Example:* Limiter failure never makes the API unavailable | Chaos test with Redis down |
| NFR-04 | Throughput | *Example:* 1 million decisions/s at peak | Load test |
| NFR-05 | Isolation | *Example:* One abusive key cannot raise latency for others | Hot-key test |
| [NFR-nn] | [Quality] | [Target] | [Measurement] |

### 2.3 Out of scope

- [*Example:* billing for overage usage (separate metering pipeline)]
- [*Example:* volumetric DDoS protection at layers 3 and 4 (handled by the edge provider)]
- [*Example:* concurrency limits on long-running jobs]
- [Item deferred, with the release that will take it]

## 3. Capacity estimation

<!-- Guidance: Back-of-the-envelope numbers decide the shape of the design. Show inputs, formulas and results; round generously and say which way you rounded. -->

### 3.1 Assumptions

| Input | Symbol | Value | Source |
|---|---|---|---|
| Peak API requests per second (all regions) | $R$ | *Example:* 1 000 000 | Gateway metrics x growth |
| Regions | $n$ | *Example:* 3 | Deployment plan |
| Distinct active keys per hour (API keys + IPs) | $K$ | *Example:* 20 million | Gateway logs |
| Policies evaluated per request | $p$ | *Example:* 2 (key-wide and endpoint group) | 7.1 |
| Memory per bucket in Redis incl. overhead | $s$ | *Example:* 120 bytes | Measured with a hash of 2 fields |
| Script executions per Redis primary per second | $c$ | *Example:* 80 000 | Benchmark on target instance type |

### 3.2 Redis operations

Without a local cache every policy check is one script call:

$$
O = R \times p = 10^{6} \times 2 = 2 \times 10^{6} \ \text{script calls/s}
$$

The local deny cache (7.4) and batching of the two policies into one script
call reduce this to one call per request, about $10^{6}$/s globally, or per
region:

$$
O_{region} = \frac{10^{6}}{3} \approx 333\,000 \ \text{calls/s}, \qquad
N_{primaries} = \frac{O_{region}}{c} \times 1.5 = \frac{333\,000}{80\,000} \times 1.5 \approx 6.3 \Rightarrow 8 \ \text{primaries per region}
$$

The factor 1.5 keeps headroom for uneven slot distribution; 8 is chosen as
the next power of two for even resharding.

### 3.3 Memory

$$
M = K \times p \times s = 2 \times 10^{7} \times 2 \times 120 \ \text{B} = 4.8 \ \text{GB per region}
$$

Keys expire when idle (TTL equal to the time to refill the bucket plus one
second), so memory follows active keys, not all keys ever seen. With one
replica per primary the cluster holds about 9.6 GB per region.

### 3.4 Latency

A same-zone Redis round trip is typically 0.2 to 0.5 ms; the script itself
runs in microseconds. With a 2 ms budget there is room for one round trip and
a timeout of 5 ms after which the request falls back (7.6).

### 3.5 Summary

| Quantity | Estimate | Design consequence |
|---|---|---|
| Decisions | 1 million/s globally | One script call per request; local deny cache |
| Redis primaries | 8 per region | Redis Cluster, hash slot by limiter key |
| Memory | ~5 GB per region (~10 GB with replicas) | Small instances; TTL on all keys |
| Latency | Under 1 ms typical | Same-zone placement; strict timeouts |

## 4. API design

<!-- Guidance: Define the contract first: resources, methods, request and response shapes, errors, idempotency, pagination, rate limits and versioning. Use RFC 9457 problem details for errors. -->

### 4.1 Interfaces

The limiter is an in-process library in the gateway plus an admin API for
policies.

| Interface | Operation | Purpose |
|---|---|---|
| Library | `check(identity, route, cost) -> Decision` | Decide allow or reject; returns remaining, reset and retry values |
| Admin API | `PUT /v1/policies/{id}` | Create or update a policy (versioned) |
| Admin API | `GET /v1/policies?tier=` | List policies |
| Admin API | `POST /v1/overrides` | Temporary per-key override with expiry |
| Admin API | `GET /v1/keys/{key}/usage` | Current bucket state for support |

### 4.2 Response headers

Every response carries the current quota state; a rejected request also gets
`Retry-After` (RFC 9110) with status 429 (RFC 6585). The RateLimit fields
below follow the structured-field syntax of the IETF httpapi working group
draft; the syntax has changed between draft revisions, so pin the revision
you implement and say so in your API documentation.

```http
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 3
RateLimit-Policy: "burst";q=100;w=1, "daily";q=50000;w=86400
RateLimit: "burst";r=0;t=3

{"type": "https://api.example.com/problems/rate-limited", "title": "Rate limit exceeded", "status": 429, "detail": "Policy 'burst' allows 100 requests per second for this key.", "instance": "/v1/orders"}
```

### 4.3 Policy format

Policies are versioned JSON documents managed through the admin API; a new
version starts in `shadow` mode unless the reviewer sets `enforce`:

```json
{"id": "pro-default", "version": 7, "match": {"tier": "pro", "routes": ["/v1/*"]}, "algorithm": "token_bucket", "rate_per_sec": 100, "burst": 200, "cost": {"POST /v1/exports": 10}, "on_store_failure": "open", "mode": "enforce"}
```

## 5. Data model

<!-- Guidance: Choose storage by access pattern, not by habit. Show entities, keys, partitioning and the queries each store must answer. -->

### 5.1 Entities

Policies and overrides are configuration; buckets are short-lived counters.

```mermaid
erDiagram
  TIER {
    string tier_id PK
    string name
  }
  POLICY {
    string policy_id PK
    int version PK
    string tier_id FK
    string route_pattern
    string algorithm
    int rate_per_sec
    int burst
    string on_store_failure
    string mode
  }
  OVERRIDE {
    string override_id PK
    string subject_key
    string policy_id FK
    int rate_per_sec
    datetime expires_at
  }
  BUCKET {
    string limiter_key PK
    float tokens
    bigint updated_ms
    int ttl_ms
  }
  TIER ||--o{ POLICY : defines
  POLICY ||--o{ OVERRIDE : "overridden by"
  POLICY ||--o{ BUCKET : "evaluated in"
```

### 5.2 Storage choices

| Data | Store | Key | Main queries | Why |
|---|---|---|---|---|
| Buckets | *Example:* Redis Cluster per region | `rl:{policy_id}:{subject}` with hash tag on subject | Atomic read-modify-write | Sub-millisecond, scripts run atomically |
| Policies, overrides | Relational database, pushed to gateways | `policy_id`, `version` | All active policies | Small, audited, versioned |
| Decisions log | Sampled to the event pipeline | Time | Analysis, shadow mode results | Never on the request path |

Keys use a hash tag on the subject (`rl:{key_8f2a}:burst`) so all policies for
one subject land on the same slot and can be checked in one script call.

## 6. High-level design

<!-- Guidance: One diagram that shows every component on the request paths and where state lives. Name components by responsibility. -->

The decision is made inside the gateway; only the bucket state is remote.

```mermaid
flowchart LR
  client([Client]) --> edge[Edge / CDN]
  edge --> gw[API gateway]
  subgraph gwproc["Gateway process"]
    id[Identify subject] --> pol[Policy cache]
    pol --> deny[Local deny cache]
    deny --> lim[Limiter client]
  end
  gw --> id
  lim --> redis[(Redis Cluster, regional)]
  lim -->|allow| svc[Backend services]
  lim -->|reject 429| client
  admin[Policy admin API] --> cfgdb[(Policy DB)]
  cfgdb --> push[Config distributor]
  push --> pol
  lim --> logs[[Sampled decision events]]
```

| Component | Responsibility | Scales by |
|---|---|---|
| Identify subject | Derive the limiter subject from API key, user or IP prefix | With gateway |
| Policy cache | Hold active policies in memory; refresh on push or every 30 s | With gateway |
| Local deny cache | Remember keys rejected until their retry time | With gateway |
| Limiter client | Run atomic script, enforce timeout, apply fallback | With gateway |
| Redis Cluster | Store buckets | Primaries per region |
| Config distributor | Push policy versions to gateways | Single service, cached |

## 7. Detailed design

<!-- Guidance: Go deep on the parts that are new, risky or contested. Show the sequence of calls and the failure behaviour of each step. -->

### 7.1 Policies

| Tier | Policy | Rate | Burst | Window behaviour |
|---|---|---|---|---|
| *Example:* Free | Per key, all routes | 10/s | 20 | Token bucket |
| *Example:* Free | Per key, daily | 10 000/day | none | Fixed window, UTC day |
| *Example:* Pro | Per key, all routes | 100/s | 200 | Token bucket |
| *Example:* Anonymous | Per IP or /64 prefix | 5/s | 10 | Token bucket |
| *Example:* Login | Per account and per IP | 5/15 min | none | Sliding window counter, fail closed |

### 7.2 Algorithms compared

| Algorithm | State per key | Bursts | Accuracy | Best for |
|---|---|---|---|---|
| Token bucket | Tokens + last refill time | Allows up to bucket size | Exact for rate and burst | API quotas with bursty clients |
| Leaky bucket (queue) | Queue or level + time | Smooths to constant output | Exact output rate | Shaping traffic to a fragile backend |
| Fixed window counter | One counter per window | Up to 2x at window edges | Low at edges | Daily or monthly quotas |
| Sliding window log | Timestamp per request | None beyond limit | Exact | Low limits (login), small volumes |
| Sliding window counter | Two counters | Small, approximated | Good (assumes even spread in previous window) | High-volume per-minute limits |

Token bucket, with refill rate $r$ tokens per second and capacity $b$:

$$
tokens_{now} = \min\left(b,\ tokens_{prev} + r \cdot \Delta t\right), \qquad \text{allow if } tokens_{now} \ge cost
$$

Sliding window counter estimate for window length $w$ and elapsed time $e$
in the current window:

$$
\hat{n} = n_{curr} + n_{prev} \times \left(1 - \frac{e}{w}\right)
$$

For example with $w = 60$ s, $e = 15$ s, $n_{prev} = 80$, $n_{curr} = 30$:
$\hat{n} = 30 + 80 \times 0.75 = 90$, so a limit of 100 allows 10 more.

### 7.3 Atomic Redis script

Read, refill, decide and write must be one atomic step, otherwise two gateways
can both read 1 token and both allow. The script uses Redis server time so
gateway clock skew cannot mint tokens.

```lua
-- KEYS[1] = bucket key; ARGV = rate_per_sec, burst, cost
local rate  = tonumber(ARGV[1])
local burst = tonumber(ARGV[2])
local cost  = tonumber(ARGV[3])
local t = redis.call('TIME')
local now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)
local data = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(data[1]) or burst
local ts = tonumber(data[2]) or now
tokens = math.min(burst, tokens + math.max(0, now - ts) / 1000 * rate)
local allowed, retry_ms = 0, 0
if tokens >= cost then
  tokens = tokens - cost
  allowed = 1
else
  retry_ms = math.ceil((cost - tokens) / rate * 1000)
end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(burst / rate * 1000) + 1000)
return {allowed, math.floor(tokens), retry_ms}
```

The sequence for one request on the critical path:

```mermaid
sequenceDiagram
  participant C as Client
  participant G as Gateway
  participant L as Local deny cache
  participant R as Redis primary
  participant B as Backend
  C->>G: GET /v1/orders (API key)
  G->>G: identify subject, select policies
  G->>L: is key blocked until later?
  alt blocked locally
    L-->>G: yes, retry in 2 s
    G-->>C: 429, Retry-After 2
  else not blocked
    G->>R: EVALSHA token_bucket (timeout 5 ms)
    alt allowed
      R-->>G: allowed, remaining 57
      G->>B: forward request
      B-->>G: 200
      G-->>C: 200 + RateLimit headers
    else rejected
      R-->>G: rejected, retry 1 200 ms
      G->>L: block key for 1.2 s
      G-->>C: 429 + Retry-After 2
    end
  end
```

`Retry-After` is in whole seconds, so 1 200 ms rounds up to 2.

### 7.4 Local deny cache and hot keys

A client hammering after rejection would otherwise cost one Redis call per
request and concentrate load on one slot. The gateway remembers rejected keys
until their retry time and rejects locally. Over-admission is bounded: a key
that becomes allowed again is checked in Redis on its next request.

### 7.5 Multi-region and consistency model

- **Per region, strongly consistent**: each region's Redis is the single
  source of truth for its buckets; the script is atomic, so within a region
  the limit is exact (NFR-02).
- **Globally, approximate**: each region enforces a share of the global limit,
  initially $L_{global}/n$. Every 10 s regions publish observed usage per key
  to the others and shares are rebalanced toward where traffic is. A client
  that moves all traffic to one region may be under-served for up to one
  rebalance interval; one that spreads evenly gets the full limit.
- **Daily quotas** use fixed-window counters replicated asynchronously; the
  worst-case overshoot is the traffic served during replication lag, which is
  accepted for non-billing quotas. Billing-grade quotas are reconciled from
  metering, not from the limiter.

### 7.6 Fail-open versus fail-closed

| Policy class | On Redis error or timeout | Rationale |
|---|---|---|
| Quota (per key) | Fail open, then apply a coarse local limit of rate x 2 per gateway instance | Availability of paid API matters more than exact quotas for minutes |
| Capacity protection (global) | Local limits per instance, sized as global / instance count | Backend must still be protected |
| Security (login, OTP, password reset) | Fail closed with clear error | Brute force during an outage is worse than blocked logins |

A circuit breaker opens after 50% errors over 10 s so failing calls stop
adding latency; it probes again after 5 s.

## 8. Scalability and performance

<!-- Guidance: Show how the design meets the numbers in section 3 and where it will break first. -->

| Concern | Approach | Limit before redesign |
|---|---|---|
| Decision rate | One script call per request, sharded cluster | *Example:* ~640 000 calls/s per region with 8 primaries |
| Hot keys | Local deny cache; per-key rate itself caps calls | One allowed key at its own limit |
| Memory | TTL on every bucket | Tens of GB; well within instance sizes |
| Policy churn | Versioned push; gateways keep last good version | [Limit] |
| Many policies per request | Single script evaluates all policies for a subject | About 5 policies per request |

Latency budget for the limiter step (target p99 2 ms):

| Step | Budget |
|---|---|
| Identify subject and select policies (in memory) | 0.1 ms |
| Local deny cache lookup | 0.05 ms |
| Redis round trip and script | 1.0 ms |
| Header formatting | 0.05 ms |
| Headroom | 0.8 ms |

## 9. Reliability, security and operations

<!-- Guidance: Name the failure modes, the threats and the signals operators watch. Each mitigation should be testable. -->

### 9.1 Failure modes

| Failure | Impact | Detection | Mitigation |
|---|---|---|---|
| Redis primary failover | Buckets on that shard reset or briefly unavailable | Error rate per shard | Fail-open policy; reset buckets start full, so at most one burst extra |
| Redis cluster down | Exact limits unavailable | Circuit breaker open | Local fallback limits per 7.6 |
| Bad policy pushed | Legitimate traffic rejected | 429 rate spike by tier | Shadow mode for new policies; one-click rollback to previous version |
| Clock skew between gateways | Incorrect refill | Not applicable | Script uses Redis server time |
| Identification bypass (rotating IPs) | Anonymous limits ineffective | Unique IP counts per route | Prefix-based keys; edge bot management |
| [Failure] | [Impact] | [Signal] | [Mitigation] |

### 9.2 Security and abuse

| Threat | Control |
|---|---|
| Spoofed client IP headers | Trust `X-Forwarded-For` only from known proxies; use the edge-provided client IP |
| Key sharing to multiply quota | Limits per account in addition to per key |
| Enumeration of limits to time attacks | Login limits not exposed in headers |
| Policy tampering | Admin API requires privileged role, changes audited and reviewed |
| [Threat] | [Control] |

### 9.3 Observability

| Signal | Metric or log | Alert threshold |
|---|---|---|
| Limiter latency | p99 per gateway | Above 2 ms for 10 min |
| Rejection rate | 429 share by tier and route | Doubling vs. 7-day baseline |
| Fallback mode | Requests decided without Redis | Any for more than 1 min |
| Redis CPU | Per primary | Above 70% |
| Top limited keys | Sampled decision events | Dashboard for support |

### 9.4 Cost estimate

| Item | Unit cost | Quantity | Monthly cost |
|---|---|---|---|
| Redis primaries and replicas | [$/node-month] | *Example:* 16 per region x 3 | [$] |
| Policy service and database | [$/month] | 1 small deployment | [$] |
| Decision event sampling (1%) | [$/GB ingested] | [GB] | [$] |
| **Total** | | | **[$]** |

## 10. Trade-offs and alternatives

<!-- Guidance: For each major decision list the options, what each costs and why you chose one. Reviewers will propose the alternatives anyway; answer them here. -->

| Decision | Options | Chosen | Why | What we give up |
|---|---|---|---|---|
| Algorithm for API quotas | Fixed window; sliding log; sliding counter; token bucket | Token bucket | Explicit burst and rate, O(1) state | Less intuitive than "N per minute" for customers |
| Placement | Separate limiter service; in-gateway library; sidecar | In-gateway library | No extra network hop | Library updates require gateway deploys |
| State store | Local memory only; central database; Redis | Redis with scripts | Atomic, fast, shared across instances | Another stateful system to run |
| Global limits | Single global store; per-region shares | Per-region shares | No cross-region latency on requests | Approximate global accuracy |
| Store failure | Always fail closed; always fail open; per class | Per policy class | Balances availability and security | More configuration to review |
| [Decision] | [Options] | [Chosen] | [Why] | [Cost] |

## 11. Delivery plan and risks

<!-- Guidance: How the design reaches production safely, and what could stop it. -->

### 11.1 Milestones

| Milestone | Scope | Exit criterion | Target date |
|---|---|---|---|
| M1 | Library, Redis cluster, shadow mode in one region | Shadow decisions match expected limits within 5% | YYYY-MM-DD |
| M2 | Enforce for anonymous and free tier | No unexpected 429 complaints for 14 days | YYYY-MM-DD |
| M3 | All tiers, headers documented | Chaos test with Redis down passes | YYYY-MM-DD |
| M4 | Multi-region share rebalancing | Global accuracy within 15% in test | YYYY-MM-DD |

### 11.2 Rollout and rollback

[*Example:* Every new policy starts in shadow mode for 7 days. Enforcement is
enabled per tier with a feature flag; rollback sets the policy mode back to
shadow, which takes effect within 60 s.]

### 11.3 Risks

| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|---|---|---|---|---|---|
| R-01 | *Example:* Customers with legitimate bursts are throttled | Medium | High | Shadow data review; burst sized from p99 usage | [@name] |
| R-02 | *Example:* Limiter adds latency during Redis incidents | Medium | Medium | 5 ms timeout and circuit breaker | [@name] |
| R-03 | *Example:* RateLimit header draft changes | High | Low | Version the header behaviour; keep `Retry-After` as the stable signal | [@name] |
| [R-nn] | [Risk] | [L / M / H] | [L / M / H] | [Mitigation] | [@name] |

## 12. Appendices

<!-- Guidance: Supporting detail, glossary and open questions. -->

### 12.1 Open questions

| ID | Question | Owner | Due |
|---|---|---|---|
| OQ-01 | *Example:* Do enterprise customers get per-region dedicated limits? | [@name] | YYYY-MM-DD |
| OQ-02 | *Example:* Should GraphQL requests be costed by query complexity? | [@name] | YYYY-MM-DD |
| [OQ-nn] | [Question] | [@name] | YYYY-MM-DD |

### 12.2 Glossary

| Term | Meaning |
|---|---|
| Subject | The identity a limit applies to: API key, account, user or IP prefix |
| Burst | Maximum tokens a bucket holds, so the largest instant spike allowed |
| Fail open | Allow requests when the limiter cannot decide |
| Fail closed | Reject requests when the limiter cannot decide |
| Shadow mode | Evaluate and log decisions without enforcing them |
| [Term] | [Meaning] |

### 12.3 References

| Reference | Use |
|---|---|
| RFC 6585 Additional HTTP Status Codes | Status 429 |
| RFC 9110 HTTP Semantics | `Retry-After` field |
| IETF httpapi draft "RateLimit header fields for HTTP" | `RateLimit` and `RateLimit-Policy` fields (draft, revision to be pinned) |
| RFC 9457 Problem Details for HTTP APIs | Error body |
| Redis documentation on scripting and Redis Cluster hash tags | 7.3 and 5.2 |

## Standard and licence note

Based on common industry practice for distributed rate limiting and on the
HTTP specifications cited in 12.3; the RateLimit header fields are an IETF
draft, not a published RFC. This is a reference design to adapt, with
illustrative numbers. Original text written for this collection. Template
licence: MIT.
