Add per-account rate limiting with GCRA#121
Conversation
Implements per-account GCRA rate limiting for the platform API: - Redis-backed limiter (go-redis/redis_rate) for production with Valkey/ElastiCache - In-memory GCRA limiter for local development and CLI test mode - Configurable per-route overrides, exempt accounts, and fail-open on backend errors - Prometheus counter (ratelimit_requests_total) with ok/over_limit/failure_mode_allowed labels - RATE_LIMIT_TEST_MODE=true for low-rate testing (rate=3, burst=6, window=1s) - Unit tests with miniredis covering middleware, GCRA algorithm, metrics, concurrency Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
E2E test suite that runs against a deployed API with real Valkey backend. Uses concurrent goroutines with a start-gate channel pattern to overwhelm GCRA burst before token replenishment. Tests: rate limit headers, 429 triggering, 429 response body, exempt accounts, per-account isolation, and window reset. Auto-discovers the server's rate from X-RateLimit-Limit response header; RATE_LIMIT_TEST_MODE=true on the test runner assumes rate=3. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- API design doc: GCRA algorithm, headers, 429 response format, exempt accounts, fail-open behavior, Prometheus metrics - Testing doc: three-layer strategy (unit/GCRA/E2E), scenario coverage matrix, environment variables, running commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds configurable per-account API rate limiting with Redis/Valkey or in-memory GCRA, HTTP middleware enforcement, server integration, metrics and headers, E2E coverage, and operational documentation. ChangesRate Limiting
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Middleware
participant RateLimiter
participant APIHandler
Client->>Middleware: Send account-scoped request
Middleware->>RateLimiter: Check account, method, route, and limit
RateLimiter-->>Middleware: Return allow, deny, or backend error
Middleware->>APIHandler: Forward allowed request
Middleware-->>Client: Return headers or JSON 429
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 2 warnings)
✅ Passed checks (8 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
🧹 Nitpick comments (2)
CLAUDE.md (1)
28-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUpdate the canonical source instead of editing only the generated file.
CLAUDE.mdsays it is auto-generated fromAGENTS.md, but all new rate-limit guidance is added here. Regeneration can silently discard these changes; add the content toAGENTS.mdand regenerateCLAUDE.md.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@CLAUDE.md` around lines 28 - 57, Move the new rate-limiting guidance from CLAUDE.md into the canonical AGENTS.md source, preserving the Architecture and Rate Limiting sections and their details. Then regenerate CLAUDE.md using the repository’s documented generation process and verify the generated file contains the transferred content.pkg/ratelimit/config_test.go (1)
1-365: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse Ginkgo/Gomega for this test suite.
This new
*_test.gofile usestesting.Tassertions instead of the repository’s required Ginkgo/Gomega style. Migrate these cases somake testruns a consistent test framework. As per coding guidelines, “**/*_test.go: Use Ginkgo/Gomega for tests.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/ratelimit/config_test.go` around lines 1 - 365, The ratelimit test suite uses testing.T instead of the repository-required Ginkgo/Gomega framework. Migrate all tests in config_test.go, including TestLoadConfig_*、TestNewDefaultConfig_*、and TestConfig_IsExempt, into Ginkgo Describe/Context/It blocks with Gomega assertions, preserving the existing scenarios and coverage so they run through the repository’s Ginkgo test setup.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmd/rosa-regional-platform-api/main.go`:
- Around line 181-182: Remove the Redis/Valkey endpoint from the rate-limit
test-mode logs in the relevant logging branches, including the log statements
near the rate-limit backend selection. Log only the selected backend identifier,
such as “redis” or “in_memory”, and retain the rate configuration if needed
without including cfg.RateLimit.RedisAddr.
- Around line 197-205: Handle conversion errors for RATE_LIMIT_DEFAULT_RATE,
RATE_LIMIT_DEFAULT_BURST, and RATE_LIMIT_DEFAULT_WINDOW in the
environment-loading logic, rather than discarding strconv.Atoi errors. Return a
descriptive error identifying the malformed variable and stop configuration
initialization when conversion fails.
In `@docs/api/rate-limit.md`:
- Around line 114-116: Add language identifiers to the fenced code blocks
containing the `ratelimit_requests_total` metric and directory-tree examples,
using `text` or another appropriate language so markdownlint passes.
In `@docs/testing/rate-limit.md`:
- Around line 128-138: Align the test-mode backend semantics across
docs/testing/rate-limit.md and docs/api/rate-limit.md. Update the test-mode
documentation to state that RATE_LIMIT_TEST_MODE changes the defaults, while
backend selection still depends on whether REDIS_ENDPOINT is set; ensure both
documents consistently describe in-memory GCRA versus real Valkey behavior.
In `@go.mod`:
- Around line 88-89: Upgrade the aligned go.opentelemetry.io/otel and
go.opentelemetry.io/otel/trace dependencies from v1.39.0 to v1.41.0 or later in
go.mod, and update the dependency lock data consistently.
In `@pkg/ratelimit/config.go`:
- Around line 39-43: Prevent integer overflow when deriving the default burst in
the config normalization paths, including the logic around cfg.Default and the
additional rate-limit configurations. Before computing rate * 2, validate that
the rate is no greater than MaxInt/2; reject or return a configuration error
otherwise, and add boundary tests covering MaxInt/2 and MaxInt/2+1.
In `@pkg/ratelimit/local_rate_limiter_test.go`:
- Around line 69-110: Update the Allow calls in TestLocalLimiter_IsolatesKeys,
TestLocalLimiter_ResetAfterIsPositive, and
TestLocalLimiter_DeniedResultHasZeroRemaining to check and assert returned
errors before inspecting results. Replace discarded errors with explicit test
failures so unexpected setup or request errors are reported clearly instead of
allowing assertions on invalid results.
In `@pkg/ratelimit/middleware_test.go`:
- Around line 22-28: Update the cleanup handlers in setupTest and the other two
Redis client cleanup locations to capture the error returned by rdb.Close() and
report non-nil errors with t.Errorf, ensuring no Redis cleanup error is ignored.
In `@pkg/ratelimit/middleware.go`:
- Around line 123-130: Handle the error returned by json.NewEncoder(w).Encode in
Limiter.writeRateLimitError instead of discarding it; capture the error and
report it through the middleware’s established logging or error-handling
mechanism, while preserving the existing HTTP response behavior.
- Around line 71-95: Sanitize rate-limit warning logs to avoid exposing tenant
identifiers, raw resource paths, or backend details. In the failure branch of
the rate-limit middleware, replace the logged key and raw error with a fixed
failure category and log the matched route pattern instead of request-specific
identifiers; in the “rate limit exceeded” branch, remove account_id and
r.URL.Path while retaining only non-sensitive method, route pattern, rate, and
retry metadata.
In `@pkg/server/server.go`:
- Around line 70-90: Update the rate-limit configuration logic in the server
initialization block to preserve YAML routes and exempt accounts: when a config
file is configured, call ratelimit.LoadConfig first, then overwrite only the
non-zero RATE_LIMIT_DEFAULT_* fields; use ratelimit.NewDefaultConfig only when
no config file is configured. Keep logging consistent with the resulting source
and ensure route and exemption data from YAML remains intact.
In `@test/e2e-api/ratelimit_e2e_test.go`:
- Around line 88-89: Redact sensitive values in all GinkgoWriter logs within the
rate-limit E2E test, including the messages around the baseURL/accountID
diagnostic and the additionally referenced ranges. Use safe redacted
representations for account IDs and potentially internal URLs while preserving
useful logging context; update the relevant logging statements without exposing
raw os.Getenv("E2E_BASE_URL") or accountID values.
- Around line 55-90: Rate-limit E2E specs share a persistent account and bucket,
making results dependent on test order and prior runs. Update the Rate Limiting
suite setup and each spec to use isolated account IDs/namespaces or explicitly
reset and wait for rate-limit state before execution; do not rely on Ordered
alone to provide isolation.
- Around line 110-112: The rate-limit tests incorrectly derive burst capacity as
rateLimitRate*2, despite burst being independently configurable. Update the
affected tests and request-count calculations to discover the actual burst value
from configuration or headers, or explicitly use a test route/configuration with
a known burst; ensure all 429 assertions exceed that real burst.
- Around line 205-217: In the rate-limit response handling within
concurrentRequests, stop ignoring strconv.Atoi errors: require X-RateLimit-Reset
to be present and assert that parsing succeeds, failing the test immediately for
a missing or malformed header instead of defaulting resetSeconds to 2.
---
Nitpick comments:
In `@CLAUDE.md`:
- Around line 28-57: Move the new rate-limiting guidance from CLAUDE.md into the
canonical AGENTS.md source, preserving the Architecture and Rate Limiting
sections and their details. Then regenerate CLAUDE.md using the repository’s
documented generation process and verify the generated file contains the
transferred content.
In `@pkg/ratelimit/config_test.go`:
- Around line 1-365: The ratelimit test suite uses testing.T instead of the
repository-required Ginkgo/Gomega framework. Migrate all tests in
config_test.go, including TestLoadConfig_*、TestNewDefaultConfig_*、and
TestConfig_IsExempt, into Ginkgo Describe/Context/It blocks with Gomega
assertions, preserving the existing scenarios and coverage so they run through
the repository’s Ginkgo test setup.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: openshift-online/coderabbit/.coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 3e2b02a5-32a2-473f-bfb8-c943692b840d
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (18)
.gitignoreCLAUDE.mdMakefilecmd/rosa-regional-platform-api/main.godocs/api/rate-limit.mddocs/testing/rate-limit.mdgo.modpkg/config/config.gopkg/ratelimit/config.gopkg/ratelimit/config_test.gopkg/ratelimit/local_rate_limiter.gopkg/ratelimit/local_rate_limiter_test.gopkg/ratelimit/match.gopkg/ratelimit/match_test.gopkg/ratelimit/middleware.gopkg/ratelimit/middleware_test.gopkg/server/server.gotest/e2e-api/ratelimit_e2e_test.go
| ``` | ||
| ratelimit_requests_total{method, path, result} | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add language identifiers to fenced code blocks.
Use text (or another appropriate language) for the metric and directory-tree examples so markdownlint passes.
Also applies to: 143-149
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 114-114: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/api/rate-limit.md` around lines 114 - 116, Add language identifiers to
the fenced code blocks containing the `ratelimit_requests_total` metric and
directory-tree examples, using `text` or another appropriate language so
markdownlint passes.
Source: Linters/SAST tools
| ## Test Mode Configuration (`RATE_LIMIT_TEST_MODE`) | ||
|
|
||
| `RATE_LIMIT_TEST_MODE=true` sets low rate limit defaults for testability. The backend depends on whether `REDIS_ENDPOINT` is also set: | ||
|
|
||
| | `RATE_LIMIT_TEST_MODE` | `REDIS_ENDPOINT` | Backend | Use case | | ||
| |---|---|---|---| | ||
| | `true` | unset | In-memory GCRA | CLI / local development | | ||
| | `true` | set | Real Valkey | E2E testing in deployed environments | | ||
| | `false` | set | Real Valkey | Production | | ||
|
|
||
| Test mode defaults: `rate=3`, `burst=6`, `window=1s`. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Clarify the backend semantics of test mode across the documentation.
This table says RATE_LIMIT_TEST_MODE=true uses in-memory mode only when REDIS_ENDPOINT is unset, while docs/api/rate-limit.md describes test mode as in-memory/no Redis. Document that test mode changes defaults and backend selection still depends on REDIS_ENDPOINT, or make both documents describe the same behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/testing/rate-limit.md` around lines 128 - 138, Align the test-mode
backend semantics across docs/testing/rate-limit.md and docs/api/rate-limit.md.
Update the test-mode documentation to state that RATE_LIMIT_TEST_MODE changes
the defaults, while backend selection still depends on whether REDIS_ENDPOINT is
set; ensure both documents consistently describe in-memory GCRA versus real
Valkey behavior.
…serve YAML config
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: ravitri The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
/label review-ready |
PR #121 Review: Add per-account rate limiting with GCRA (API)PR: #121 Overall AssessmentSolid GCRA implementation that aligns with the design doc. Pete's feedback (failOverTimeout rename, dead config, field documentation) has been addressed in the latest commit. From an SRE perspective, simplification (remove in-memory backend) and security hardening (TLS) are the remaining must-fix items. What's Good
Must-Fix Before Merge1. OpenTelemetry CVE (GHSA-mh2q-q3fh-2475)
Action: 2. Valkey TLS in client — must match RDS standardThe Go code connecting to Valkey must use TLS when rdb := redis.NewClient(&redis.Options{
Addr: cfg.RedisAddr,
TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12},
})Overhead is <1ms. We store account IDs. FedRAMP SC-8 requires encryption in transit. Customer-managed KMS key for encryption at rest (infra PR handles this). Action: Add TLSConfig when connecting to deployed Redis. No TLS needed for 3. Consider removing in-memory backend — simplify to single code pathThe in-memory GCRA implementation is nice (works locally without infra), but it adds maintenance cost:
A simpler approach: shared Redis/Valkey among all pods, fail-open on timeout with metric. This is how ROSA v1 works with Limitador — one backend, one code path. Less code, fewer libs to maintain. For unit tests:
Recommendation: Consider removing in-memory backend in favor of simpler single-path architecture. Less to maintain, less to break. Should-Fix4. E2E tests: verify per-account isolation with distinct account IDsCritical test gap: do different account IDs have independent rate-limit buckets? If the key format accidentally doesn't namespace per account, one customer's traffic would consume another's quota. The E2E should:
Note: ephemeral E2E env already deploys Valkey — this runs against real infra as production would. 5. Window field validationThe GCRA emission interval is calculated as Currently, a misconfigured YAML like Action: Add validation in
This catches operator mistakes in the ConfigMap before they reach production. Pete's request for field documentation (comments on struct fields) was addressed in latest commit 6. Redis timeout default 50ms — lower to 10-20msDefault Dismiss7. CodeRabbit
|
| # | Item | Priority | Action |
|---|---|---|---|
| 1 | OpenTelemetry CVE | Must-fix | Bump to v1.41.0 |
| 2 | Valkey TLS in client | Must-fix | Add TLSConfig when REDIS_ENDPOINT set |
| 3 | Consider removing in-memory backend | Recommended | Simpler: single backend, less libs, less code to maintain |
| 4 | E2E: per-account isolation | Should-fix | Use 2+ distinct account IDs, verify independent buckets |
| 5 | Window field validation | Should-fix | Reject <=0 at config load to avoid zero-division/infinite-rate |
| 6 | Redis timeout default 50ms | Should-fix | Lower to 10-20ms — don't add 50ms latency per request if Valkey is degraded |
| 7 | CodeRabbit log findings | Dismiss | Both follow existing patterns (DynamoDB, Maestro, etc.) |
| 8 | Test framework mismatch | Non-blocking | config_test.go uses testing.T, repo convention is Ginkgo |
| 9 | CLAUDE.md vs AGENTS.md | Non-blocking | Rate-limit docs in wrong source file, will be overwritten |
| 10 | strconv.Atoi errors silent |
Non-blocking | Malformed env vars silently use defaults, add log.Warn |
Verdict
Excellent work — really well done. The code is clean, well-structured, and shows a strong understanding of the GCRA algorithm and production-grade middleware design. Test coverage is thorough (unit + E2E with concurrent patterns), Pete's feedback was addressed quickly, and the implementation follows the design doc closely. This is exactly the quality bar we want.
Approve with minor changes on items 1-2 (CVE bump, TLS). Item 3 (in-memory removal) is a recommendation for keeping simpler code. Items 4-5-6 (E2E isolation, validation, timeout) are should-fix within this PR. Item 7 is a CodeRabbit dismiss. Items 8-9-10 are non-blocking — can be fixed or not, no impact. Great job.
|
Removing review-ready as a review has been submitted by @slopezz -please re-add when this is ready again. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pkg/ratelimit/config.go`:
- Around line 60-62: Validate RedisTimeout in the configuration normalization
logic before it is converted to time.Duration, rejecting values above the
maximum safe millisecond duration instead of allowing overflow. Preserve the
existing defaulting for non-positive values, and add a boundary test covering
the maximum accepted value and the first overflowing value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: openshift-online/coderabbit/.coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: d461639a-848a-40c7-af8a-6c3e4347d9ea
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (5)
docs/api/rate-limit.mdgo.modpkg/ratelimit/config.gopkg/ratelimit/middleware_test.gopkg/server/server.go
🚧 Files skipped from review as they are similar to previous changes (3)
- docs/api/rate-limit.md
- pkg/ratelimit/middleware_test.go
- pkg/server/server.go
| if cfg.RedisTimeout <= 0 { | ||
| cfg.RedisTimeout = 10 | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
rg -n -C 4 'RedisTimeout|WithTimeout' pkg/ratelimit
rg -n -C 3 'redisTimeout|RedisTimeout' pkg/ratelimit/config_test.go pkg/ratelimit/middleware_test.goRepository: openshift-online/rosa-hyperfleet-api
Length of output: 7601
🏁 Script executed:
#!/bin/bash
set -eu
sed -n '1,140p' pkg/ratelimit/config.go
printf '\n----\n'
sed -n '1,180p' pkg/ratelimit/middleware.goRepository: openshift-online/rosa-hyperfleet-api
Length of output: 6860
🏁 Script executed:
#!/bin/bash
set -eu
rg -n -C 4 'type RateLimiter|func .*Allow\(|context\.DeadlineExceeded|DeadlineExceeded|context.Canceled|fail-open|failure_mode_allowed' pkg/ratelimitRepository: openshift-online/rosa-hyperfleet-api
Length of output: 4965
🏁 Script executed:
#!/bin/bash
set -eu
rg -n -C 3 'RedisTimeout|redisTimeout|MaxDuration|overflow|boundary' pkg/ratelimitRepository: openshift-online/rosa-hyperfleet-api
Length of output: 4388
🏁 Script executed:
#!/bin/bash
set -eu
cat <<'EOF' >/tmp/timeout_overflow.go
package main
import (
"fmt"
"time"
)
func main() {
vals := []int{
10,
2147483647,
9223372036854,
9223372036855,
9223372036854775,
}
for _, v := range vals {
d := time.Duration(v) * time.Millisecond
fmt.Printf("%d => duration=%d ns, repr=%s, expiredNow=%v\n", v, int64(d), d, time.Now().Add(d).Before(time.Now()))
}
}
EOF
go run /tmp/timeout_overflow.goRepository: openshift-online/rosa-hyperfleet-api
Length of output: 573
Bound RedisTimeout before converting it to time.Duration. A large YAML value can wrap time.Duration(l.config.RedisTimeout) * time.Millisecond negative, which makes the context expire immediately and pushes Redis errors into the fail-open path. Reject values above the max safe millisecond duration and add a boundary test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pkg/ratelimit/config.go` around lines 60 - 62, Validate RedisTimeout in the
configuration normalization logic before it is converted to time.Duration,
rejecting values above the maximum safe millisecond duration instead of allowing
overflow. Preserve the existing defaulting for non-positive values, and add a
boundary test covering the maximum accepted value and the first overflowing
value.
Source: Path instructions
|
/label review-ready |
|
@ravitri: The label(s) DetailsIn response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
Description
Adds per-account rate limiting to the platform API using the Generic Cell Rate Algorithm (GCRA). Requests are tracked per AWS account ID, extracted from the
X-Amz-Account-Idheader injected by API Gateway via SigV4 signing.Commit 1: Rate limiting middleware with Redis and in-memory backends
pkg/ratelimit/with two backends:go-redis/redis_rate) for production with Valkey/ElastiCachefailure_mode_allowedmetric emittedratelimit_requests_totalwith labels:method,path,result(ok|over_limit|failure_mode_allowed)X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,Retry-After(on 429)RATE_LIMIT_TEST_MODE=truesets low defaults (rate=3,burst=6,window=1s)minirediscovering middleware, GCRA algorithm, metrics, concurrency, fail-open, route overridesCommit 2: E2E tests for rate limiting
ratelimit: headers, 429 triggering, 429 response body, exempt accounts, per-account isolation, window resetX-RateLimit-Limitresponse header;RATE_LIMIT_TEST_MODE=trueon the test runner assumesrate=3make test-e2e-api E2E_LABEL_FILTER=ratelimitCommit 3: Documentation
docs/api/rate-limit.md— design doc covering algorithm, headers, 429 format, config, fail-open, metricsdocs/testing/rate-limit.md— three-layer testing strategy (unit/GCRA/E2E), scenario coverage matrix, environment variablesCLAUDE.mdupdated with rate limiting sectionTest plan
make test— unit tests pass (miniredis, GCRA algorithm, config parsing)RATE_LIMIT_TEST_MODE=true make test-e2e-api E2E_LABEL_FILTER=ratelimit— E2E tests pass against deployed API with Valkeycurlagainst a running instance withRATE_LIMIT_TEST_MODE=true/metricsendpoint showratelimit_requests_totalcounter🤖 Assisted by Claude Code
Summary by CodeRabbit
X-RateLimit-*response headers.429withRetry-After, plus Prometheus metrics, with fail-open behavior when the rate-limit backend is unreachable.