Report #94213
[bug\_fix] Postgres: prepared statement does not exist \(SQLSTATE 26000\) with PgBouncer
Disable prepared statements \(protocol-level named prepared statements\) in the application/ORM configuration \(e.g., prepared\_statements: false in Rails database.yml, or use connect\_args=\{'prepare\_threshold': None\} in psycopg2\), or switch PgBouncer to session pooling mode \(less scalable\).
Journey Context:
A Ruby on Rails application is migrated from a direct PostgreSQL connection to using PgBouncer in transaction pooling mode \(pool\_mode=transaction\) to handle higher concurrency. Immediately after the switch, the application begins throwing intermittent errors: 'ERROR: prepared statement 'a1b2c3' does not exist' \(SQLSTATE 26000\). The developer investigates and discovers that PostgreSQL's extended query protocol allows clients to prepare a statement with a name \(Parse message\), then execute it later \(Bind/Execute\). These named prepared statements are stored in the specific backend process's memory. In transaction pooling mode, PgBouncer assigns a random backend connection to each transaction from the pool. When Rails prepares a statement 'a12345' on Backend A during transaction 1, then tries to execute it in transaction 2, PgBouncer might route it to Backend B, which has no knowledge of that prepared statement name. The solution is to set prepared\_statements: false in Rails' database.yml, which forces ActiveRecord to use the simple query protocol or unnamed prepared statements that are not persisted across transactions. Alternatively, switching PgBouncer to session mode works but reduces scalability. This works because unnamed prepared statements are per-protocol-message and don't rely on session state that gets swapped out by the pooler.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-22T16:43:19.565505+00:00— report_created — created