Nobody has started this yet — be first.
Business impact
Rate limiting is meant to be a best-effort protection, not a hard dependency for whether an endpoint works at all. As built today, a Redis blip -- routine infra maintenance, a network hiccup, a failover -- takes down every protected endpoint with a 500, converting a minor, expected infrastructure event into a full customer-facing outage. That's a much larger blast radius than the actual failure warrants, and it directly threatens uptime SLAs for something that should, worst case, only make rate limiting slightly less accurate for a few minutes.
Problem
Every Redis-backed limiter's AllowN returns Result{}, err verbatim whenever client.Eval(...) fails, and middleware.RateLimit treats any error from the limiter as fatal to the request (a 500). A stub exists at pkg/ratelimit/fallback.go: NewWithFallback currently always defers to primary and returns its error untouched.
Current behavior
A Redis outage doesn't just disable rate limiting -- it takes the entire protected endpoint down with a 500, for every request, for as long as Redis is unreachable.
Expected behavior
NewWithFallback(primary, fallback Limiter) Limiter calls primary.AllowN first; on success, returns that result untouched. On error, it calls fallback.AllowN instead and returns that result with no error (a degraded but functional decision). If the fallback also errors, that error propagates.
Steps to reproduce
primary := &alwaysErroringLimiter{} // simulates a Redis outage fallback := ratelimit.NewTokenBucketMemory(10, 10, time.Minute) l := ratelimit.NewWithFallback(primary, fallback) res, err := l.Allow(ctx, "user-1") // got: err is the primary's error (simulated redis outage) -- the request fails // want: err is nil, res reflects the fallback limiter's decision
Why this matters
This is the classic graceful-degradation pattern: a non-critical dependency (the exact rate is best-effort; enforcing it slightly wrong for a few minutes during an incident is a much smaller problem than every request failing). Treating "the rate limiter's backend hiccuped" as equivalent to "the whole endpoint is down" is a common real-world overreaction that turns a minor infrastructure blip into a customer-facing outage.
Suggested approach
This is a small, self-contained wrapper type -- no locking or shared state needed beyond holding references to the two child Limiters. Leave a doc comment noting the trade-off that a caller who wants to know they got a degraded decision (for logging/metrics) has no out-of-band signal for it in the current Limiter interface, the same way task-10 documents its own known limitation.
Acceptance criteria
Verification
go test ./test/ -run TestTask14 -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b feat/redis-fallbackFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/redis-fallbackSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inhttp://localhost:8080localhost:6380http://localhost:8081curl localhost:8080/healthz
curl localhost:8080/api/v1/rules
curl -s -X POST localhost:8080/api/v1/check -d '{"rule":"free-tier","key":"user-123"}' | jq
No Docker? STORE_BACKEND=memory is the default, so go run ./cmd/server alone (no Redis needed) exercises everything except the Redis-backed code paths.
Work the tickets in tasks/ (ordered easy → hard); each names one Go test in test/:
make test-race # go test -race -count=1 ./...
go test ./test/ -run TestTask01 -v # a single ticket
./test/run_all.sh # all 15, pass/fail summary
Level 2
Implement a feature
Extend the system within its own patterns.