Web development

Connection pools: real timings

Reusing a connection costs 0.191 ms against 0.598 ms on loopback: a pool earns its keep when the network has latency, not before.

Key points

  • On loopback, 200 sequential requests cost 0.598 ms with a fresh connection and 0.191 ms with a keep-alive pool.
  • A burst of 200 requests at concurrency 5 drops from 108.2 ms to 26.6 ms with the pool.
  • A single loopback TCP handshake measures 0.305 ms: over a network the round-trip time is added several times.
  • PostgreSQL uses a process per connection, and max_connections defaults to 100 in the usual case.

A connection pool exists so you do not pay for opening a connection on every request. Measured on loopback over 200 sequential requests, a fresh connection costs 0.598 ms and one reused from a keep-alive pool 0.191 ms: three times as much, with the server doing nothing different.

What opening a connection costs

Opening a connection means at least three steps: the TCP handshake, authentication and session setup. PostgreSQL adds a process per connection, so the cost is not only network but memory and scheduler time. The PostgreSQL 18 documentation, read on 1 November 2026, notes that max_connections is typically 100 and that raising it increases shared memory allocation: a pool is not optional, it is how you stay under that number.

The numbers, and what they mean

The measured saving is 0.407 ms per request on loopback, where the network adds no latency. On a link with 20 ms round-trip time the same handshake costs at least three times that latency, so the gap grows by an order of magnitude. The burst shows it plainly: 26.6 ms against 108.2 ms, four times less time for the same work.

How many connections to keep open

Start from the cores available to the database and multiply by the application instances: the total must stay below max_connections minus the superuser slots, which default to three. A pool of 20 connections across five instances is already a hundred connections, the limit of a default PostgreSQL. The command below shows the ceiling and the real usage.

psql "$DATABASE_URL" -Atc "show max_connections; show superuser_reserved_connections;"
psql "$DATABASE_URL" -Atc "select state, count(*) from pg_stat_activity group by state order by 2 desc;"

A pool is not an infinite queue

When every connection is busy, requests wait. Without an acquisition timeout the wait grows past the client timeout, and the service looks slow instead of looking saturated. Set a maximum wait shorter than the endpoint timeout, a statement timeout and a metric on waits: pool latency must be visible before it becomes an incident.

The limit: loopback has no latency

This measurement is the floor, not the ceiling. On loopback the round-trip time is close to zero, so the pool gains little; on a real network it gains much more, but no pool shortens a slow query. If a query takes two seconds, the connection stays busy for two seconds: a pool spreads the wait, it does not remove it. In a serverless environment the shape changes, because each instance opens its own connections and the pool has to move to the database.

Where to start

    Frequently asked questions

    How many connections should a pool have?

    Few, tied to the database cores. A starting point is the core count multiplied by the application instances, and the total must stay below max_connections minus the reserved slots.

    PgBouncer or the application pool?

    They do different jobs. The application pool cuts opens per process, PgBouncer shares connections across processes and instances. With many short-lived instances, as in serverless, PgBouncer or a managed pooler becomes necessary.

    Why does a pool not speed up queries?

    Because it only removes the cost of opening. Execution time depends on the plan, the indexes and the data; a pool full of slow queries stays full, and the wait moves to the queue.

    What happens when the pool is full?

    Requests wait for a free connection. Without an acquisition timeout the wait exceeds the client timeout and the service looks broken; with a short timeout you get a fast error you can measure.

    Do I need a pool with SQLite?

    Not in the PostgreSQL sense. SQLite opens a file, not a process per client, and in WAL mode it allows concurrent readers with a single writer. The limit to manage is the write queue, not the connection count.