23505
PostgreSQLERRORCommonIntegrity Constraint ViolationHIGH confidence

duplicate key value violates unique constraint

What this means

Postgres attempted to insert or update a row but found that the new value already exists in a column (or combination of columns) protected by a UNIQUE index. The executor checks the index before committing the heap write and aborts the statement when a duplicate is detected.

Why it happens
  1. 1Inserting a row with a primary key or unique column value that already exists in the table
  2. 2Updating a row so that its unique column value collides with another existing row
  3. 3Bulk-loading data (COPY, INSERT ... SELECT) that contains internal duplicates or conflicts with existing rows
  4. 4Race condition: two concurrent transactions both passed an application-level uniqueness check before either committed
How to reproduce

A table with a UNIQUE constraint on the email column receives a duplicate insert.

trigger — this will error
trigger — this will error
CREATE TABLE users (
  id   SERIAL PRIMARY KEY,
  email TEXT UNIQUE NOT NULL
);

INSERT INTO users (email) VALUES ('alice@example.com');
INSERT INTO users (email) VALUES ('alice@example.com'); -- triggers 23505

expected output

ERROR:  duplicate key value violates unique constraint "users_email_key"
DETAIL:  Key (email)=(alice@example.com) already exists.

Fix 1

Use ON CONFLICT DO NOTHING

WHEN When a duplicate should be silently skipped and the existing row left unchanged.

Use ON CONFLICT DO NOTHING
INSERT INTO users (email)
VALUES ('alice@example.com')
ON CONFLICT (email) DO NOTHING;

Why this works

The executor speculatively inserts the tuple into the heap and checks the unique index. When a conflict is detected it rolls back only that speculative insertion and returns 0 rows affected, without aborting the surrounding transaction. Introduced as "upsert" infrastructure in Postgres 9.5.

Fix 2

Use ON CONFLICT DO UPDATE (upsert)

WHEN When a duplicate should overwrite or merge specific columns of the existing row.

Use ON CONFLICT DO UPDATE (upsert)
INSERT INTO users (email, updated_at)
VALUES ('alice@example.com', NOW())
ON CONFLICT (email) DO UPDATE
  SET updated_at = EXCLUDED.updated_at;

Why this works

On conflict the executor fetches the conflicting heap tuple, applies the SET expressions using the EXCLUDED pseudo-table (which contains the proposed new values), and writes an updated tuple. The unique index is then re-checked on the updated values.

What not to do

✕ Drop the UNIQUE constraint to silence the error

Removes data integrity protection and allows genuine duplicate data to accumulate, corrupting application logic.

✕ Catch the error and retry blindly in a loop

Under high concurrency this creates a busy-wait loop; use ON CONFLICT instead which is handled atomically by the executor.

Version notes
Postgres 9.5+

ON CONFLICT (upsert) syntax introduced. Earlier versions require advisory locks or CTE-based workarounds.

Sources
Official documentation ↗

src/backend/executor/nodeModifyTable.c — ExecInsert()

INSERT ON CONFLICT documentation ↗

Content generated with AI assistance and reviewed for accuracy. Found an error? hello@errcodes.dev

← All PostgreSQL errors