# Redis Distributed Rate-Limit Increment Design **Status:** implemented as standalone R1 **Parent:** `2026-07-26-redis-production-capability-design.md` §§19–21 ## Goal and readiness Provide three selectable, bounded distributed rate-limit algorithms: - fixed window; - sliding-window counter; - token bucket. This increment is a standalone Redis R1 provider. It does not claim R2 topology/security/failover qualification and does not implement sliding log, GCRA, leaky bucket, evaluation dedup, hierarchical all-or-nothing policies or local emergency fallback. ## Ownership - `shared-contract` owns the edge-enforcement semantic port and provider-neutral request, policy, decision and failure outcomes. Business quotas remain application use-case policy and do not use this port. - `adapter:outbound:cache-redis` owns Redis keys, atomic Lua programs, structured reply parsing, failure certainty and the provider implementation. - `app-bootstrap` owns the explicit provider/policy selection. - The existing inbound-web local limiter remains a compatibility path until a separate inbound migration. Its types do not cross into the Redis provider. The rate-limit runtime does not reuse `app.cache.redis`, the cache connection or cache fail-open decorators. Coordination has different failure and deployment semantics. ## Shared semantic contract `EdgeRateLimitPort.evaluate(RateLimitRequest)` accepts: - bounded `policyId`; - already pseudonymized/bounded `subjectDigest`; - positive request cost; - optional evaluation ID (rejected in this non-deduplicating revision); - finite caller deadline. `RateLimitPolicy` freezes policy ID/revision, one algorithm-specific parameter subtype, maximum cost, cleanup grace, maximum clock regression and `FAIL_CLOSED`. Construction rejects mismatched algorithm/parameters, arithmetic outside Lua's exact integer range and unsupported failure/dedup claims. The outcome is one of: - `Evaluated(decision)`; - `Unavailable(policyId, retryAfter, category)` for known pre-send/no-mutation failures and unsafe server clock; - `Indeterminate(policyId, retryAfter)` for post-dispatch uncertain mutation; - `Incompatible(policyId, category)` for state/program/reply mismatch. `RateLimitDecision` includes allow/deny, limit, remaining, retry-after, reset-at, policy ID/revision, `GLOBAL_REDIS` source and certainty. Fixed window and token bucket are `CERTAIN`; sliding-window counter is `APPROXIMATE_ALGORITHM`. ## Atomic programs Each v1 program uses one versioned hash key and calls Redis `TIME` exactly once. ```text rate-fixed-window-v1.lua rate-sliding-counter-v1.lua rate-token-bucket-v1.lua ``` Every program returns exactly seven bounded scalar fields: ```text status, serverNowMillis, effectiveNowMillis, limit, remaining, retryAfterMillis, resetAtMillis ``` Statuses are `ALLOWED`, `DENIED`, `CLOCK_UNSAFE`, `STATE_INCOMPATIBLE`, `INVALID`. Unknown arity/status/numeric syntax/range is a compatibility failure, never allow/fail-open. Common rules: - Redis server time drives enforcement; - small backward movement clamps to stored `lastObservedMillis`; - regression beyond policy threshold returns `CLOCK_UNSAFE` without consuming state; - policy/schema/algorithm mismatch returns `STATE_INCOMPATIBLE`; - denied requests do not consume quota; - state receives a finite TTL; - all arithmetic stays within `2^53-1`; - raw principal/IP/API-key/route never appears in the physical key. The existing scalar Lua executor stays intact. A structured program path adds bounded MULTI reply support and uses `EVALSHA`, falling back to the exact compiled source only on `NOSCRIPT`. ## Algorithm rules Fixed window stores window ID and consumed count. Allow increments only when `consumed + cost <= limit`; retry/reset points to the current window end. Sliding counter stores previous/current window IDs and counts, using scale `1_000_000` and conservative ceiling weight. It reports approximate certainty and a bounded conservative retry. Token bucket stores scaled tokens, last refill time and the sub-token division remainder. Refill is therefore independent of evaluation frequency, uses quotient/remainder arithmetic without an unsafe `numerator + denominator - 1` intermediate, and saturates at capacity. Denial does not subtract tokens; retry and full-reset use integer ceiling. ## Physical key The existing canonical builder is reused with: ```text capability=rate region= kind=state digest(policyId, policyRevision, algorithm, subjectDigest) ``` Policy revision appears in both digest input and stored state. A policy revision therefore rolls to a new key while old state expires naturally. ## Runtime and composition `app.rate-limit` is disabled by default. Enabling requires: - `provider=redis`; - one default policy and an exact policy definition; - a dedicated Redis coordination endpoint and HMAC secret; - finite command/admission bounds. Only `role=coordination` and `failure-policy=fail-closed` are accepted in v1. Disabled mode creates no connection, thread or semantic port. Cache Redis settings/beans are never an implicit fallback. ## Evidence Unit tests cover contract bounds, policy arithmetic, key privacy/revision, structured reply validation, `NOSCRIPT`, boundary vectors, denial-no-consume, clock regression, pre/post-dispatch failure certainty and disabled composition. The explicit Redis 7.4 service lane executes all three programs, exact-boundary admission after a denied non-consuming request, excessive clock-regression state immutability, `TYPE` response normalization, token refill-remainder carry, malformed hash-state classification, cache `NX`, and observation-token compare-and-replace. Redis 7.4 is the minimum version declared by the program manifests until a lower-version service lane exists. The caller deadline is an admission precheck against the fixed command timeout; R1 does not claim per-command dynamic timeout or hard cancellation after dispatch. Missing TLS/ACL, Sentinel/Cluster, failover and persistence/eviction evidence keeps the provider at R1.