Nobody has started this yet — be first.
Business impact
The shared IngestQueue is sized generously, but nothing currently stops one device from claiming all of it. DeviceRateLimiter is already wired into both POST /events and POST /events/batch — both already call rate_limiter.allow(event.device_id) before enqueueing and reject with 429 when it returns False — but allow() always returns True, so a single noisy device can starve every other device sharing the same process's capacity, and there is no fairness protection at all today.
Problem
DeviceRateLimiter.allow(device_id) is a stub that always returns True, no matter how many requests a given device_id has made or how fast, even though Settings.per_device_rate_limit_per_second is already threaded into its constructor.
Current behavior
Calling DeviceRateLimiter(limit_per_second=5).allow("d1") 50 times back-to-back returns True every single time; none are throttled.
Expected behavior
allow(device_id) enforces per_device_rate_limit_per_second as a per-device_id rate limit: a device sending well under its limit is never throttled, a device sending far over it eventually gets False, and the limit is tracked independently per device_id.
Steps to reproduce
pytest practice_tickets/tests/test_ticket_10_rate_limiter.py -v — calls allow("d1") 50 times back-to-back against a 5/sec limit and confirms none are throttled.Why this matters
This is a fairness problem, not a capacity problem: the queue itself is sized generously, but nothing currently stops one device from claiming all of it. A token-bucket or sliding-window counter keyed by device_id is the standard shape for this (a simple fixed window is easier to implement correctly than a true sliding window, at the cost of allowing bursts right at window boundaries).
Suggested approach
Look at DeviceRateLimiter.init — it already stores limit_per_second. allow() needs some per-device_id state (a dict keyed by device_id, holding whatever a token-bucket or fixed-window counter needs) to decide, on each call, whether this device is currently within its budget.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_10_rate_limiter.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 feat/per-device-rate-limitingFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/per-device-rate-limitingSubmit 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:8000localhost:5432curl localhost:8000/healthzcurl localhost:8000/readyzWork the tickets in practice_tickets/tickets/ (ordered easy → hard); each names one dedicated test in practice_tickets/tests/:
pytest practice_tickets/tests/test_ticket_01_persisted_count.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 20, clean pass/fail summary
TICKET-17 needs a real Postgres to run at all (it checks which column a value lands in, which no fake/in-memory DB backend can meaningfully verify) — set TEST_DATABASE_URL=postgresql://<you>@localhost/ingest_test before running it or the full suite; without it, that one test is skipped, not failed.
Level 2
Implement a feature
Extend the system within its own patterns.