Skip to content

VOIP-1302-Redis-customer-rate-limit - #1179

Merged
pchero merged 4 commits into
mainfrom
VOIP-1302-Redis-customer-rate-limit
Aug 5, 2026
Merged

VOIP-1302-Redis-customer-rate-limit#1179
pchero merged 4 commits into
mainfrom
VOIP-1302-Redis-customer-rate-limit

Conversation

@pchero

@pchero pchero commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Add Redis-backed per-customer rate limit quotas alongside the existing per-IP
tiers, keyed on customer_id after JWT/accesskey authentication, with
Retry-After support and updated RST/service docs. Design finalized through
6 rounds of independent architecture review and 3 rounds of code review
(general + security), all APPROVE, no CRITICAL/HIGH findings.

  • bin-api-manager: Add pkg/ratelimithandler, a thin wrapper around
    go-redis/redis_rate/v9 (GCRA) behind a local RateLimiter interface with a
    mockgen mock and a miniredis integration test
  • bin-api-manager: Add CustomerRateLimit() middleware with tiers
    v1_customer (agent/accesskey, shared bucket), v1_customer_direct, and
    v1_customer_delegate, keyed ratelimit:{tier}:{customer_id}; skips on nil
    CustomerID; fails open with a 50ms Redis timeout
  • bin-api-manager: Split Authenticate() into Authenticate() +
    EnforceAccountStatus() so CustomerRateLimit can run before the frozen-
    account RPC check on the v1 group only; authProtected keeps its original
    Authenticate()->EnforceAccountStatus() order unchanged (regression-tested
    against frozen accounts calling POST /auth/delegate)
  • bin-api-manager: Add Retry-After to the existing IP-based RateLimit
    middleware via Reserve()+immediate Cancel() so rejected requests don't
    consume extra bucket budget; add details[0].limited_by ("ip"/"customer")
    to the 429 error envelope
  • bin-api-manager: Add nil-CustomerID canary logging to the auth identity
    constructors (models/auth/auth.go), no signature changes
  • bin-api-manager: Add RATE_LIMIT_CUSTOMER_V1_{,DIRECT_,DELEGATE_}RPS/BURST
    and RATE_LIMIT_CUSTOMER_REDIS_TIMEOUT_MS config, defaulting to enforce
    mode with the ticket's example values (16.7/33, 50/100, 8.3/16); rps<=0
    disables a tier, matching the existing IP-tier safe-rollback convention
  • bin-api-manager: Wire a dedicated rate-limiter Redis client (separate
    from cachehandler) in runDaemon with a deferred Close()
  • bin-api-manager: Reuse the existing rate-limit Prometheus counters with
    new tier labels; add a per-customer, per-minute sampled WARN logger for
    repeated rejections, reusing ratelimit.go's cleanup() goroutine pattern
  • bin-api-manager: Update docsdev/source/architecture_security.rst,
    restful_api_errors.rst, common_overview.rst and docs/architecture.md,
    docs/operations.md; clean RST rebuild included

Operational note: confirmed via kubectl against the production Redis
instance (infrastructure/redis-6466f86c48-dkqf4) that maxmemory-policy is
allkeys-lru with maxmemory 256MB, currently ~1.7% utilized. Under this
policy, rate-limit keys can be evicted under memory pressure alongside
cache keys, since this Redis instance is shared between caching and rate
limiting. Per design decision, this is absorbed into the already-accepted
fail-open risk rather than requiring a policy change or dedicated
instance; worth revisiting if cache memory pressure grows materially.

pchero added 4 commits August 5, 2026 14:21
- bin-api-manager: Add pkg/ratelimithandler wrapping redis_rate/v9 behind a local RateLimiter interface
- bin-api-manager: Add mockgen-generated mock and miniredis-backed integration test
- bin-api-manager: Add redis_rate and miniredis dependencies to go.mod/go.sum
- bin-api-manager: Split middleware.Authenticate() into Authenticate() + EnforceAccountStatus() so v1 route group can rate-limit before the frozen-account RPC check
- bin-api-manager: Add middleware.CustomerRateLimit() enforcing per-customer GCRA quotas (tiers v1_customer, v1_customer_direct, v1_customer_delegate) keyed by customer_id, applied only to the v1 route group
- bin-api-manager: Add Retry-After header and details[0].limited_by to both the IP-based and customer-based 429 responses, using Reserve()+Cancel() on the IP limiter to avoid double-consuming budget on rejection
- bin-api-manager: Add nil-CustomerID warning logs to the AuthIdentity constructors in models/auth/auth.go (log only, no behavior/signature change)
- bin-api-manager: Add RATE_LIMIT_CUSTOMER_V1_*/_DIRECT_*/_DELEGATE_*/_REDIS_TIMEOUT_MS config fields with VOIP-1302 initial values
- bin-api-manager: Wire a dedicated rate-limiter Redis client in cmd/api-manager/main.go (runDaemon), separate from pkg/cachehandler, with its own Close() lifecycle
- bin-api-manager: Update docsdev/source/architecture_security.rst rate limiting table with v1_customer/v1_customer_direct/v1_customer_delegate tiers, remove stale VOIP-1302-not-implemented footnote
- bin-api-manager: Update docsdev/source/restful_api_errors.rst RATE_LIMIT_EXCEEDED entry with Retry-After header and details[0].limited_by field
- bin-api-manager: Update docsdev/source/common_overview.rst customer-facing rate limit section to describe the per-customer global quota and Retry-After handling
- bin-api-manager: Update docs/architecture.md middleware stack section for the Authenticate()/EnforceAccountStatus() split and CustomerRateLimit() ordering
- bin-api-manager: Update docs/operations.md config table with the new RATE_LIMIT_CUSTOMER_* env vars
- bin-api-manager: Clean rebuild docsdev/build/ HTML output
- bin-api-manager: math.Round(rps) could produce a GCRA Rate of 0 for
  0 < rps < 0.5, sending redis_rate's Lua script into an undefined
  division-by-zero state instead of a well-defined restrictive limit.
  Clamp the rounded rate to a minimum of 1. Found in code review
  round 1 (MEDIUM).
@pchero
pchero merged commit fdf2f76 into main Aug 5, 2026
2 of 3 checks passed
@pchero
pchero deleted the VOIP-1302-Redis-customer-rate-limit branch August 5, 2026 06:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant