A Prisma + PgBouncer setup using transaction mode. Random queries failing with 'prepared statement does not exist'. Connection leak suspected. Real cause: transaction-mode incompatibility with prepared statements.
The vulnerable diff
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // points at PgBouncer port 6432, txn mode
}
// app log:
// PrismaClientUnknownRequestError: prepared statement "s1" does not exist
// when subsequent queries land on a different physical connection.What is wrong
PgBouncer has three pooling modes: session (one client = one connection), transaction (one client gets a connection only for the duration of a transaction), and statement (one connection per statement). Transaction mode is the most efficient but incompatible with anything that depends on connection state — including prepared statements, session variables, advisory locks. Prisma (and many Postgres drivers) use prepared statements aggressively; under transaction mode, the prepared statement is created on one connection and the next query lands on a different connection where it doesn't exist. The fix is to either use session mode (less efficient), tell the driver to skip prepared statements (`?pgbouncer=true` in Prisma), or disable PgBouncer entirely.
The attack
Symptom:
# Random query failures, no pattern:
PrismaClientUnknownRequestError: prepared statement "s1" does not exist
PrismaClientUnknownRequestError: prepared statement "s2" does not exist
# Initially blamed on connection leaks. Actually a config mismatch.Hours of debugging usually.
Mesrai's review comment
mesraipilot · Bot · reviewed 2 min ago
[mesrai] [code-review] [Infrastructure] [PgBouncer] [medium]
PgBouncer transaction mode is incompatible with prepared statements.
Prisma uses prepared statements by default.
Three options:
1. Tell Prisma to skip prepared statements (recommended for serverless):
DATABASE_URL="postgres://...:6432/db?pgbouncer=true&connection_limit=1"
2. Use PgBouncer session mode (uses connections less efficiently):
# pgbouncer.ini: pool_mode = session
3. Skip PgBouncer for Prisma; use a separate connection pool
(pg-pool, prisma's built-in) and let PgBouncer handle other
clients.
Reference: Prisma Docs § Connection management for PgBouncer.The fix
// .env — fixed
DATABASE_URL="postgresql://app:pwd@pgbouncer:6432/app?pgbouncer=true&connection_limit=1"
// prisma/schema.prisma — unchanged
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}`?pgbouncer=true` tells Prisma not to use prepared statements. `connection_limit=1` recommended for serverless where each function instance has its own connection budget.
Why human review missed it
PgBouncer config bugs are subtle because Postgres docs and PgBouncer docs live in different places. Mesrai catches Prisma/PgBouncer combos without the right URL parameters.
Related rules + further reading
Mesrai rule pack: infrastructure/pgbouncer-prepared-statements — flags Prisma + PgBouncer config mismatches.
PgBouncer docs: Pooling modes.
Prisma docs: Connection management for serverless.
Takeaway
Transaction-mode PgBouncer = no prepared statements. `?pgbouncer=true` in URL. Or session mode. Mesrai catches the mismatch.