Concepts
What pooling actually does to your connection, and how to choose between pgagroal's three pipelines.
Pooling is not one thing
pgagroal supports three pipelines. They differ on two independent axes:
- -- Connection scope: how long a single client holds a backend connection (whole session, or a single transaction).
- -- Feature set: whether TLS, failover, and
disconnect_clientare available.
The container ships with pipeline = auto, which selects performance or session based on configuration. Transaction pooling is opt-in. The choice that matters for application code is session-scoped vs transaction-scoped reuse.
Pipeline comparison
Read this table down a column to understand a pipeline, or across a row to compare a single property.
| performance | session | transaction | |
|---|---|---|---|
| Connection scope | Whole client connection | Whole client connection | One transaction |
| Session state preserved | Yes (cleared on disconnect) | Yes (cleared on disconnect) | No |
| Server-side prepared statements | Safe | Safe | Unsafe — must be disabled in driver |
Temp tables, WITH HOLD, LISTEN | Available | Available | Not available across transactions |
TLS, failover, disconnect_client | Not supported | Supported | Supported |
| Backend connections vs clients | ~1:1 while connected | ~1:1 while connected | Many clients per backend |
| Scaling ceiling | Bound by PG max_connections | Bound by PG max_connections | Far above PG max_connections |
| Recommended use | Session semantics on a trusted network where TLS to the backend is not required | Default for almost everything; session semantics with full features | High concurrency, short transactions, no session state |
performance and session behave identically from the application's point of view. The difference is feature set, not pooling behavior. The jump in behavior is between session-scoped (either of them) and transaction-scoped.
Choosing a pipeline
If you are unsure, use the default (pipeline = auto).
In practice, this behaves like a direct PostgreSQL connection: session-level behavior is preserved and your application does not need to change.
Switch to transaction only when
- -- Your workload is stateless (web / API / OLTP), and
- -- You have verified that your application does not rely on session-level
SET, server-side prepared statements, temporary tables,LISTEN/NOTIFY, or session advisory locks.
Server-side prepared statements must be disabled in your driver before transaction pooling will work. Most ORMs and modern drivers cache them by default. See Disabling server-side prepared statements below.
If these conditions are not met, your application may behave incorrectly under load.
Use performance only when
- -- You need maximum throughput, and
- -- The pooler runs in a trusted network where TLS to the backend is not required.
Workload examples
Concrete cases for each pipeline.
Use transaction for
- -- REST or GraphQL APIs where each request is a single short transaction (Go, Java, Python, Node)
- -- High-concurrency OLTP workloads with bursty client counts
- -- Serverless or per-request worker models (Lambda, Cloud Run, ephemeral containers)
- -- Microservices where total client count exceeds what PostgreSQL can hold open
Use session (the default) for
- -- Application servers with long-lived workers that reuse a connection (typical Rails, Django, Spring, Go pool with persistent conns)
- -- Batch jobs that build temp tables, use
COPYacross statements, or hold session-scoped advisory locks - -- BI and OLAP query tools with long-lived sessions and ad-hoc queries
- -- Anything that depends on driver-side prepared statement caching and cannot disable it
- -- Any production workload over an untrusted network (TLS to the backend is required)
Use performance for
- -- Pooler co-located with PostgreSQL on the same host or trusted private network, where TLS to the backend is not required
- -- Throughput benchmarks where the feature overhead of
sessionis the measured bottleneck
Switching to transaction is a code change, not a config change. Audit the failure modes in the next section before flipping it on.
What breaks under transaction pooling
Transaction pooling changes connection semantics. The following patterns will break if used incorrectly.
These are not hypotheticals. They are the failure modes you will hit if you switch to pipeline = transaction without auditing your code first.
Session-level SET
- -- What breaks:
SET search_path = ...and other session-level settings applied at session start. - -- Why: They apply to the current backend session. The next transaction may run on a different backend that never saw the SET.
- -- What to do instead: Use
SET LOCALinside the transaction, or set defaults on the role / database withALTER ROLE ... SET.
Server-side prepared statements
- -- What breaks:
PREPAREand named queries that the server caches per connection (the default for most ORMs and modern drivers). - -- Why: Prepared statements live on the backend that prepared them. The next client can inherit a backend with statements it did not create, or one missing a statement it expects.
- -- What to do instead: Disable server-side prepared statement caching in your driver. See Disabling server-side prepared statements below for driver-specific settings.
Temporary tables and WITH HOLD cursors
- -- What breaks:
CREATE TEMP TABLEandDECLARE ... CURSOR WITH HOLD. - -- Why: Both are tied to the session. They will not be visible from the next transaction once the backend has been returned to the pool.
- -- What to do instead: Use unlogged regular tables for cross-transaction scratch data, or keep the cursor inside a single transaction.
LISTEN / NOTIFY
- -- What breaks:
LISTENregistrations on a connection. - -- Why:
LISTENregisters the current backend as a notification target. Once the pool returns the connection, notifications go nowhere the application can see. - -- What to do instead: Connect directly to PostgreSQL (bypass pgagroal) for any
LISTEN/NOTIFYconsumer.
Advisory locks across transactions
- -- What breaks:
pg_advisory_lockand the other session-scoped variants. - -- Why: They are held by the session and survive commits. Under transaction pooling the lock and the next query may sit on different backends.
- -- What to do instead: Use the transaction-scoped variant
pg_advisory_xact_lock, which acquires and releases within a single transaction.
Long-running or idle-in-transaction sessions
- -- What breaks: Pool reuse. The pool model assumes transactions are short.
- -- Why: A transaction that holds a backend for ten seconds blocks pool reuse for ten seconds. Other clients queue.
- -- What to do instead: Keep transactions short. Move long work to background jobs that connect direct to PostgreSQL.
None of these are pgagroal bugs. They are direct consequences of changing what "the connection" means. The same caveats apply to PgBouncer in transaction mode and to RDS Proxy.
Disabling server-side prepared statements
If you adopt transaction pooling, every driver in your stack needs to stop caching prepared statements on the server. The setting name varies.
| Driver | Setting | Effect |
|---|---|---|
| pgx (Go) | default_query_exec_mode=simple_protocol | Skips the extended protocol entirely |
| psycopg (Python) | prepare_threshold=None | Disables the per-connection prepare cache |
| JDBC (Java) | prepareThreshold=0 | Forces unnamed statements; nothing is cached server-side |
| node-postgres | Avoid the name field on queries | Named queries become server-side prepared statements |
ORMs that build on these drivers inherit the behavior. Check the ORM's own settings if it manages a higher-level statement cache.
See also: Choosing your connection path for whether you need a pooler at all, or Configuration for the environment variables that control pipeline selection.