Skip to content

Commit b97232c

Browse files
Merge pull request #182 from CodeForPhilly/develop
Release: v0.2.4
2 parents ae7ff3a + eccebfe commit b97232c

3 files changed

Lines changed: 15 additions & 14 deletions

File tree

‎apps/api/src/plugins/rate-limit.ts‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22
* In-memory rate-limit plugin.
33
*
44
* Enforces per-IP and per-account caps per specs/api/conventions.md#rate-limiting:
5-
* - Unauthenticated reads: 1200 req / min / IP
5+
* - Unauthenticated reads: 300 req / min / IP
66
* - Authenticated reads: 300 req / min / account
7-
* - Writes: 30 req / min / account (120 / min / IP anonymous)
8-
* - Credential endpoints: 120 req / min / IP
7+
* - Writes: 30 req / min / account (30 / min / IP anonymous)
8+
* - Credential endpoints: 20 req / min / IP
99
*
1010
* Only `/api/**` is counted. The SPA shell, its assets, and thumbnails come out
1111
* of the same process and a single page load fetches dozens of them; counting
1212
* those against the read cap is what produced a site-wide 429 storm at cutover.
1313
*
14-
* Per-IP caps are generous on purpose: production's load balancer does not yet
15-
* preserve client addresses (cfp-live-cluster #201), so every visitor shares
16-
* one "IP" until that lands.
14+
* Per-IP caps assume the first X-Forwarded-For hop is the real client — the
15+
* NodeBalancer speaks PROXY protocol to Envoy (cfp-live-cluster #203).
1716
*
1817
* Counters are reset on restart (intentional — single replica, civic scale).
1918
* Exceeded limit → RateLimitedError(retryAfterSeconds).
@@ -32,11 +31,11 @@ interface BucketEntry {
3231
const WINDOW_MS = 60_000; // 1 minute
3332

3433
export const RATE_LIMITS = {
35-
unauthenticatedReadsPerIp: 1200,
34+
unauthenticatedReadsPerIp: 300,
3635
authenticatedReadsPerAccount: 300,
3736
writesPerAccount: 30,
38-
anonymousWritesPerIp: 120,
39-
credentialPerIp: 120,
37+
anonymousWritesPerIp: 30,
38+
credentialPerIp: 20,
4039
} as const;
4140

4241
function getOrCreate(map: Map<string, BucketEntry>, key: string): BucketEntry {

‎plans/rate-limit-scope.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,6 @@ cap back down once real client IPs arrive.
5353

5454
## Follow-ups
5555

56-
- Tracked as: cfp-live-cluster #201 (PROXY protocol so per-IP is per visitor).
56+
- Tracked as: cfp-live-cluster #203 landed PROXY protocol the same night; the
57+
generous caps were then tightened to real per-visitor numbers (reads 300,
58+
credential 20, anonymous writes 30 per minute per IP).

‎specs/api/conventions.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,12 @@ Single-replica means rate-limit state is in-memory. Counters reset on restart; a
161161

162162
Only `/api/**` requests are counted. The SPA shell, its assets, and image thumbnails are served by the same process but are never rate limited — a single page load fetches dozens of them.
163163

164-
- Unauthenticated reads: 1200 requests / minute / IP
164+
- Unauthenticated reads: 300 requests / minute / IP
165165
- Authenticated reads: 300 requests / minute / account
166-
- Writes: 30 requests / minute / account (120 / minute / IP when anonymous)
167-
- Credential endpoints: 120 requests / minute / IP. These are the routes that accept or mint a credential: `POST /api/auth/login`, `/api/auth/github/start` and `/callback`, `/api/auth/link-github`, `/api/auth/password-reset/*`, and `POST /api/account-claim/by-password`. Session reads such as `GET /api/auth/me` (called on every page load) and `/api/auth/refresh` are ordinary reads.
166+
- Writes: 30 requests / minute / account (30 / minute / IP when anonymous)
167+
- Credential endpoints: 20 requests / minute / IP. These are the routes that accept or mint a credential: `POST /api/auth/login`, `/api/auth/github/start` and `/callback`, `/api/auth/link-github`, `/api/auth/password-reset/*`, and `POST /api/account-claim/by-password`. Session reads such as `GET /api/auth/me` (called on every page load) and `/api/auth/refresh` are ordinary reads.
168168

169-
The per-IP caps are deliberately generous: in production the gateway sits behind a load balancer that does not yet preserve client addresses, so every visitor currently shares one "IP" (cfp-live-cluster #201). Tighten the credential cap once that lands.
169+
Per-IP caps assume the gateway delivers the real client address in the first `X-Forwarded-For` hop (the NodeBalancer speaks PROXY protocol to Envoy — cfp-live-cluster #203).
170170

171171
Exceeded → `429 rate_limited`, `Retry-After` header in seconds.
172172

0 commit comments

Comments
 (0)