Report #47603
[bug\_fix] PgBouncer prepared statement 'xxx' does not exist
Either switch PgBouncer to session pooling mode \(pool\_mode=session\), or disable prepared statements in the application/ORM \(e.g., Django CONN\_MAX\_AGE=0 with DISABLE\_SERVER\_SIDE\_CURSORS, or SQLAlchemy execution\_options=\{'prepare\_threshold': None\}\). Root cause is that PgBouncer in transaction pooling mode shares one server connection across multiple client transactions, but prepared statements are stored per-backend session state; when the client gets a different backend on the next transaction, the prepared handle is invalid.
Journey Context:
A Django application works perfectly when connecting directly to PostgreSQL. After adding PgBouncer \(transaction mode\) for scaling, endpoints randomly throw DatabaseError: prepared statement 'pq\_123' does not exist. Investigation reveals Django's database wrapper uses prepared statements \(server-side cursors\) for QuerySet iteration to save memory. With PgBouncer transaction mode, the flow is: \(1\) Client sends PREPARE pq\_123, PgBouncer assigns Backend A, prepares it. \(2\) Client sends EXECUTE pq\_123, PgBouncer assigns Backend B \(A is busy\), Backend B has no pq\_123, throws error. The initial instinct is to enable server\_reset\_query in PgBouncer to clear prepared statements, but this is inefficient and racy. The correct debugging path checks PgBouncer's pool\_mode \(shows 'transaction'\), realizes the stateful nature of prepared statements conflicts with stateless transaction pooling. Solutions are: \(1\) Change PgBouncer to session mode, preserving session state but losing multiplexing efficiency, or \(2\) Disable prepared statements in Django via OPTIONS=\{'prepare\_threshold': None\} in DATABASES config, trading slightly more CPU for compatibility with transaction pooling.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-19T10:22:48.872292+00:00— report_created — created