Nobody has started this yet — be first.
Business impact
GET /notifications/stream has no limit on how many concurrent connections a single user_id can hold open at once. app/config.py already defines max_connections_per_user (default 5) with a comment explaining its purpose, but nothing enforces it -- a client with a retry-loop bug that opens a new connection on every failed attempt without closing the old one, or a malicious script, can accumulate an arbitrarily large number of open connections for one user_id, each holding its own Redis pub/sub subscription and its own slot in whatever connection limits the deployment has.
Problem
Nothing anywhere counts or caps how many concurrent GET /notifications/stream connections a single user_id currently has open; app/routers/stream.py accepts every request and app/streaming.py subscribes unconditionally regardless of the count.
Current behavior
With settings.max_connections_per_user set to 2, opening a 3rd concurrent stream connection for the same user_id still returns 200 instead of 429, even though the first two already succeeded.
Expected behavior
Once a user_id already has max_connections_per_user streams open on this instance, one more GET /notifications/stream for that same user_id is rejected with 429 Too Many Requests instead of being accepted as an additional concurrent connection. A connection that later closes frees up its slot for a future connection attempt.
Steps to reproduce
pytest practice_tickets/tests/test_ticket_14_per_user_connection_cap.py -vWhy this matters
This needs state that outlives any single request -- most naturally a per-user connection count on app.state -- incremented when a stream starts and decremented when it ends, including when it ends via an exception, a client disconnect, or the generator being cancelled outright, not just the clean-exit path. Get the decrement wrong and the bug this ticket exists to prevent just moves from the client opening unbounded new connections to the counter itself never going back down, which is arguably worse since it doesn't even require a misbehaving client.
Suggested approach
event_stream()'s existing finally block is exactly where a per-connection resource that must always be released already gets cleaned up -- the existing gauge decrement there is the precedent to follow for whatever per-user counter gets added. The rejection itself needs to happen before a new pub/sub subscription is created at all, which points at app/routers/stream.py, checking the count before calling event_stream(), as the natural place to return 429.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_14_per_user_connection_cap.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-user-connection-capFix 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-user-connection-capSubmit 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:8000http://localhost:8001Authorization: Bearer <user_id>X-User-Id: <user_id>curl localhost:8000/healthz
curl -N -H "Authorization: Bearer alice" localhost:8000/notifications/stream
# watch `: heartbeat` lines arrive roughly every 15s with no other traffic
Work the tickets in practice_tickets/tickets/ (ordered easy → hard); each names one dedicated pytest test in practice_tickets/tests/:
pytest practice_tickets/tests/test_ticket_01_heartbeat_default_margin.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 15, clean pass/fail summary
Tickets 14 and 15 need genuinely concurrent, long-lived streams, which the ASGI test transport used everywhere else in this project can't represent -- their tests spin up a real (short-lived) uvicorn server on a loopback socket instead (LiveServer in practice_tickets/tests/conftest.py).
Level 2
Implement a feature
Extend the system within its own patterns.