PostgreSQLRunbook 0/4 done
23505

PostgreSQL Error 23505: Unique violation

Error✕ Not retryable · fix the payloadHTTP 409 ConflictDatabase
TL;DR · 10-second read

An INSERT or UPDATE produced a value that already exists in a column covered by a UNIQUE constraint or index. Use INSERT ... ON CONFLICT, or return 409 to the client.

Identify: does your log match?

What you see · exact log / response
ERROR:  duplicate key value violates unique constraint "users_email_key"
DETAIL:  Key (email)=(ada@example.com) already exists.
  1. High

    Check-then-insert race condition

    Two requests both SELECT, see no row, then both INSERT. The second one hits the unique index.

  2. Medium

    Sequence out of sync after import

    Rows were bulk-loaded with explicit ids, so the serial sequence still returns ids that already exist. Every new INSERT collides on the primary key.

  3. Medium

    Retried request without idempotency

    A client or queue retried a successful INSERT after a timeout. The original write already committed.

Fix

Use an atomic upsert instead of check-then-insert

✕ Broken · SQL
SELECT id FROM users WHERE email = $1;
-- app code: if no row, then...
INSERT INTO users (email, name) VALUES ($1, $2);
✓ Fixed · SQL
INSERT INTO users (email, name)
VALUES ($1, $2)
ON CONFLICT (email)
DO UPDATE SET name = EXCLUDED.name
RETURNING id;

ON CONFLICT resolves the race inside the database in a single statement. Use DO NOTHING if you only want to skip duplicates.

Verify: reproduce it on purpose

Reproduce locally · psql
$ psql -c "CREATE TEMP TABLE u(email text UNIQUE); INSERT INTO u VALUES ('a@x.io'),('a@x.io');"

Run it against a dev environment to trigger 23505 on purpose and test your error handler.

Prevent

  • Enforce uniqueness in the database, never only in app code.
  • Map 23505 to HTTP 409 with the constraint name from err.constraint.
  • Send an Idempotency-Key on retried POSTs.

FAQ

How do I get the constraint name from 23505 in node-postgres?

The pg error object has err.code === '23505' and err.constraint (e.g. users_email_key). err.detail contains the conflicting key.

Does ON CONFLICT DO NOTHING still consume a sequence value?

Yes. nextval() is evaluated before the conflict check, so gaps appear in serial ids. That is expected and harmless.

Is 23505 safe to retry?

Not as-is: the same values will collide again. Retry only after changing the input or switching to an upsert.

Related errors

Last verified 2026-10-18 · Source: Official Appendix A error codes (errcodes.txt in the postgres repo) · Official docs ↗ · ~12,100 searches/mo