PostgreSQL

PostgreSQL "Prepared Statement Does Not Exist" Error in Connection Poolers (PgBouncer)

· by DebuggedIt

Quick answer

"Prepared statement does not exist" specifically under PgBouncer almost always comes down to a fundamental incompatibility between how prepared statements work...

"Prepared statement does not exist" specifically under PgBouncer almost always comes down to a fundamental incompatibility between how prepared statements work (tied to a specific backend connection) and PgBouncer's transaction or statement pooling modes, which can hand your client a genuinely different underlying Postgres connection between statements.

The Problem

An application using prepared statements, working normally against Postgres directly, fails when connecting through PgBouncer:

ERROR:  prepared statement "s1" does not exist

The exact same application code works correctly when connecting directly to Postgres without going through PgBouncer, pointing clearly at the pooling layer as the source of the incompatibility.

Why It Happens

A prepared statement is created on, and exists only within, a specific backend Postgres connection — it's not a database-wide or session-independent object. PgBouncer's pooling modes work by potentially reusing or swapping the underlying Postgres connection your client is actually talking to at different points, and depending on the mode, this can break the connection-specific nature of prepared statements:

  • Transaction pooling mode (a very common PgBouncer configuration, since it provides the best pooling efficiency) returns a Postgres connection to the pool after each transaction completes, meaning a prepared statement created in one transaction may no longer exist on whatever underlying Postgres connection your client happens to be assigned for a subsequent transaction, even though the client-side connection to PgBouncer itself appears unchanged and continuous
  • Statement pooling mode, an even more restrictive mode, can swap the underlying connection between individual statements within the same transaction, making prepared statement usage even more fundamentally incompatible
  • Session pooling mode maintains a consistent one-to-one mapping between client and backend connection for the duration of the session, which is compatible with prepared statements, but provides less efficient pooling than transaction mode, representing a genuine tradeoff between pooling efficiency and this specific compatibility

The Fix

First, confirm which pooling mode PgBouncer is actually configured with, since this determines the appropriate fix:

[pgbouncer]
pool_mode = transaction

If you're using transaction or statement pooling mode (the common, efficient choices) and need prepared statements to work reliably, the most direct fix is disabling prepared statement usage at the application/driver level specifically for connections going through PgBouncer, relying on plain, non-prepared query execution instead:

# Example: many drivers support disabling prepared statement caching explicitly
# psycopg2/psycopg3, for instance, generally use simple query protocol by default
# unless explicitly configured otherwise for prepared statement usage

# For drivers/ORMs that default to prepared statements, check for
# a configuration option to disable this specifically:
DATABASE_URL=postgresql://user:pass@pgbouncer-host:6432/db?prepareThreshold=0

The exact configuration mechanism varies significantly by language and driver/ORM — check your specific tooling's documentation for how to disable automatic prepared statement usage (sometimes called "statement caching" or similar), since this is a common enough PgBouncer compatibility concern that most mature drivers have an explicit option for it.

If prepared statements are genuinely important for your application's performance and you want to retain their benefit, switch PgBouncer to session pooling mode instead, accepting its reduced pooling efficiency as a deliberate tradeoff in exchange for full prepared statement compatibility:

[pgbouncer]
pool_mode = session

This is a meaningful architectural decision, not a purely mechanical fix — session mode requires holding a dedicated backend connection for the full duration of each client session, which reduces the total number of concurrent client connections PgBouncer can efficiently support compared to transaction mode's more aggressive connection reuse, so this tradeoff should be evaluated against your actual concurrency needs.

PgBouncer versions 1.21 and later include specific support for handling prepared statements more gracefully even under transaction pooling mode, via a feature that tracks and re-prepares statements as needed across connection handoffs — if you're on an older PgBouncer version, upgrading may provide a way to retain both transaction pooling's efficiency and reasonable prepared statement support, worth checking against your specific PgBouncer version's release notes and this feature's exact current capabilities and limitations.

Still Not Working?

If you've disabled prepared statements at the application level but still occasionally see this error, check whether your ORM or driver has multiple, separate code paths that might each need their own explicit configuration — some tools use prepared statements for certain specific operations (like bulk inserts) even when a general "disable prepared statements" setting is applied elsewhere, meaning the fix might need to be applied more comprehensively across every relevant configuration surface in your specific stack, rather than assuming a single setting change comprehensively addresses every possible code path that might attempt to use a prepared statement.