Nobody has started this yet — be first.
Business impact
Token bucket backs the pro-tier plan (and any small-capacity policy, e.g. "1 password reset email per key at a time"). Losing the last unit of every configured burst means paying customers systematically get less throughput than the plan they're sold -- worse for small capacities, where "capacity 1" effectively means "capacity 0, always denied." That shows up as support tickets ("your API says I'm rate-limited but I've made zero requests"), erodes trust in the pricing tiers, and is the kind of quietly-wrong behavior that's easy to ship unnoticed because most manual testing checks "does it deny eventually," not "does it admit exactly N."
Problem
TokenBucketMemory.AllowN is supposed to allow bursts "up to capacity" (see the package doc and the README's algorithm table). In practice, the last request that would exactly exhaust the bucket is being rejected instead of admitted.
Current behavior
Given a fresh token bucket with capacity 3, the 3rd request in immediate succession (before any refill) is denied, even though the bucket started with exactly 3 tokens and this is the 3rd request.
Expected behavior
A request is allowed whenever the bucket holds at least as many tokens as requested. Consuming the last available token(s) should succeed, not fail -- the bucket should only reject a request when it does not have enough tokens, not when it has exactly enough.
Steps to reproduce
tb := ratelimit.NewTokenBucketMemory(3, 0 /* no refill */, time.Minute) tb.Allow(ctx, "user-1") // call it 3 times in a row // got: the 3rd call returns Allowed: false, RetryAfter: 0 // want: all three calls return Allowed: true; only a 4th call should be denied
Why this matters
Any caller relying on "capacity N means N requests get through" silently loses one unit of their configured burst allowance. For a small capacity (e.g. capacity 1, used to gate a single in-flight operation per key) this means the feature never actually admits anything.
Suggested approach
The bug is a single comparison operator inside AllowN's token-sufficiency check. Re-derive, from the doc comment above the function and the README's description of "allows bursts up to a capacity," what the boundary condition (tokens == n) should do, and compare that to what the current operator does. Do not change the refill math or the Remaining/ResetAt calculations -- only the admission check is wrong.
Acceptance criteria
Verification
go test ./test/ -run TestTask01 -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 fix/token-bucket-off-by-oneFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/token-bucket-off-by-oneSubmit 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 1
Fix a bug
Read existing behaviour, correct it.