Skip to content

fix(monitors): key rate-limit buckets by caller identity, not IP - #108

Merged
R7rainz merged 1 commit into
masterfrom
fix/identity-keyed-rate-limit
Jul 17, 2026
Merged

R7rainz merged 1 commit into
masterfrom
fix/identity-keyed-rate-limit

Conversation

@R7rainz

@R7rainz R7rainz commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Incident

Follow-up to #107, found by spam-testing it: exceeding the check-now rate limit blanked the entire dashboard — no workspaces, no monitors — until the 1-minute window rolled over and the page was refreshed.

Root cause

The Next.js server proxies all browser traffic to the API — server components, server actions, and the 5s live poll (/api/monitors/live). The backend therefore sees a single IP for every user. With the global limiter keyed by IP (100/min), one user spamming "check now" (each click = 1 POST + a revalidatePath re-render firing several GETs, on top of the live poll) burned the shared bucket, and every subsequent read 429'd. Pages fall back to [] on failed fetches, so the dashboard rendered as "no data" instead of an error.

The same collapse would hit production behind Caddy, where all users share the proxy's IP — one abusive client could 429 everyone.

Fix

Key rate-limit buckets by caller identity instead of IP:

  • bearer token → API key → IP (fallback for anonymous routes — login/signup brute-force keeps its IP keying)
  • Route-level overrides (auth, check-now) inherit the keyGenerator, so the 10/min check budget is now per-user rather than per-shared-IP
  • Global budget raised 100 → 300/min per identity, sized for a dashboard that polls live state every 5s and re-renders on every action

Verified against the running stack

The Next.js server proxies all browser traffic (server components,
actions, live polling), so every backend request arrives from a single
IP. With the global limiter keyed by IP, one user spamming "check now"
burned the shared 100/min bucket and 429'd every read — the dashboard
rendered empty because pages fall back to [] on failed fetches. The
same collapse would happen behind Caddy in production, where all users
share the proxy's IP.

Key buckets by bearer token / API key instead, falling back to IP only
for anonymous routes (login/signup brute force keeps its IP keying).
Route-level overrides (auth, check-now) inherit the keyGenerator, so
the 10/min check budget is now per-user too. Global budget raised to
300/min per identity to fit a dashboard that polls live state every 5s
and re-renders on every action.

Verified: 14 rapid check requests → 1×200 + 13×429, while workspace,
monitor, live and stats reads on the same token all stayed 200.
@R7rainz
R7rainz merged commit afa0bf9 into master Jul 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant