{10x}debuggable
Rate Limiter Service
bugbeginnerP1pkg/ratelimit/leaky_bucket.goLevel 1 · Fix a bug10 XP30–45 minutes

Leaky bucket fills instead of draining

Nobody has started this yet — be first.

Business impact

This algorithm guards login-brute-force-guard -- the login page, the single most trafficked, most first-impression-critical path in the product. A bug that turns a temporary slow-down into a permanent lockout means any legitimate user who ever tried logging in 5 times in a row (a mistyped password, a shared office IP) is locked out forever, with no recovery except an operator restarting the whole service -- which resets everyone's state, not just theirs. It reads to customers as "the login page is broken" and can look like a full outage even though every other system is healthy.

Problem

The leaky bucket is supposed to leak -- its fill level should decrease over time at leakRate units/sec, so a key that stops sending requests eventually becomes free to send again. Right now the level only ever goes up over time, never down.

Current behavior

Elapsed idle time is being added to the bucket's level instead of subtracted from it. A key that receives one burst of traffic and then goes completely idle still shows its level rising indefinitely, permanently denying that key.

Expected behavior

Elapsed idle time should reduce the level by elapsed * leakRate, floored at 0. A key that stops sending traffic must eventually recover -- the whole point of a leaky bucket over a hard blocklist.

Steps to reproduce

  1. lb := ratelimit.NewLeakyBucketMemory(1, 50 /* drains 50/sec */, time.Minute)
  2. lb.Allow(ctx, "user-1") -> allowed (level now 1, at capacity).
  3. lb.Allow(ctx, "user-1") -> denied (over capacity), as expected.
  4. time.Sleep(30 * time.Millisecond) -- 1.5 units of drain at 50/sec, more than enough to bring the level back under capacity 1.
  5. lb.Allow(ctx, "user-1") again.
  6. Observe: still denied, and RetryAfter keeps growing no matter how long you wait.
  7. Expected: step 5 should be Allowed: true -- the bucket had time to drain.

Why this matters

The seeded login-brute-force-guard rule (capacity 5, drains 1/10sec) is meant to slow down repeated login attempts, not permanently ban every IP that ever tried to log in 5 times. With this bug live, the first burst of 5 login attempts from any legitimate user permanently locks that IP/key out of /demo/login for the lifetime of the process -- a self-inflicted denial-of-service against your own users.

Suggested approach

One arithmetic operator, in the same spot where elapsed is combined with lb.leakRate to update e.level. Compare against the in-memory token bucket's refill line in token_bucket.go for the mirror-image case (tokens go up over time there; level should go down here) -- and against the Redis Lua script leaky_bucket.lua, which already does this correctly.

Acceptance criteria

  • A bucket at capacity, given enough idle time (elapsed * leakRate >= current level), admits a new request again
  • go test ./pkg/ratelimit/... -run LeakyBucketMemory passes, including the pre-existing TestLeakyBucketMemory_DrainsOverTime
  • go test ./test/ -run TestTask02 -v passes

Verification

go test ./test/ -run TestTask02 -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.

01

Branch off your fork

$git checkout -b fix/leaky-bucket-drains-backwards
02

Fix it and commit

Meet every acceptance criterion, and add a test that would have caught this.

03

Push the branch

$git push -u origin fix/leaky-bucket-drains-backwards
04

Submit it below

Paste your fork URL and the branch name, with a short write-up of the root cause.

Submit your fix

Sign in to submit a solution and track your progress.

Sign in to submit

Questions

Stuck on something?

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 in
No questions yet. If something in this ticket reads ambiguously, you are probably not the only one — ask.