Skip to content

Matches page titles and page text. Forty pages, indexed at build time.

Pooled vs direct

Transaction pooling is the default. The complete list of what needs the direct endpoint, including the one thing widely believed not to work and does.

Every service has a pooled endpoint and a direct endpoint. They share one hostname and differ only by port, and the lower of your two ports is the pooled one. Both are on your service's Connect tab.

Your ports are your service's own. Services in a location share their hosts' addresses and are told apart by port, so a second service does not get the same pair as the first. They do not change for the life of the service, including when we move it to another host.

Use pooled for application traffic. Use direct for anything that keeps state in a session.

How to choose

  1. List the things your application does that outlive a single transaction. The table below is the complete set.
  2. Point those consumers at your direct port, the higher of the two.
  3. Point everything else at your pooled port, the lower one.

Using both endpoints from one application is normal. A common shape:

  • request handling on pooled
  • the LISTEN consumer for your job queue on direct
  • long-running analytical or maintenance work on direct, so a slow query does not occupy a pooled server connection for minutes

What transaction pooling does

A server connection is assigned to your client for the duration of a transaction and returned to the pool at commit. Between transactions your client does not hold a server connection.

That is the benefit: an application with hundreds of mostly-idle connections occupies server connections only while it is executing. It is also the source of every incompatibility below, all of which have the same shape. Anything scoped to a session has no session to live in.

What works on the pooled endpoint

FeaturePooledNote
Ordinary queries and transactionsYesThe case pooling exists for
Protocol-level prepared statementsYesSupported in transaction mode with a statement cache
SET LOCAL inside a transactionYesTransaction-scoped
Advisory locks taken and released in one transactionYesTransaction-scoped advisory locks are fine
Cursors inside a transactionYesWithout WITH HOLD

Prepared statements are worth stating explicitly. Older guidance says they do not work in transaction pooling and a lot of documentation still repeats it. Current PgBouncer supports protocol-level prepared statements in transaction mode when a prepared-statement cache is configured. Documenting them as broken would push you onto the direct endpoint for no reason. Confirm the behaviour against your own workload before you rely on it.

What needs the direct endpoint

  • Session-level SET or RESET outside a transaction
  • LISTEN and NOTIFY
  • Session-level advisory locks
  • WITH HOLD cursors
  • Temporary tables that persist across transactions
  • SET SESSION AUTHORIZATION and session-scoped role changes
  • Session state assumed by some ORMs and job frameworks

The last item is the one that causes support tickets. A framework that sets a session variable at checkout and reads it in a later transaction is relying on session affinity it does not have. It does not error. It reads the default value, and the bug surfaces somewhere else entirely.

Where the pooler runs

One PgBouncer per service, inside that service's own cgroup and under its own user, terminating TLS and forwarding to PostgreSQL over a local Unix socket. Not one pooler per host: that would put several tenants inside one process, make one tenant's connection storm everybody's problem, and give every service on the machine a shared restart to wait for.

The consequence you can see: the pooler's memory and CPU are charged to your plan allocation, and they are included in the configuration profile for your plan rather than pretended away. The consequence you cannot see, and benefit from anyway: a pooler failure affects one service, and no other tenant shares the process.

Sizing your own pool

Size your client pool against your plan's max_connections, which is a hard ceiling set by the platform and listed per plan on plan limits. Pool sizes on our side derive from the same figure, set so the pooled path cannot consume every backend slot and leave the direct endpoint unusable.

Troubleshooting

A session variable reads as its default. The consumer is on the pooled endpoint and is relying on session affinity. Move it to the direct port.

LISTEN never receives anything. Same cause. LISTEN is session-scoped and needs the direct endpoint.

A WITH HOLD cursor is empty after the transaction commits. Same cause, same fix.

sorry, too many clients already. You have reached max_connections. See error messages.