Nobody has started this yet — be first.
Business impact
requests_per_second: 0 is a reasonable, sensible-looking way to configure "block all traffic on this route through the rate limiter" (e.g. temporarily killing a specific client's access without deleting their route or auth credential entirely). Instead of cleanly rejecting every request with 429, the route errors out on every single request -- the rate-limit check itself throws, which the gateway has no specific handling for, so it surfaces as an unhandled exception. What was meant to fully block a client instead breaks the route for everyone hitting it, in a way that is much harder to diagnose than a plain 429 would be.
Problem
The token bucket's Lua script computes an idle-expiry TTL for the bucket key with . When () is , divides by zero, producing , and then gets asked to expire a key in seconds -- which Redis rejects because requires an integer, raising a out of .
Current behavior
RateLimiter.check(..., requests_per_second=0, ...) raises redis.exceptions.ResponseError: value is not an integer or out of range instead of returning a RateLimitResult, because the TTL housekeeping line divides by rate without guarding against rate == 0.
Expected behavior
A route configured with requests_per_second: 0 (or, more generally, any rate that would make this TTL computation degenerate) cleanly rejects every request -- the same RateLimitResult(allowed=False, ...) shape every other exhausted bucket produces -- rather than raising.
Steps to reproduce
Why this matters
This is a boundary condition that is invisible from reading the token bucket script's main logic in isolation -- the token-accounting math is completely correct for rate=0 on its own (tokens simply never refill, so the bucket empties and stays empty, which is exactly "block everything"). It is only the housekeeping line -- the TTL calculation added so idle buckets do not accumulate in Redis forever -- that assumes rate is always positive and blows up otherwise.
Suggested approach
The TTL only exists so an idle bucket eventually expires from Redis instead of leaking forever; it was never meant to depend on the request actually succeeding or failing. Think about what a sane, finite TTL would be for a bucket that will never refill (rate=0), independent of the burst / rate formula that assumes refilling is happening at all -- and where in the Lua script (or in RateLimiter.check()'s Python side, before it ever calls the script) that special case belongs.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_13_rate_limit_zero_rps_crashes.py -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/rate-limit-zero-rps-crashesFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/rate-limit-zero-rps-crashesSubmit 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 inlocal ttl = math.ceil((burst / rate) * 2) + 1raterequests_per_second0burst / rateinfredis.call('EXPIRE', key, ttl)infEXPIREredis.exceptions.ResponseErrorRateLimiter.check()http://localhost:8080fast_echo/echo/.../large/{size_mb}slow_upstream/delay/{s}/flaky/always-downroutes.yamlcurl localhost:8080/healthzcurl localhost:8080/readyz/admin/*Authorization: Bearer <GATEWAY_ADMIN_TOKEN>You do not need Docker running at all to work the tickets — the test suite fakes Redis with fakeredis (plus lupa for real Lua-script support), so everything runs against a plain virtualenv:
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/pytest practice_tickets/tests/test_ticket_09_breaker_threshold_off_by_one.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 15, clean pass/fail summary
./practice_tickets/run_tickets.sh 06 09 15 # just a few, by number
All 15 tests fail at the start; as you fix each ticket in app/, its test (and only its test) should flip to passing. .venv/bin/pytest tests/ runs the project's own main suite — as of writing it shows 8 pre-existing failures caused by ticket bugs 02, 04, 06, and 09; fixing those tickets fixes those failures too, not a separate task.
Level 1
Fix a bug
Read existing behaviour, correct it.