Commita41e675unknown_key
fix(rate-limit): dramatic headroom — 1000/min API, 300/min git
fix(rate-limit): dramatic headroom — 1000/min API, 300/min git
User hit 429 on `git push` to gluecron.com after the prior fix. Two root
causes:
1. `/api/*` was capped at 200/min anonymous. Browser tabs + CDN-shared
IPs blew through that fast. Bumped to 1000/min. Auth rate limits
(login=20/min, register=10/min, forgot-password=5/min, magic=5/min)
are unchanged so brute-force protection is unaffected.
2. `gitRateLimit` was 60/min for `/:owner/:repo.git/*`. A single `git
push` is only 2 requests, but a developer with the same IP clicking
the operator UI in another tab burned through the bucket. Bumped to
300/min.
The `authedMultiplier` from the prior commit only fires when an
upstream middleware has already populated c.get("user"), which isn't
the case for `/api/*` (softAuth is per-route, not global). Kept the
multiplier in place — it's best-effort and harmless — but the new
anonymous base is high enough that humans never feel it.
https://claude.ai/code/session_01QFLWDxWw65DX6enMcS5Lwe2 files changed+18−7a41e675d48fa28adce78826e9201477bc155b731
2 changed files+18−7
Modifiedsrc/app.tsx+14−6View fileUnifiedSplit
@@ -160,16 +160,24 @@ app.use("*", async (c, next) => {
160160});
161161// Rate-limit API + auth endpoints.
162162//
163// `/api/*`: anonymous IPs are capped at 200/min; authenticated users get 4×
164// (800/min) so an admin clicking around the operator console doesn't
165// exhaust the anonymous budget. Three dashboard-plumbing endpoints are
166// exempt entirely so they never count against the bucket:
167// /api/version — layout polls every 15s to detect new deploys
163// `/api/*`: 1000/min per IP — generous so an admin clicking around the
164// operator console (or a CDN/proxy concentrating multiple users behind one
165// IP) doesn't hit the wall. Bot-resistant headroom comes from the auth
166// rate limits below, not this one.
167//
168// `authedMultiplier` is set but only fires when an upstream middleware has
169// already populated c.get("user") — most app.use() chains apply softAuth
170// per-route, so the multiplier is best-effort. Keep the anonymous base
171// high enough that humans never feel it.
172//
173// Skip-paths: dashboard plumbing endpoints that the layout polls on a
174// fixed cadence and that we don't want consuming any bucket:
175// /api/version — layout polls every 15s
168176// /api/notifications/count — nav bell unread-count fetcher
169177// /pwa/vapid-public-key — fetched once per push-notification opt-in
170178app.use(
171179 "/api/*",
172 rateLimit(200, 60_000, "api", {
180 rateLimit(1000, 60_000, "api", {
173181 authedMultiplier: 4,
174182 skipPaths: ["/api/version", "/api/notifications/count", "/pwa/vapid-public-key"],
175183 })
Modifiedsrc/middleware/rate-limit.ts+4−1View fileUnifiedSplit
@@ -139,5 +139,8 @@ export function clearRateLimitStore() {
139139
140140export const apiRateLimit = rateLimit(100, 60_000, "api"); // 100 req/min
141141export const authRateLimit = rateLimit(10, 60_000, "auth"); // 10 req/min (login/register)
142export const gitRateLimit = rateLimit(60, 60_000, "git"); // 60 req/min
142// 300/min for git Smart-HTTP. A clone of a busy repo fans out into many
143// info/refs + upload-pack requests; 60 was way too tight when the same IP
144// (a developer's laptop) was clicking around the UI in another tab.
145export const gitRateLimit = rateLimit(300, 60_000, "git");
143146export const searchRateLimit = rateLimit(30, 60_000, "search"); // 30 req/min
144147