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()
@Transactional
public Subscription subscribe(long customerId, String plan) {
if (subscriptions.existsByCustomerIdAndStatus(customerId, ACTIVE)) throw new AlreadySubscribedException();
return subscriptions.save(new Subscription(customerId, plan, ACTIVE));
}
const existing = await prisma.subscription.findFirst({ where: { customerId, status: "ACTIVE" } });
if (!existing) await prisma.subscription.create({ data: { customerId, plan, status: "ACTIVE" } });
Reproduce it
Check-then-insert in application code, interleaved by two requests:
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:
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
-- no partial indexes: use a nullable generated column that is only set for active rows
ALTER TABLE subscriptions ADD active_customer_id BIGINT
GENERATED ALWAYS AS (IF(status = 'active', customer_id, NULL)) STORED,
ADD UNIQUE KEY one_active (active_customer_id);
INSERT INTO daily_stats (day, signups) VALUES (CURRENT_DATE, 1) AS new
ON DUPLICATE KEY UPDATE signups = daily_stats.signups + new.signups;
try:
with transaction.atomic():
sub = Subscription.objects.create(customer=customer, plan=plan, status="active")
except IntegrityError:
raise AlreadySubscribed() # the index said no
DailyStat.objects.bulk_create(
[DailyStat(day=day, signups=1)],
update_conflicts=True, unique_fields=["day"], update_fields=["signups"]) # simple upsert (sets, not adds)
# for "add 1", use raw SQL ON CONFLICT … SET signups = daily_stats.signups + 1
try {
return subscriptions.saveAndFlush(new Subscription(customerId, plan, ACTIVE));
} catch (DataIntegrityViolationException e) {
throw new AlreadySubscribedException();
}
DB::table('daily_stats')->upsert(
[['day' => today(), 'signups' => 1]], ['day'], ['signups' => DB::raw('daily_stats.signups + 1')]
);
await prisma.dailyStat.upsert({
where: { day }, create: { day, signups: 1 }, update: { signups: { increment: 1 } },
});
try { await prisma.subscription.create({ data }); }
catch (e: any) { if (e.code === "P2002") throw new AlreadySubscribed(); throw e; } // unique violation
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 UPDATEfor 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.