Architecture
What pgagroal actually does between your application and PostgreSQL.
Two halves of every connection
Every connection through pgagroal has two halves that do not map one-to-one:
- -- A client connection: a TCP socket from your application to pgagroal on port
6432. - -- A backend connection: a TCP socket from pgagroal to PostgreSQL on port
5432, drawn from a pool.
The asymmetry is the point. You can have hundreds of client connections sharing tens of backend connections. PostgreSQL only sees the backend side, so its load is determined by the pool size, not by how many clients are connected.
Where state lives
pgagroal is deliberately thin. Almost no state lives in the pooler; it routes bytes between client and backend. The location of state is what determines what survives a backend swap under transaction pooling.
| State | Where it lives | Survives a backend swap? |
|---|---|---|
| Tables, rows, indexes | PostgreSQL on disk | Yes |
| Open transaction | A specific backend session | N/A — pgagroal never swaps mid-transaction |
Session settings (SET), prepared statements, temp tables, LISTEN | A specific backend session | No (under transaction pipeline) |
| Authentication, current user, current database | A specific backend session, established at login | Yes — pgagroal only swaps backends for the same user/database |
| Pool membership, idle/active counts | pgagroal process memory | Lost on pgagroal restart |
The reason transaction pooling breaks session-scoped features is structural, not a pgagroal limitation: that state lives on a specific backend, and the pool may hand you a different one on the next transaction.
A query under the session pipeline
In session or performance mode, the client owns one backend for the whole duration of the client connection.
- Client opens TCP to pgagroal on
6432. - pgagroal authenticates the client.
- pgagroal pulls a backend from the pool for the requested user and database. If none is available, it opens a new one (up to
MAX_CONNECTIONS) or waits up toblocking_timeoutfor one to free up. - From this point until the client disconnects, every byte is proxied between the same client socket and the same backend socket. Queries, results, transactions — all on one backend.
- Client disconnects. pgagroal runs
DISCARD ALLon the backend to clear session state and returns it to the pool.
From the application's point of view, this is indistinguishable from a direct PostgreSQL connection, except that the connection-establishment cost is paid once per pool slot rather than once per client.
A query under the transaction pipeline
In transaction mode, the client only holds a backend for the duration of one transaction.
- Client opens TCP and authenticates as above.
- Client issues a statement. pgagroal borrows a backend from the pool, attaches it to this client, and forwards the statement.
- pgagroal proxies the transaction in both directions until it sees
COMMITorROLLBACK. - Backend is detached from the client and returned to the pool. The client connection stays open with no backend attached.
- Next statement from the client may be served by a different backend. The new backend has no memory of the previous one's session state.
- Client disconnects. No backend is held at this point, so nothing needs to be returned.
Steps 4 and 5 are where the connection ratio comes from. A client that sits idle between transactions holds no backend, so other clients can use that capacity. This is also why long transactions defeat the model: a backend held for ten seconds is unavailable to other clients for ten seconds.
The pool: warmup, validation, recycling
The pool is not a fixed set of pre-opened connections. pgagroal grows and shrinks it within configured bounds, and optionally tests connections before handing them out.
Sizing bounds
Each database+user pair has an initial_size, min_size, and max_size. The container exposes a single ceiling via MAX_CONNECTIONS. See Configuration for the env vars and defaults.
Validation
pgagroal can verify a backend is alive before handing it out (foreground), on a timer in the background (background), or not at all (off, the default). Foreground validation costs a round-trip per acquire; background validation amortizes the cost but allows a small window where a stale connection can be handed out.
Idle timeout and recycling
Backends idle in the pool past their timeout are closed to free PostgreSQL resources. A connection that has served many transactions can also be recycled to release backend memory.
Failure handling
If PostgreSQL restarts, pgagroal's pooled connections become invalid. The next acquire will reconnect transparently (typically within 60s); existing in-flight clients see the underlying disconnect.
Queueing under load
When all backend connections are in use, new requests do not fail immediately. They wait.
- -- A client requesting a backend (at connect time in
sessionmode, or per-transaction intransactionmode) blocks until either a backend becomes available orblocking_timeoutis reached. - -- Application latency increases — visibly so during sustained pool saturation.
- -- PostgreSQL load does not increase proportionally. It is doing the same work as before; the pooler is queueing the surplus on the client side.
- -- This is expected behavior. The pool size is the throttle. If you do not want the throttle, raise
MAX_CONNECTIONSwithin your capacity budget.
A small amount of queueing is normal and expected in a healthy system.
Queueing protects PostgreSQL from concurrency it cannot serve. Removing the queue (by oversizing the pool) does not increase throughput — it just moves the contention from the pool to PostgreSQL itself, where it is harder to reason about. See Performance and capacity for sizing math, or Troubleshooting for what sustained queueing looks like in practice.
What pgagroal is not
Worth stating explicitly, because pooler products vary in scope:
- -- Not a query rewriter. Statements pass through unchanged.
- -- Not a load balancer in the read-replica sense. The container connects to one backend address; failover and replica routing are configured separately.
- -- Not a query cache. Identical queries hit PostgreSQL every time.
- -- Not a TLS terminator that decrypts and re-encrypts to change protocols. It can carry TLS end-to-end (in
sessionortransactionpipelines), but it does not transform traffic.
The thinness is deliberate. It keeps the per-statement overhead small enough that a pooler is worth using even for workloads where the connection ratio is not the primary motivation.
See also: Concepts for what each pipeline preserves, Choosing your connection path for whether to put a pooler in front at all, or Observability for inspecting pool state at runtime.