Transactions, Locking & Concurrency in Code · Locking Strategies

Constraints and Upserts as Concurrency Control

Let the database enforce uniqueness: check-then-insert races, unique indexes, ON CONFLICT and ON DUPLICATE KEY.

Scenario

Example app: a SaaS billing service. A customer may have only one active subscription, and each webhook from the payment provider must be processed once. A daily stats table counts sign-ups per day.

What you see in production

Some customers are billed twice — they have two active subscriptions created 40 ms apart (a double-click plus a retry). A few provider webhooks were processed twice. Daily stats occasionally fail with "duplicate key" errors.

The naive code

Check whether a row exists, and insert if it doesn't:

def subscribe(customer, plan):
    if Subscription.objects.filter(customer=customer, status="active").exists():
        raise AlreadySubscribed()
    return Subscription.objects.create(customer=customer, plan=plan, status="active")

def count_signup(day):
    stat, _ = DailyStat.objects.get_or_create(day=day)    # races: two creates for the same day
    stat.signups += 1                                      # and a lost update on top
    stat.save()

Reproduce it

Check-then-insert in application code, interleaved by two requests:

Check-then-insert raceTry it yourself
subscriptions = []

def subscribe(request):
    exists = any(s["status"] == "active" for s in subscriptions)
    yield f"{request}: active subscription exists? {exists}"
    if not exists:
        subscriptions.append({"by": request, "status": "active"})
        yield f"{request}: created subscription"

a, b = subscribe("click"), subscribe("retry")
for step in (a, b, a, b):
    print(next(step))
print("active subscriptions:", len(subscriptions))

Output

click: active subscription exists? False
retry: active subscription exists? False
click: created subscription
retry: created subscription
active subscriptions: 2

The database version, on real SQLite: a partial unique index rejects the second insert, and an upsert counts atomically:

Unique index + upsertTry it yourself
CREATE TABLE subscriptions (id INTEGER PRIMARY KEY, customer_id INT, status TEXT);
CREATE UNIQUE INDEX one_active_sub ON subscriptions (customer_id) WHERE status = 'active';
INSERT INTO subscriptions (customer_id, status) VALUES (1, 'active');
INSERT OR IGNORE INTO subscriptions (customer_id, status) VALUES (1, 'active');   -- the duplicate is rejected
SELECT count(*) AS active_subs FROM subscriptions WHERE customer_id = 1 AND status = 'active';

CREATE TABLE daily_stats (day TEXT PRIMARY KEY, signups INT NOT NULL);
INSERT INTO daily_stats VALUES ('2026-10-13', 1) ON CONFLICT (day) DO UPDATE SET signups = signups + 1;
INSERT INTO daily_stats VALUES ('2026-10-13', 1) ON CONFLICT (day) DO UPDATE SET signups = signups + 1;
INSERT INTO daily_stats VALUES ('2026-10-13', 1) ON CONFLICT (day) DO UPDATE SET signups = signups + 1;
SELECT * FROM daily_stats;

Output

active_subs
1

day | signups
2026-10-13 | 3

Root cause

"Does it exist?" and "insert it" are two steps; nothing stops two requests from both seeing "no" before either inserts. And there's no existing row to lock. Only the database sees every insert, so only a unique index can guarantee uniqueness.

The fix

Declare the rule as a constraint and handle the violation; use an upsert when the right answer to a duplicate is "update instead":

CREATE UNIQUE INDEX one_active_subscription ON subscriptions (customer_id) WHERE status = 'active';
CREATE UNIQUE INDEX webhook_once ON processed_webhooks (provider, event_id);

INSERT INTO daily_stats (day, signups) VALUES (current_date, 1)
ON CONFLICT (day) DO UPDATE SET signups = daily_stats.signups + 1;

INSERT INTO processed_webhooks (provider, event_id) VALUES ($1, $2)
ON CONFLICT DO NOTHING RETURNING id;      -- no row returned → already processed

Trade-offs

  • Constraints protect every writer — background jobs, admin scripts, other services — not just the code path you fixed.
  • A unique violation aborts the current Postgres transaction; wrap the insert in a savepoint (nested atomic()) if the transaction must continue.
  • Some ORM "upsert" helpers emulate the operation with a read then a write; check the generated SQL is a real ON CONFLICT / ON DUPLICATE KEY.
  • Upserts on a single hot key (one stats row per day) still serialize on that row — fine at hundreds per second, not at tens of thousands.

Detect it in production

  • Duplicate finders: SELECT customer_id FROM subscriptions WHERE status = 'active' GROUP BY 1 HAVING count(*) > 1;
  • Unique-violation counts (SQLSTATE 23505, MySQL error 1062) per endpoint — a steady trickle is normal (double clicks); a spike is a client bug.

Key points

  • Check-then-insert races; there is no row to lock yet.
  • Enforce uniqueness with unique (partial) indexes and handle the violation.
  • Use ON CONFLICT / ON DUPLICATE KEY UPDATE for insert-or-update.
  • Constraints are the guard that works for every code path.

Practice questions

A customer ends up with two active subscriptions created 40 ms apart. Why did the existence check fail, and what fixes it?

Answer

Both requests checked before either inserted, so both saw no active subscription. A partial unique index on (customer_id) WHERE status = 'active' makes the database reject the second insert; the app maps the violation to 'already subscribed'.

How do you count events per day atomically without a lost update or duplicate-key errors?

Answer

Use an upsert: INSERT … ON CONFLICT (day) DO UPDATE SET signups = daily_stats.signups + 1 (MySQL: ON DUPLICATE KEY UPDATE). The database resolves the race in one statement.

How can you make webhook processing happen exactly once per event?

Answer

Record each processed event id under a unique index (INSERT … ON CONFLICT DO NOTHING RETURNING id) in the same transaction as the processing; if no row is returned, the event was already handled.

Open in the app to mark lessons complete, take notes and track your progress.