Most people meet connection pooling the hard way: the app runs fine in testing, then falls over in production with FATAL: sorry, too many clients already. The database didn't run out of CPU or disk — it ran out of connections. Pooling is the fix, and on WattleDB it's already set up for you. This explains what's happening and how to use it.

Which connection string, at a glance

  • DATABASE_URLpooled (PgBouncer, transaction mode). Your app, your API routes, serverless functions.
  • DIRECT_URLdirect session. Migrations, psql, long-running jobs, anything using session state.
  • Both are TLS-encrypted Australian endpoints — keep sslmode=require.

Connections are expensive

In PostgreSQL, every client connection is backed by its own server process with its own memory. A few dozen is fine; a few thousand is not. Each database has a max_connections ceiling, and once you hit it, new connections are refused — even though the server has plenty of CPU left. The problem isn't load, it's concurrency of connections.

What a pooler does

A connection pooler like PgBouncer sits between your app and Postgres. It keeps a small pool of real database connections open and lends them out to incoming clients. In transaction mode — what WattleDB's pooled endpoint uses — a real connection is assigned only for the duration of a single transaction, then returned to the pool. Thousands of clients can share a handful of backend connections, because at any instant only a few are mid-transaction.

That's why the pooled DATABASE_URL is the right default for your application: it absorbs spikes that would otherwise exhaust the database.

When you need the direct connection

Transaction-mode pooling has one trade-off: because a connection isn't dedicated to you between transactions, anything that relies on session state won't work through the pooler. That includes:

For those, use DIRECT_URL — a direct session connection that bypasses the pooler. The usual pattern: your app's runtime uses DATABASE_URL, and your migration tool uses DIRECT_URL.

The serverless trap

Serverless and edge functions make the connection problem worse: each invocation can spin up its own instance, and each instance wants its own connection. A burst of traffic becomes a burst of connections, and you hit the ceiling fast. This is exactly what transaction-mode pooling is for — point your serverless functions at DATABASE_URL and the pool soaks up the churn.

One gotcha: with some drivers you must disable prepared statements when going through a transaction pooler (for example, prepare: false in postgres.js). Prepared statements assume a stable session, which transaction mode doesn't guarantee.

Rules of thumb