Nobody has started this yet — be first.
Business impact
Not every product need justifies the sliding window's extra complexity -- some quota shapes (a daily export limit, a per-billing-period cap) are naturally and correctly expressed as a plain fixed window, and forcing every customer-facing quota through the sliding window's blending logic is more code, more to explain to customers who ask "how exactly does my limit work," and more surface for the class of bug in task 09. This ticket is also a direct test of the codebase's central design promise -- if it can't be done cleanly, that's a signal the abstraction underpinning every future rate-limiting feature needs rework before the product commits to more policies built on top of it.
Problem
The README explicitly calls this out as the intended extension point: "Add a fourth algorithm: implement ratelimit.Limiter (Allow/AllowN) in a new file under pkg/ratelimit/; nothing else in the codebase needs to change." There is no plain fixed-window counter yet. A stub exists at pkg/ratelimit/fixed_window.go that currently ignores its configured limit entirely and allows everything.
Current behavior
FixedWindowMemory.AllowN always returns Allowed: true regardless of how many requests have already been made for a key -- the configured limit has no effect.
Expected behavior
Time is divided into fixed-size, non-overlapping windows of length window (clock-aligned to window boundaries). Each key tracks a single counter for "requests seen in the current window." A request is allowed if counter + n <= limit, and the counter resets to 0 the instant the wall clock crosses into a new window -- no blending with the previous window.
Steps to reproduce
fw := ratelimit.NewFixedWindowMemory(5 /* limit */, time.Minute, time.Hour) for i := 0; i < 6; i++ { res, _ := fw.Allow(ctx, "user-1") } // got: the 6th request in the same window is allowed (the stub always returns Allowed: true) // want: the 6th request in the same window is denied; the counter resets once the window rolls over
Why this matters
This is the extension path the codebase was explicitly designed for. Doing it once is what proves (or disproves) the claim that the Limiter interface is actually sufficient abstraction for "any future algorithm" -- if you find yourself needing to touch code outside pkg/ratelimit/ for the core memory implementation to work, that's worth noticing.
Suggested approach
This is structurally the "current window" half of sliding_window.go with the "blend with previous window" part deleted -- read that file for the window-alignment (curStart, via now / window * window) trick and the per-key entry/mutex-or-atomic pattern, then decide for yourself whether a single counter needs a mutex or can be done with atomic.Int64 + CAS. Reuse the existing generic janitor (pkg/ratelimit/janitor.go) for idle-key eviction rather than reinventing it.
Acceptance criteria
Verification
go test -race ./test/ -run TestTask12 -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/fixed-window-algorithmFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/fixed-window-algorithmSubmit 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.