Skip to content

Rate-limit per client IP instead of one global bucket#212

Merged
PatrikBak merged 1 commit into
mainfrom
patrik/per-ip-rate-limiting
Jun 18, 2026
Merged

Rate-limit per client IP instead of one global bucket#212
PatrikBak merged 1 commit into
mainfrom
patrik/per-ip-rate-limiting

Conversation

@PatrikBak

Copy link
Copy Markdown
Owner

Summary

The two ASP.NET rate limiter policies were registered with AddFixedWindowLimiter, which creates a single shared bucket per policy for the whole server. The effective budget was therefore 20 search / 60 general requests per minute across all visitors combined, and excess requests sat in the queue waiting for the fixed window to roll over rather than being rejected — so under mild concurrency, site-wide search would stall.

This makes each policy partition by caller IP, so the limits apply per visitor:

  • New ClientPartitionKey helper keys on the client IP — full address for IPv4, and the /64 network prefix for IPv6 so a client can't rotate addresses within their block to evade the limit. IPv4-mapped IPv6 is unwrapped first.
  • Throttled requests now return 429 (Too Many Requests) instead of the ASP.NET default 503.

For the per-IP key to resolve to the real client behind Traefik, the forwarded-headers trust allowlist is cleared (KnownIPNetworks/KnownProxies). By default ASP.NET trusts only loopback, but the proxy reaches the API from the Docker bridge, so X-Forwarded-For was being dropped and every request keyed on Traefik's IP. This is safe because the API port is never published to the host — Traefik is the only ingress — and the default ForwardLimit = 1 reads the proxy-appended (rightmost) entry, which a client can't forge.

Also adds an X-Api-Contact response header inviting anyone scripting against the API to get in touch.

🤖 Generated with Claude Code

The two rate limiter policies were built with AddFixedWindowLimiter, which
creates a single shared bucket per policy for the whole server — so the real
budget was 20 search and 60 general requests per minute across all visitors
combined, and excess requests queued and waited for the window to roll over.

Partition each policy by caller IP so the limits apply per visitor. IPv6
clients are collapsed to their /64 prefix so they can't rotate addresses to
dodge the limit; IPv4 keys on the full address. Throttled requests now answer
429 instead of the default 503.

For the per-IP key to be the real client behind Traefik, clear the
forwarded-headers trust allowlist (default trusts loopback only, but the proxy
reaches the API from the Docker bridge). Safe because the API port is closed to
the host, so the proxy is the only ingress and ForwardLimit=1 reads the
proxy-appended entry.

Also add an X-Api-Contact response header inviting API users to get in touch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
math-comps Ready Ready Preview, Comment Jun 18, 2026 9:34pm

@PatrikBak PatrikBak enabled auto-merge June 18, 2026 21:33
@PatrikBak PatrikBak merged commit 37c2b37 into main Jun 18, 2026
5 checks passed
@PatrikBak PatrikBak deleted the patrik/per-ip-rate-limiting branch June 18, 2026 21:37
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