P0: Add per-IP request rate limiting (#166)#185
Open
dkijania wants to merge 1 commit into
Open
Conversation
A public GraphQL endpoint with no throttle lets a single client monopolise the server and the backing Postgres. Add a global, per-client-IP rate limiter that runs on every request before GraphQL parsing, rejecting over-limit traffic with HTTP 429 as cheaply as possible. - RATE_LIMIT_MAX (requests per client per window, default 600; 0 disables) - RATE_LIMIT_WINDOW_MS (window length in ms, default 60000) The client IP is taken from X-Forwarded-For (first hop), then X-Real-IP, then the socket address, falling back to a shared `unknown` bucket so unproxied traffic is still bounded. Health checks are never throttled. The fixed-window counter is in-memory and per-instance; a shared store for exact cross-replica limits is left as deployment hardening and noted in the docs. Implemented as a custom plugin rather than @envelop/rate-limiter, whose directive model bakes limits into the schema and is per-field rather than a global per-IP bucket. Malformed env values fall back to safe defaults. Unit tests cover config parsing, the windowing/reset/prune logic with an injected clock, and prove end-to-end through Yoga that the (max+1)th request from an IP gets 429 while other clients are unaffected. Closes #166. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSuak9smCHbp4N17xjjLF6
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.
What & why
Part of the production-readiness epic (#163). Closes #166.
A public GraphQL endpoint with no throttle lets a single client monopolise the server and the backing Postgres. This adds a global, per-client-IP rate limiter that runs on every request before GraphQL parsing, rejecting over-limit traffic with HTTP 429 as cheaply as possible.
RATE_LIMIT_MAX6000disablesRATE_LIMIT_WINDOW_MS60000Design
X-Forwarded-For(first hop) →X-Real-IP→ socket address → sharedunknownbucket. Run behind a proxy that setsX-Forwarded-Forfor correct per-client identification (documented).RATE_LIMIT_MAX. A shared store (Redis) for exact cross-replica limits is left as deployment hardening and noted in the docs./healthcheck) are never throttled.Retry-After,X-RateLimit-Limit,X-RateLimit-Remaining.Why not @envelop/rate-limiter
That library is directive-based: limits are baked into the schema per field and it's per-field rather than a global per-IP bucket, plus it requires a schema/codegen change. A small custom plugin gives a true global per-IP DoS bucket that's env-tunable with no schema impact. (Discussed and chosen during implementation.)
Testing
npm run build— cleannpm run test:unit— all pass (config parsing; windowing/reset/prune with an injected clock; end-to-end through Yoga proving the (max+1)th request returns 429 and other clients are unaffected)npm run lint— cleannpx prettier --debug-check .— exit 0No new runtime dependency.
🤖 Generated with Claude Code