Connection Pooling — PgBouncer for PostgreSQL & MySQL
TL;DR — A managed connection pool lets thousands of application connections share a small fixed number of real database connections. Open the Pools tab on a PostgreSQL or MySQL cluster, click Create Pool, pick a mode and size, and use the pool's URI in your app.
Connection pooling is supported on:
- ✅ PostgreSQL (via PgBouncer)
- ✅ MySQL (via the equivalent pooler)
- ❌ MongoDB, Valkey, Kafka, OpenSearch — they pool natively in the client driver
Why Use a Pool
Each database connection costs memory and a backend process. A Starter cluster supports a low double-digit number of concurrent connections; even an HA cluster maxes out in the low hundreds. If your application opens one connection per request and you run many app servers, you'll hit too many connections errors quickly.
A pool multiplexes:
- App servers → connect to the pool (a lightweight TCP service) with no per-connection cost
- Pool → maintains a small fixed set of real connections to the database, handing them out as needed
- One pool can handle up to 5,000 concurrent client connections with a backend of just 10–50 real connections
Result: fewer database resources used, more app servers supported, no too many connections errors.
Pool Modes
| Mode | When Connection Is Returned to the Pool | Use Case | Caveats |
|---|---|---|---|
| Transaction | After every COMMIT / ROLLBACK | Most web apps (recommended default) | Session-level features unavailable: prepared statements, SET, advisory locks, LISTEN/NOTIFY |
| Session | When the client disconnects | Apps relying on session state (e.g. SET search_path, prepared statements) | Pool fills up faster — needs more backend connections |
| Statement | After every statement | Maximum concurrency for autocommit-only workloads | No multi-statement transactions; rarely needed |
Start with Transaction. Most ORMs (Prisma, Sequelize, Django, Rails ActiveRecord, SQLAlchemy) work fine in transaction mode as long as you don't rely on session-scoped state.
Create a Pool
- Open your cluster → Pools tab
- Click Create Pool
- Fill in:
| Field | Description |
|---|---|
| Pool name | Friendly identifier (e.g. myapp-transaction) |
| Mode | transaction, session, or statement (default: transaction) |
| Size | Number of real backend connections the pool will hold open (typically 10–25 for Starter, 25–100 for HA) |
| User | The database user the pool will authenticate to the database as |
| Database | The logical database the pool serves |
- Click Save
The pool appears in the list with its own URI you can copy.
Use the Pool in Your App
Replace your direct database URI with the pool URI (visible on the pool row's copy button). It looks the same as a normal connection URI but ends with the pool's port.
# Before — direct connection postgresql://doadmin:PWD@host:25060/defaultdb?sslmode=require # After — through the pool postgresql://doadmin:PWD@host:25061/myapp-transaction?sslmode=require
Update your application's DATABASE_URL environment variable and redeploy. Your app code does not change.
Sizing the Pool
A common rule of thumb: pool size = (cluster's max connections) − (a small reserve for migrations and ad-hoc access). With a Starter cluster (≈ 25 max connections), a pool size of 15–20 is sensible. With an HA cluster (≈ 150 max), 100 is typical.
If your app opens its own pool too (e.g. Prisma's default of 10 per app server), multiply by app server count:
- 4 app servers × 10 client-side pool = 40 simultaneous server-side connections expected
- Pool size should be ≥ 40 — but the great trick of PgBouncer is that client-side pools can be much larger than server-side pools (transaction mode), so 4 app servers × 50 client-side pool, with server-side pool 15, works perfectly
Transaction Mode Caveats
If you use Transaction mode (the default), avoid:
- Prepared statements that span transactions. Most modern drivers handle this automatically by re-preparing on each connection (Prisma, pg, mysql2 all do).
SETstatements outside a transaction. Wrap them inBEGIN ... COMMITor use database-level configuration.LISTEN / NOTIFYin PostgreSQL — needs Session mode.- Advisory locks across statements — needs Session mode.
- Temporary tables that need to outlive a single transaction — needs Session mode.
If any of these break, create a second pool in Session mode for the workload that needs it, and use the transaction pool for everything else.
Delete a Pool
Click the trash icon on the pool row. Update your application config first — any connection currently using the pool will fail when it's removed.
Monitoring Pools
The pool itself is transparent — there's no separate metrics tab for pools. To monitor:
- Watch the Active Connections chart on the Insights tab — it should now hover near the pool size, not the per-app-server count
- Look for
pgbouncerlines in the cluster's logs if you've enabled log forwarding
Troubleshooting
"prepared statement does not exist"
You're hitting Transaction mode's caveat. Either switch your ORM to disable cross-transaction prepared statements, or create a Session-mode pool.
"no such database" when connecting to the pool
The pool's database parameter must match the logical database you created. The pool URI uses the pool name as the database segment of the URI — but internally it maps to the database you configured.
Pool fills up under load (server connections busy)
Increase the pool size, or convert the pool from Session mode to Transaction mode. Inspect the cluster's Active Connections chart to see whether you're hitting cluster limits too.
App connections still spiking on the cluster
Make sure the app is actually using the pool URI, not the direct URI. Check DATABASE_URL on the running app server.