Rate-limit per client IP instead of one global bucket#212
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
ClientPartitionKeyhelper 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.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, soX-Forwarded-Forwas 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 defaultForwardLimit = 1reads the proxy-appended (rightmost) entry, which a client can't forge.Also adds an
X-Api-Contactresponse header inviting anyone scripting against the API to get in touch.🤖 Generated with Claude Code