The difference between PgBouncer's pooling modes, why transaction mode breaks prepared statements, and which to use for Next.js apps.
PgBouncer has three pooling modes. Picking the wrong one causes silent failures that are genuinely confusing to debug.
Session mode: One client gets one server connection for the duration of its session. Behaves identically to a direct Postgres connection. Least efficient — connection count reduction is minimal.
Transaction mode: A server connection is assigned only for the duration of a transaction. Between transactions, it goes back to the pool. Most efficient — this is usually what you want.
Statement mode: Server connection assigned per statement. Breaks anything that uses multi-statement transactions. Basically unusable for most apps.
Postgres prepared statements are connection-local state. PREPARE stmt AS SELECT ... is valid only on the connection that ran it.
In transaction mode, consecutive transactions may land on different server connections. If your client prepares a statement on connection A and then tries to execute it on connection B (because A was returned to the pool), Postgres returns:
ERROR: prepared statement "s1" does not existDrizzle ORM uses prepared statements by default. With PgBouncer in transaction mode, you need to disable them:
Karanveer Singh Shaktawat
Full Stack Engineer & Infrastructure Architect
Building portfolio, contributing to open source, and seeking remote full-time roles with significant technical ownership.
Pick what you want to hear about — I'll only email when it's worth it.
Did this resonate?
A common Next.js App Router misconception: 'use client' doesn't make a file client-only — it marks the boundary where the client tree starts.
What Drizzle's migration system actually generates, why that's better than magic ORM migrations, and how to handle the edge cases.
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
// Tell pg not to use prepared statements
statement_timeout: 30000,
});
// Disable prepared statements in the pg client
const db = drizzle(pool, {
logger: false,
// Pass this to underlying pg: no prepared statements
});Or use the connection string parameter:
postgresql://user:pass@pgbouncer:6432/mydb?options=-c%20statement_timeout%3D30000Some drivers accept ?pgbouncer=true which disables prepared statements automatically.
Transaction mode, with prepared statements disabled at the driver level. You get proper connection pooling (100+ app instances sharing 10–20 Postgres connections) without the prepared statement issue.
This is the setup on TravelOre: PgBouncer in front of Postgres, Drizzle with pg driver, prepared statements off.