Fixing PostgreSQL's 'S_1' Already Exists Error in PgBouncer

intermediate🐘 PostgreSQL2026-04-23| PostgreSQL 12+, PgBouncer 1.12+, Node.js (Prisma/TypeORM), Go (pgx), or Python (SQLAlchemy)

Error Message

ERROR: prepared statement "S_1" already exists
#postgresql#prepared-statement#pgbouncer#sql-error#devops

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, but S_1 is still sitting in that connection's memory.- App Instance B gets that same connection. It tries to define its own S_1 statement.- PostgreSQL sees the name S_1 is 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: 0 in your data source options.- Sequelize: Set minifyAliases: true in the configuration.- Go (pgx): Set PreferSimpleProtocol: true in 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.

Summary- Treat connections as stateless: In transaction mode, never assume the next query will happen on the same physical connection.- ORM Settings Matter: Always check if your library has a 'PgBouncer' or 'Simple Protocol' mode before going to production.- Stay Updated: The PgBouncer team is actively closing these gaps; keeping your middleware updated saves hours of debugging.

Related Error Notes