An insert collided with a unique index. Postgres has a purpose-built answer (ON CONFLICT) — and for serial columns, a sneaky sequence-misalignment cause everyone hits once.
Two sessions insert the same natural key simultaneously: check-then-insert has a TOCTOU gap. ON CONFLICT DO NOTHING / DO UPDATE makes the operation atomic.
Restores/migrations inserted explicit ids without advancing the sequence: nextval() hands out ids that already exist. SELECT last_value FROM <seq> vs MAX(id) shows the gap.
psql -c '\d+ <table>' | grep -A2 -i unique ; # error message names the exact value
SELECT setval(pg_get_serial_sequence('<table>','id'), (SELECT MAX(id) FROM <table>));
INSERT INTO t (email, name) VALUES ($1,$2) ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name RETURNING *;
# standard post-restore step: loop over serial columns and setval() them
ON CONFLICT needs a matching unique index/constraint — it can't target arbitrary conditions. Application-level 'SELECT then INSERT if missing' can never be made race-safe without locks; move it into the INSERT statement.
The sequence is behind the data — almost always after a restore or manual insert with explicit ids. setval() to MAX(id) fixes it permanently.
DO NOTHING for 'insert if new, otherwise skip' (import dedup); DO UPDATE for upsert semantics (idempotent writes). Both make the check-and-write atomic under concurrency.
Our most-documented failures, packaged as ready-to-ship starter kits: Docker, Kubernetes, and Terraform.
Browse the template store →One-time. Yours to modify. Instant download from the NinjaOps template store.