PostgreSQL error: no unique or exclusion constraint matching the ON CONFLICT specification

ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification

INSERT ... ON CONFLICT needs an arbiter: a unique (or exclusion) constraint that defines what "conflict" means. The conflict target you wrote must match one exactly — same column set, same expression, same partial predicate. No match, no upsert.

What this error means

PostgreSQL does not guess which constraint you mean. ON CONFLICT (email) requires a unique index on exactly (email):

Common causes

How to diagnose it

\d users   -- lists indexes and constraints with their exact definition

Compare the conflict target in the failing INSERT against the index definitions, checking columns, expressions and WHERE clauses literally.

How to fix it

-- Missing constraint → create it (in production: CONCURRENTLY)
CREATE UNIQUE INDEX CONCURRENTLY users_email_key ON users (email);

-- Expression index → the target must repeat it:
INSERT INTO users (email) VALUES ('[email protected]')
ON CONFLICT (lower(email)) DO NOTHING;

-- Partial index → the target must repeat the predicate:
INSERT INTO users (email) VALUES ('[email protected]')
ON CONFLICT (email) WHERE deleted_at IS NULL DO NOTHING;

Alternatively name the constraint directly — ON CONFLICT ON CONSTRAINT users_email_key — though the column form survives constraint renames better.

🔍 The error ON CONFLICT exists to prevent: duplicate key value violates unique constraint.
🔍 Related reading: Choosing the right index — unique, partial and expression indexes.