Nobody has started this yet — be first.
Business impact
Any two clients racing to claim the same custom slug at the same instant (a common pattern for anything gamified or scarce -- "claim your brand name before someone else does") no longer get the advertised, documented behavior ("a collision must return a clean 409, never a 500"). Instead, depending on timing, one or both requests can crash with a raw database error. A public API that occasionally 500s under concurrent load on a completely foreseeable, common case (two people wanting the same short name) erodes trust in the API and generates noisy, confusing error reports that don't point at "someone else already has this slug."
Problem
if custom_slug: existing = ShortURL.query.filter_by(slug=custom_slug).first(); if existing is not None: return 409 -- this is a check-then-act (TOCTOU) pattern with no safety net: between the SELECT that checks for an existing row and the later commit() that actually inserts the new one, there is no lock and no except IntegrityError around the commit.
Current behavior
If two requests for the same custom_slug both run their SELECT before either commits, both see "no existing row" and both proceed to insert -- the resulting IntegrityError (or, depending on timing, an OperationalError from two overlapping transactions) is never caught, surfacing as an uncaught exception instead of the intended 409 for the loser.
Expected behavior
Exactly one of two concurrent requests for the same custom_slug succeeds with 201; the other gets a clean 409. Neither ever surfaces a raw, uncaught database exception to the client.
Steps to reproduce
Why this matters
This is the canonical "check-then-act" concurrency bug: correctness under concurrency requires either (a) making the check-and-insert atomic (rely on the database's own uniqueness constraint as the single source of truth, and treat a constraint violation as the expected "someone beat you to it" signal -- i.e. wrap the commit in try/except IntegrityError), or (b) taking an explicit lock before checking. Removing the except IntegrityError safety net that used to wrap this commit, while adding a separate, unsynchronized pre-check, is exactly how a previously-safe insert-and-catch pattern regresses into a real race.
Suggested approach
Think about what actually prevents two inserts of the same slug from both succeeding: it's the database's UNIQUE constraint, not the SELECT check (which only reduces how often the race is hit, not whether it is possible). The fix should treat the constraint violation itself as the authoritative "this slug is taken" signal, and handle it gracefully at the point where the commit can fail -- rather than trying to prevent the race with a faster or better-timed check.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_16_slug_creation_race.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/slug-creation-raceFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/slug-creation-raceSubmit 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 indocker-entrypoint.shcurl -X POST http://localhost:5000/api/urls \
-H "Content-Type: application/json" \
-d '{"target_url": "https://anthropic.com", "custom_slug": "claude"}'
# -> {"slug": "claude", "short_url": "http://localhost:5000/claude", "expires_at": null}
curl -i http://localhost:5000/claude # 302 redirect, records a click
make seed # or: docker compose exec web flask seed-db
If something is already listening on 5432/6379 locally, uncomment POSTGRES_HOST_PORT / REDIS_HOST_PORT in .env to remap the host side — the app always talks to db/redis over the compose network regardless. Run production-style (gunicorn, no dev server, no bind mount) with docker compose -f docker-compose.prod.yml up --build. GET /healthz checks both Postgres and Redis connectivity.
You do not need Docker running at all to work the tickets — the test suite uses TestingConfig (SQLite in-memory via StaticPool) and fakeredis instead of a real Postgres/Redis, 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_01_base62_roundtrip.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 20, clean pass/fail summary
./practice_tickets/run_tickets.sh 05 13 16 # just a few, by number
All 20 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/ (via make test) runs the project's own main suite — some ticket bugs (TICKET-05, 07, 08, 09, 14 among others) also break assertions in that suite, since they are real bugs in shared code paths; fixing the ticket fixes those too, not a separate task.
Level 1
Fix a bug
Read existing behaviour, correct it.