Repository navigation
Enforce each API key's rateLimitPerMinute in the gateway limiter instead of only labelling metrics with it #1505
Description
Summary
Developers can set rateLimitPerMinute when they create an API key, and the value is stored and loaded with the key. Nothing enforces it. The gateway limiter buckets every key of a user together under user:<id>, and the tier limiter applies fixed per-tier policies. The per-key value is only copied into a metrics label. Separately, resolvePolicy uses tier in this.tierPolicies, so a tier string such as "constructor" resolves to an object prototype member, giving NaN limits that allow everything. This issue enforces per-key limits as a ceiling within the tier policy and hardens tier resolution.
Background and evidence
- Accepted and stored.
src/routes/apiKeyRoutes.ts(line 31) acceptsrateLimitPerMinute: z.number().int().positive().nullable().optional().default(null).src/repositories/apiKeyRepository.tspersists it (lines 146, 176).src/middleware/gatewayApiKeyAuth.ts(line 423) loads it.
- Only used as a label.
grep -rn rateLimitPerMinute srcfinds no limiter. Its only runtime use isadvertisedLimitPerMinutein a metrics call insrc/routes/proxyRoutes.ts(line 423). - Shared user bucket.
src/middleware/gatewayRateLimit.ts,getGatewayRateLimitKey(about lines 136-145), returnsuser:${apiKeyRecord.userId}, so all of a user's keys share one bucket. - Prototype lookup.
src/services/rateLimiter.ts,resolvePolicy(about lines 444-450), usesif (!tier || !(tier in this.tierPolicies)). Theinoperator walks the prototype chain, so aplan_tieroftoStringorconstructorreturns a function as the policy.
Scope
In scope
- Two-level limiting. In the gateway rate-limit middleware, after the user or tier bucket check, check a key bucket
key:<apiKeyId>with limitmin(rateLimitPerMinute, tierLimit)per 60s whenrateLimitPerMinuteis set. Reject with 429 andRateLimit-*/Retry-Afterheaders that reflect the stricter bucket. - Store reuse. Use the existing
RateLimiterStoreabstraction (InMemoryRateLimiterStoreand the Postgres store) so per-key limits work in multi-instance mode. - Safe tier lookup. Use
Object.prototype.hasOwnProperty.call(this.tierPolicies, tier), or aMap. Validateplan_tieragainst the known set when keys are created (src/routes/apiKeyRoutes.ts). - API visibility. Return the effective limit in
GET /api/developers/me/keysand in the key creation response. - Docs. Update
docs/tiered-rate-limits.mdanddocs/gateway-rate-limiting.md, and the OpenAPI schemas.
Out of scope
- Mounting the gateway proxy routers.
- Billing for rejected calls.
Design constraints
- Keys only narrow. A per-key limit can only lower the effective limit, never raise it above the tier.
- Truthful headers.
RateLimit-*headers describe whichever bucket is closest to exhaustion. - Hashed bucket keys. Bucket keys use the key id, never the raw key.
Test plan
Extend src/middleware/gatewayRateLimit.test.ts and src/services/rateLimiter.test.ts.
| # | Scenario | Expected |
|---|---|---|
| 1 | Key limit 5/min, tier 100/min | 6th call within a minute gets 429 |
| 2 | Two keys of one user, key A limited to 2 | A gets 429 on its 3rd call; B unaffected (until the user bucket) |
| 3 | Key limit above the tier | the tier limit applies |
| 4 | Null key limit | tier behaviour unchanged |
| 5 | plan_tier = "constructor" |
default policy, not NaN; finite headers |
| 6 | Creating a key with an unknown tier | 400 |
| 7 | Headers on a 429 | reflect the key bucket's remaining and reset |
| 8 | Postgres store | per-key bucket persisted under the key id |
Regression proof. Rows 1 and 5 allow unlimited calls on current main.
Acceptance criteria
- Every test-plan row passes.
-
npm run lint,npm run typecheck,npm run buildandnpm testpass, and CI is green. - At least 90% line coverage on the touched limiter code.
- OpenAPI updated and
npm run validate:openapipasses.
Getting started
git checkout -b feat/per-key-rate-limit
npm ci && npx prisma generate
npm run test:unit -- src/middleware/gatewayRateLimit src/services/rateLimiterRead first: src/middleware/gatewayRateLimit.ts, resolvePolicy and the stores in src/services/rateLimiter.ts, and createApiKeyBodySchema in src/routes/apiKeyRoutes.ts.
PR requirements
- Link this issue (
Closes #<n>). - Explain how headers are chosen when two buckets apply.
- Paste test output and coverage.
- Example commit:
feat(gateway): enforce per-key rateLimitPerMinute under the tier ceiling.
Priority rationale
Medium. It is contained to the limiter layer, but a documented control is currently a no-op and a malformed tier disables limiting entirely.
Activity
EmannuellaO commentedon Oct 19, 2026
@EmannuellaO has applied to work on this issue as part of the Stellar Wave Program's 10th wave.
ℹ️ Repo Maintainers: To accept this application, review their application or assign @EmannuellaO to this issue.
iyanukid2008-pixel commentedon Oct 19, 2026
@iyanukid2008-pixel has applied to work on this issue as part of the Stellar Wave Program's 10th wave.
ℹ️ Repo Maintainers: To accept this application, review their application or assign @iyanukid2008-pixel to this issue.
drips-wave commentedon Oct 19, 2026
Congratulations, @EmannuellaO! 🎉 Your application was accepted by the repo's maintainers, and the issue is due on October 22, 2026.
🧑💻 @EmannuellaO: Please resolve the issue such that the repo's maintainers have enough time to review your contribution before the due date. You'll earn Points for completing the issue on-time, which will make you eligible for a share of the Stellar Wave Program's reward pool.
Warning
When opening a PR, please link it to this issue to ensure it gets tracked accurately. Points are awarded when this issue is marked as completed by the maintainer.
🤠 Repo maintainers: Please keep an eye on the contributor's progress and review their work before the due date. You can manage this issue, including adjusting its complexity and points, here.
🌊 Happy Wave 🌊
drips-wave commentedon Oct 24, 2026
This issue has been completed by @EmannuellaO as part of the Stellar Wave Program's 10th Wave 🥳
😎 @EmannuellaO: You earned 100 Points for completing this issue! Once the Wave's rewards are finalized, you'll be eligible for a percentage of the Wave's reward pool based on the percentage of total points you've earned. Learn more here. You can also Leave a review to share your experience working on this issue.
🧑💻 Repo maintainers: How'd the contributor do? Leave a review to share your experience working with them.
feat(gateway): enforce per-key rateLimitPerMinute under the tier ceil…