Skip to content

In-memory rate limiter applies most-restrictive limit to all scope counters #101

@christianromeni

Description

@christianromeni

Problem / Motivation

The in-memory rate limiter (internal/ratelimit/rate_limiter.go, CheckRate) correctly resolves the most-restrictive limit across key, team, and org scopes — but then applies that single resolved limit to all three scope counters. This means a key with requests_per_minute: 5 inside an org with requests_per_minute: 1000 causes the org counter to be capped at 5, so every other key in that org is throttled far too aggressively and requests are rejected with 429 that should be allowed.

The Redis checker (internal/ratelimit/redis_checker.go) and the token counter do this correctly: each scope is checked against its own limit. The two checker implementations therefore have inconsistent semantics — switching from in-memory to Redis changes rate-limiting behavior.

Proposed Solution

Each scope (key, team, org) should be incremented and checked against its own configured limit independently. Most-restrictive-wins is preserved automatically: a request must pass all scopes, so the tightest limit still wins without polluting the counters of wider scopes.

Acceptance Criteria

  • CheckRate in the in-memory implementation checks each scope against its own limit, not the resolved minimum
  • In-memory and Redis checker produce identical allow/deny decisions for the same configuration and request sequence
  • Existing unit tests pass and new tests cover the multi-key-same-org scenario described below
  • Org with requests_per_minute: 1000 and two keys each with requests_per_minute: 5: after key A exhausts its 5 RPM, key B can still send up to its own 5 RPM without being rejected early

To reproduce (in-memory path, no Redis configured):

  1. Create an org with requests_per_minute: 1000 and two API keys each with requests_per_minute: 5
  2. Send 5 requests through key A (all succeed)
  3. Send requests through key B — observed: key B is rejected far earlier than its own limit of 5 because the org counter was capped at 5 instead of 1000

A fix is in progress.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:highShould be addressed soon

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions