The ProblemYou just deployed a scaling fix by adding PgBouncer, and everything looked perfect for the first five minutes. Then, out of nowhere, your logs start screaming with this error:
ERROR: prepared statement "S_1" already exists
This usually happens when you switch a connection pooler to transaction mode. It is a classic 'works on my machine' bug because it rarely shows up during local development. You only see it once you hit a specific concurrency thresholdâoften as low as 10â20 requests per secondâwhere multiple clients begin sharing the same backend database connections.
Why This HappensThe root cause is a fundamental disagreement between your applicationâs ORM and your connection pooler about who 'owns' a database session.
The ConflictA prepared statement is a server-side object stored in a specific connection's memory. When your ORM sends a query, it often 'prepares' it first to save time on future executions. PostgreSQL assigns this plan a name, like S_1.
In transaction mode, PgBouncer is like a busy waiter. It gives you a table (connection) for one course (transaction), then immediately gives that same table to another customer. However, the waiter doesn't clear the table. Here is how the crash happens:
- App Instance A grabs a connection, creates statement
S_1, finishes its work, and leaves.- PgBouncer puts that connection back in the pool, butS_1is still sitting in that connection's memory.- App Instance B gets that same connection. It tries to define its ownS_1statement.- PostgreSQL sees the nameS_1is already taken and throws the error, failing your query.## The Debug ProcessBefore changing code, confirm your pooler settings. Check your configuration file or run this command on your server:
grep "pool_mode" /etc/pgbouncer/pgbouncer.ini
If you see pool_mode = transaction, you have found the source of the collision. While this mode allows you to handle thousands of connections with just 50â100 actual database slots, it breaks the assumption that a session belongs to only one client.
Solutions### 1. The Prisma 'pgbouncer' FlagPrisma users have it easiest. You don't need to disable features; you just need to tell Prisma it's not alone. Append ?pgbouncer=true to your connection string in your .env file:
DATABASE_URL="postgresql://user:pass@db-host:6432/mydb?pgbouncer=true"
This flag forces Prisma to use anonymous prepared statements. Since they don't have names like S_1, they can't collide.
2. Disable Prepared Statements in your ORMFor most applications, the performance gain from prepared statements is often less than 3â5%. It's rarely worth the instability. Disabling them at the ORM level is the most robust fix.
- TypeORM: Set
prepareThreshold: 0in your data source options.- Sequelize: SetminifyAliases: truein the configuration.- Go (pgx): SetPreferSimpleProtocol: truein the connection config.Here is a standard TypeORM fix:
{
type: "postgres",
host: "localhost",
port: 6432,
extra: {
max_prepared_statements: 0,
prepareThreshold: 0
}
}
3. The Modern PgBouncer Fix (v1.22.0+)If you can upgrade your infrastructure, PgBouncer 1.22.0 finally solved this natively. It can now track prepared statements for you. Add this to your pgbouncer.ini:
max_prepared_statements = 100
This allows PgBouncer to transparently handle the cleanup. If you are on an older LTS like Ubuntu 22.04, you likely have version 1.16, so you'll need to use the ORM fixes instead.
4. Last Resort: Session ModeIf your app relies heavily on session-level state and you can't change the code, switch to session mode:
[pgbouncer]
pool_mode = session
Be careful: This removes the 'multiplexing' benefit. If you have 500 app instances and only 100 DB connections, 400 of them will now fail to connect immediately.
VerificationOnce you apply the fix, don't just hope it works. Monitor your logs during a brief load test. Use a tool like wrk to hit your most database-heavy endpoint:
wrk -t12 -c40 -d30s http://localhost:3000/api/users
While that runs, watch the Postgres logs:
tail -f /var/log/postgresql/postgresql-main.log | grep "already exists"
If the test finishes and the logs are silent, your pooling issues are solved.

