Nobody has started this yet — be first.
Business impact
Security- and abuse-sensitive endpoints (login, signup, checkout, password reset) routinely need protection along more than one dimension at once -- one bad IP, or one bad account, either alone shouldn't need to wait for the other to also misbehave before being throttled. Without a composable primitive, teams either ship weaker single-dimension protection (leaving a gap real abusers will find) or hand-roll bespoke multi-limiter logic per endpoint, which is slower to build, inconsistent across the codebase, and a larger surface for the exact kind of bugs this whole task list is about.
Problem
Every seeded rule enforces exactly one limiter against exactly one key dimension. There's no way to express "allow this request only if it's within the per-IP limit and within the per-API-key limit." A stub already exists in pkg/ratelimit/composite.go: NewComposite currently just delegates to the first limiter and ignores the rest entirely.
Current behavior
NewComposite(a, b).AllowN(...) is allowed even when one of the child limiters (b) would deny it, because the stub only ever consults the first child.
Expected behavior
NewComposite(a, b, c).AllowN(ctx, key, n) calls AllowN on every child limiter and returns Allowed: true only if every child returned Allowed: true. If any child denies, the overall RetryAfter is the longest among the denying children, and Limit/Remaining come from the most restrictive child. If any child errors, the overall call errors (fail closed).
Steps to reproduce
a := ratelimit.NewTokenBucketMemory(100, 100, time.Minute) // effectively never denies
b := ratelimit.NewTokenBucketMemory(1, 0, time.Minute) // exhausted after 1 call
composite := ratelimit.NewComposite(a, b)
composite.Allow(ctx, "key") // consumes b's only token -> allowed
composite.Allow(ctx, "key")
// got: Allowed: true (the stub only ever consults a, which is never exhausted)
// want: Allowed: false (b is exhausted, and AND semantics require every child to allow)
Why this matters
Composable rate-limit policies (this endpoint needs both a per-IP guard and a per-account guard) are extremely common in real APIs, and implementing Limiter for a combinator over other Limiters -- rather than baking multi-dimensional logic into a bespoke type -- is exactly the kind of interface design the whole package is built around.
Suggested approach
The Limiter interface only needs Allow/AllowN implemented on your new composite type; it can hold []Limiter internally. All children must always be called, even after one has already denied. Think about whether calling children sequentially vs. concurrently (errgroup-style) matters here -- it affects latency (Redis-backed children each cost a round trip) but not correctness. Note as a known, documented trade-off: because Limiter has no "undo" operation, if child A allows (consuming quota) but child B denies, A's consumed quota is not refunded.
Acceptance criteria
Verification
go test ./test/ -run TestTask10 -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/composite-limiterFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/composite-limiterSubmit 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.