Nobody has started this yet — be first.
Business impact
Retry-After is the contract a rate limiter makes with well-behaved clients: "back off for this long, then it is safe to try again." A client that honors the header and gets told Retry-After: 0 will retry immediately -- and get another 429 -- potentially forever, in a tight loop. A rate limiter that lies about its own cooldown does not protect anything; it just adds a confusing, self-defeating layer of latency on top of whatever abuse it failed to stop.
Problem
When only one axis (IP or account) is over budget -- the common case -- enforce_dual_rate_limit() computes retry_after = min(ip_retry if not ip_ok else 0, account_retry if not account_ok else 0), and the axis that did not trip contributes the placeholder 0. min(0, anything) is always 0, so Retry-After reports 0 any time exactly one axis tripped, which is most of the time this code path runs at all.
Current behavior
A 429 response's Retry-After header reads "0" even though the account (or IP) axis is genuinely rate-limited for the rest of its window.
Expected behavior
Retry-After reflects the longer of the two axes' remaining cooldowns -- how long the client actually needs to wait before either limit will have reset. max(...) achieves this and also correctly collapses to just that axis's real remaining time when only one axis is over budget, since the other term is 0 and max(0, real_value) == real_value.
Steps to reproduce
Why this matters
min/max mix-ups are easy to make and easy to miss because the function still "does something" and returns a plausible-looking integer -- the bug only shows up when you reason about what each argument represents in the single-axis-tripped case, which is the normal case in production, not an edge case.
Suggested approach
Work out, on paper, what ip_retry if not ip_ok else 0 evaluates to when only the account axis is over budget. Then ask: is min or max of that pair ever going to represent "wait long enough for both axes to clear"?
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_04.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/retry-after-min-vs-maxFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/retry-after-min-vs-maxSubmit 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:5000make seedpyotpmake docker-prod-upYou do not need Docker running at all to work the tickets -- the test suite uses an in-memory SQLite database (via StaticPool, an isolated DB per test function) and fakeredis (TestingConfig.USE_FAKE_REDIS), so everything runs against a plain virtualenv:
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pytest practice_tickets/tests/test_ticket_01.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 15, clean pass/fail summary
All 15 tests fail at the start; as you fix each ticket in app/, its test (and only its test) should flip to passing. make test (pytest -q) runs the project's own main suite -- as of writing it shows 9 pre-existing failures caused by ticket bugs 02, 03, 07, 11, and 15; fixing those tickets fixes those failures too, not a separate task. Do not look at git diff / version control history to find the bugs if this repo is ever put under source control -- the point is to find them the way you would find a real bug: from a failing test and a description of broken behavior, not a diff.
Level 1
Fix a bug
Read existing behaviour, correct it.