What it means
A prepared statement is a query the server has parsed and stored under a name, so it can be run again with new values. Names belong to one session (one server connection) and last until it ends. Creating a second one with a name the session already uses fails:
ERROR: prepared statement "stmtcache_1" already exists
Its twin, prepared statement "stmtcache_2" does not exist (code 26000), is running a name the
session never prepared.
Drivers prepare statements for you, with generated names (S_1, stmtcache_…, _pg3_0, a1…), and
remember which ones they’ve created on their connection. Both errors mean that memory and the
server disagree, and the usual reason is that the “connection” the driver holds isn’t always the
same server session.
Common causes
- PgBouncer (or another pooler) in transaction mode. Each transaction may run on a different
server connection. A driver prepares
S_1on one, and later another client whose driver also names its first statementS_1lands on that connection: “already exists”. Or a driver runsS_1on a connection where it was never prepared: “does not exist”. - A pooler that resets sessions (
DISCARD ALLwhen a client disconnects), so statements the driver cached are gone. - Your own
PREPARErun twice in the same session, such as a script executed again in an openpsqlor a pooled connection. - A connection reused after an error in application code that tracks statement names itself.
How to fix it
For your own PREPARE
DEALLOCATE get_invoice; -- or DEALLOCATE ALL;
PREPARE get_invoice (integer) AS SELECT id, total FROM invoices WHERE id = $1;
To see what the current session has prepared:
SELECT name, statement FROM pg_prepared_statements;
DISCARD ALL clears prepared statements along with the rest of the session state.
Behind PgBouncer in transaction mode
Choose one:
- Let PgBouncer track them. PgBouncer 1.21 and later support protocol-level prepared statements
in transaction mode when
max_prepared_statementsis set above zero. Statements made with SQLPREPAREstill aren’t supported there. - Stop the driver preparing named statements. pgJDBC:
prepareThreshold=0. psycopg 3:prepare_threshold=Noneon the connection. Other drivers and ORMs have a similar switch, often described in their notes on PgBouncer. - Use session mode for the clients that need prepared statements, or connect them to PostgreSQL directly.
Check you really are behind a pooler
Run SELECT pg_backend_pid(); twice, in separate transactions, on the same client connection. If
the number changes, a pooler is switching server sessions under you.
Reproduce it
On PostgreSQL 18.6, with SQL PREPARE:
PREPARE get_invoice (integer) AS SELECT id, total FROM seo_err_pg.invoices WHERE id = $1;
PREPARE get_invoice (integer) AS SELECT id, total FROM seo_err_pg.invoices WHERE id = $1;
EXECUTE nope(1);
ERROR: prepared statement "get_invoice" already exists
ERROR: prepared statement "nope" does not exist
Driver-style, through the extended protocol: psql 18’s \parse creates a named prepared statement
the way a driver does, and \bind_named runs one.
SELECT id, total FROM seo_err_pg.invoices WHERE id = $1 \parse stmtcache_1
SELECT id, total FROM seo_err_pg.invoices WHERE id = $1 \parse stmtcache_1
ERROR: prepared statement "stmtcache_1" already exists
\bind_named stmtcache_2 1 \g
ERROR: prepared statement "stmtcache_2" does not exist
After DEALLOCATE get_invoice, a second DEALLOCATE get_invoice gave
prepared statement "get_invoice" does not exist. With \set VERBOSITY verbose, psql shows the
codes: ERROR: 42P05: prepared statement "s1" already exists and
ERROR: 26000: prepared statement "nope" does not exist. We didn’t run PgBouncer for this page; its
behaviour is from its documentation.
In Inlet
Inlet’s Activity monitor lists the server’s sessions, so you can see how many connections your application, or the pooler in front of it, holds. When a statement fails, Inlet shows the server’s error; with your own Anthropic API key, Ask Claude (Pro, ⌘L) can fix it, sending the schema, the SQL and the error, never rows.