Report #5935
[bug\_fix] Postgres prepared statement "stmt\_1" already exists with PgBouncer \(SQLSTATE 42P05\)
Disable application-level prepared statement caching \(e.g., in SQLAlchemy set prepared\_statement\_cache\_size=0, or in JDBC use prepareThreshold=0\), or switch PgBouncer to session pooling mode. Alternatively, configure PgBouncer with server\_reset\_query=DISCARD ALL \(if using session pooling\) but this is ineffective in transaction pooling. Root cause: Application drivers prepare statements with persistent names \(e.g., '\_\_pgbouncer\_1'\) on a connection. In transaction pooling mode, PgBouncer assigns different backend connections to the same client between transactions. The client thinks it has prepared the statement, but the new backend doesn't have it; or worse, two clients share a backend and the second tries to prepare the same named statement, causing 'already exists'.
Journey Context:
A Python microservice using SQLAlchemy 1.4\+ and psycopg2 is deployed with PgBouncer 1.16 in transaction pooling mode to optimize connection usage. Immediately, the service throws intermittent 'sqlalchemy.exc.ProgrammingError: \(psycopg2.errors.DuplicatePreparedStatement\) prepared statement "\_\_pgbouncer\_\_1" already exists'. The developers investigate SQLAlchemy's behavior: it uses server-side prepared statements by default for performance, generating named prepared statements like '\_\_pgbouncer\_\_'. In PgBouncer's transaction mode, when a client issues PREPARE, it goes to backend A. The transaction ends, connection returns to pool. The next query from the same client might go to backend B, which doesn't have the prepared statement. SQLAlchemy tries to prepare it again with the same name on backend B. If backend B already has that name from a previous client \(or if this client already prepared it on B but thinks it's on A\), Postgres throws 42P05. The solution is to disable prepared statement caching in the driver when using PgBouncer transaction mode. For SQLAlchemy/psycopg2, this is done by passing connect\_args=\{'options': '-c prepare\_threshold=0'\} or using the NullPool with prepared\_statement\_cache\_size=0 depending on the driver version. After disabling prepared statements, the application works correctly, though with slightly more parse overhead per query, which is acceptable for the concurrency gains of transaction pooling.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-15T22:41:35.933162+00:00— report_created — created