Skip to content

Enforce each API key's rateLimitPerMinute in the gateway limiter instead of only labelling metrics with it #1505

Description

@greatest0fallt1me

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) accepts rateLimitPerMinute: z.number().int().positive().nullable().optional().default(null).
    • src/repositories/apiKeyRepository.ts persists it (lines 146, 176).
    • src/middleware/gatewayApiKeyAuth.ts (line 423) loads it.
  • Only used as a label. grep -rn rateLimitPerMinute src finds no limiter. Its only runtime use is advertisedLimitPerMinute in a metrics call in src/routes/proxyRoutes.ts (line 423).
  • Shared user bucket. src/middleware/gatewayRateLimit.ts, getGatewayRateLimitKey (about lines 136-145), returns user:${apiKeyRecord.userId}, so all of a user's keys share one bucket.
  • Prototype lookup. src/services/rateLimiter.ts, resolvePolicy (about lines 444-450), uses if (!tier || !(tier in this.tierPolicies)). The in operator walks the prototype chain, so a plan_tier of toString or constructor returns a function as the policy.

Scope

In scope

  1. Two-level limiting. In the gateway rate-limit middleware, after the user or tier bucket check, check a key bucket key:<apiKeyId> with limit min(rateLimitPerMinute, tierLimit) per 60s when rateLimitPerMinute is set. Reject with 429 and RateLimit-* / Retry-After headers that reflect the stricter bucket.
  2. Store reuse. Use the existing RateLimiterStore abstraction (InMemoryRateLimiterStore and the Postgres store) so per-key limits work in multi-instance mode.
  3. Safe tier lookup. Use Object.prototype.hasOwnProperty.call(this.tierPolicies, tier), or a Map. Validate plan_tier against the known set when keys are created (src/routes/apiKeyRoutes.ts).
  4. API visibility. Return the effective limit in GET /api/developers/me/keys and in the key creation response.
  5. Docs. Update docs/tiered-rate-limits.md and docs/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 build and npm test pass, and CI is green.
  • At least 90% line coverage on the touched limiter code.
  • OpenAPI updated and npm run validate:openapi passes.

Getting started

git checkout -b feat/per-key-rate-limit
npm ci && npx prisma generate
npm run test:unit -- src/middleware/gatewayRateLimit src/services/rateLimiter

Read 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

  1. added
    Stellar WaveStellar Wave Program issue
    backendBackend service work
    apiAPI endpoint/contract work
    securitySecurity hardening
    priority:mediumMedium implementation difficulty
    on Oct 18, 2026
  2. EmannuellaO commented on Oct 19, 2026

    @EmannuellaO
    Contributor

    @EmannuellaO has applied to work on this issue as part of the Stellar Wave Program's 10th wave.

    Hello Maintainer, thank you for assigning this issue to me to solve. After reviewing the issue, I am confident to deliver the best solution.

    ℹ️ Repo Maintainers: To accept this application, review their application or assign @EmannuellaO to this issue.

  3. iyanukid2008-pixel commented on Oct 19, 2026

    @iyanukid2008-pixel

    @iyanukid2008-pixel has applied to work on this issue as part of the Stellar Wave Program's 10th wave.

    I’ve gone through the issue in detail and have a clear understanding of the expected outcome. I’d like to take this on and approach the implementation with a focus on correctness, maintainability, and consistency with the current codebase.

    ℹ️ Repo Maintainers: To accept this application, review their application or assign @iyanukid2008-pixel to this issue.

  4. drips-wave commented on Oct 19, 2026

    @drips-wave

    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 🌊

  5. drips-wave commented on Oct 24, 2026

    @drips-wave

    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.

  6. added a commit that references this issue on Oct 24, 2026
    eacc5ca
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Stellar WaveStellar Wave Program issueapiAPI endpoint/contract workbackendBackend service workpriority:mediumMedium implementation difficultysecuritySecurity hardening

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions