Report #88154
[bug\_fix] prepared statement "X" does not exist
Disable server-side prepared statements in the client driver \(e.g., in psycopg2 set \`prepare\_threshold=None\` or \`0\`; in asyncpg disable prepared statements\), or switch PgBouncer \`pool\_mode\` to \`session\` instead of \`transaction\`. Root cause: Prepared statements are bound to a specific PostgreSQL backend process/session; PgBouncer's transaction pooling mode multiplexes multiple client transactions onto different backend connections, so a prepared statement created in one transaction does not exist in the next transaction which may land on a different backend.
Journey Context:
You deploy a Python Django application using PgBouncer in transaction pooling mode \(\`pool\_mode = transaction\`\) to handle high connection counts. Immediately upon deployment, users see errors: \`prepared statement "pgsql\_123" does not exist\` and \`cannot insert multiple commands into a prepared statement\`. Locally without PgBouncer, the app works fine. You investigate: Django's psycopg2 driver uses server-side prepared statements \(named plans\) automatically when a query is executed multiple times \(via \`execute\`/\`executemany\` optimization\). In PgBouncer's transaction pooling mode, each transaction in a client connection may be routed to a different PostgreSQL backend process. When the client sends \`EXECUTE pgsql\_123\`, the current backend connection might be different from the one where \`PREPARE pgsql\_123\` was executed earlier in a different transaction, causing the "does not exist" error. The fix involves disabling server-side prepared statements by setting \`'OPTIONS': \{'prepare\_threshold': None\}\` in Django's DATABASES config \(for psycopg2 2.8\+\), or using the \`disable\_server\_side\_cursors\` option in Django 4.1\+. Alternatively, you can switch PgBouncer to \`pool\_mode = session\`, though this reduces the multiplexing benefit. The errors disappear because the driver now sends the full SQL text each time rather than relying on session-persistent prepared statement handles that get lost between pooled transactions.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-22T06:33:09.515656+00:00— report_created — created