Odyssey PostgreSQL connection pooling: where the pool breaks
A typical failure looks like this: the application opens connections faster than the database can hand them out. Each connection in PostgreSQL is a separate process with its own memory, and the database connection limit hits a hard ceiling.
Once the pool is exhausted, new requests queue for a connection, wait, and fail by timeout — while the database itself may be half loaded.
Then a cascade begins: the application gets an error, retries the request, opens new connections — the queue grows, while the limit stays the same. The failure stops being local: all services that access the same database degrade, including background tasks and reporting.
Diagnostics run into the fact that the query metrics themselves are clean — it is not the query that is slow, but waiting for a connection.
Odyssey PostgreSQL connection pooling is a layer between the application and the database: the service maintains its own pool, and clients take a connection from it instead of opening a new one in PostgreSQL.
Multiplexing in transaction mode allows hundreds of client connections to work through dozens of server connections, while limits are set separately per user, database, and pool.
The connection request queue becomes manageable: you can see who occupies it, how long they wait, and by which timeout they drop.
The key here is to understand exactly where the pool breaks and which parameters are responsible. We cover below: operating modes, limits, behavior under overflow, and what connection failures look like under load before and after tuning.
Managed connection pool: fewer failures and stable latency
A pool makes sense only as a separate managed layer between the application and the database.
The application opens a short-lived connection, while the manager maintains a persistent set of warmed-up sessions and hands them out on demand — then connection reuse becomes the norm, and the number of concurrent connections stops growing with the number of application processes and replicas.
Reducing failures starts with a hard limit: when free slots run out, new connections queue with a maximum wait time instead of overwhelming the database. Dead sessions are cut off by a health check, so the application gets a clear error instead of a hanging connection.
Stable response time comes from avoiding repeated handshakes: the connection is already established and warmed up; each request does not spend time on negotiation and authentication.
It is not the average that improves, but the tail latency — the one that usually ruins the user scenario.
Recovery after a failure also changes class. The pool re-establishes sessions on its own schedule and spreads out reconnection, so services do not storm the database all at once.
The load on the database at that moment is predictable, and return to normal operation is measured in seconds, not minutes of manual restarts.
We verify the effect by metrics: pool occupancy, queue length, slot wait time, share of connection failures, share of reused sessions, and number of active database connections.
A before-and-after snapshot shows where the headroom went, and the same values are used to configure alert thresholds.
Pooler operating modes: session, transaction, statement
The pooling mode is set by the pool_mode parameter separately for each pool, so in one Odyssey configuration different clients are served differently.
It is chosen not by a general recommendation, but by whether the application stores state between requests: prepared statements, advisory locks, temporary tables, and LISTEN/NOTIFY are tied to a specific server session.
The wrong mode manifests as intermittent failures: some queries fail on prepared statements, locks are not released, and session parameters are applied to the wrong connection.
| Mode | When it fits | Limitations |
|---|---|---|
| Session | Applications with session state: prepared statements, advisory locks, LISTEN/NOTIFY, temporary tables, long administrative connections | The connection is pinned to the client until disconnect — there is no savings on idle sessions |
| Transaction | Main workload: short transactions, high concurrency, web services and queues | Session state cannot be kept between requests; a multi-step operation must fit into one transaction |
| Statement | Single queries without transactions, smoothing connection bursts | Transactions are absent, state is not preserved between requests |
In practice, transaction mode is taken as the basis, while session mode is left selectively — for specific users, databases, or pools where the application really relies on connection state. Statement mode is kept for simple single queries and offloading during bursts.
Before rollout, each option is run against a real workload profile: in synthetic tests, an error in mode selection does not manifest.
How implementation proceeds: stages of engineering work
The order of work here is not a formality: pool limits set by assumptions cause failures at peaks or leave the database without free processes. We go from the actual connection profile to load testing — each stage relies on data from the previous one.
- Audit of database connections. We count how many connections each service opens, which ones it holds persistently, what background tasks and queues do. The output is a consumer map with peak and average values.
- Connection routing scheme. We separate traffic by entry points: write services, read, analytics, and utility scripts separately, so a heavy report does not take up space in the common pool.
- Designing the Odyssey configuration. We choose the pool mode, limits per user and database, connection lifetime, and behavior on disconnect — so that the application does not notice the layer.
- Deployment in your environment. The pooler is installed next to the database or as a separate layer, with redundancy and health checks; the configuration is stored in a version control system.
- Gradual traffic switchover. We first migrate one service, compare its errors and latency with the previous mode; rollback is by editing the configuration, without restarting the database.
- Load testing. We run the real profile: peak hours, batch tasks, failure of one node. We see whether the pooler holds the queue and what happens to the application when the limit is exhausted.
- Handover to operations. We hand over the configuration, routing scheme, metrics, and change procedure — your team edits the limits itself.
What you keep: configurations, schemes, and procedures
We hand over not server access, but a complete set of artifacts: using them, your team can bring up the pooler from scratch, change the configuration, and troubleshoot incidents without us.
We put everything into your repository and your documentation system — no “settings on the contractor’s laptop.”
- Pooler configuration: connection sets by “database/user” pairs, limits for each, establishment, wait, and idle timeouts, transaction routing mode.
- Application connection schemes: which services go through Odyssey and into which set, which remain direct — analytics, replication, migrations.
- Access policies: role set, rules for admission by addresses and authentication methods, separation of privileges between the application and the database administrator.
- Operations procedure: procedure for editing the configuration, reloading without dropping active sessions, maintenance windows, actions when the connection set is exhausted.
- Monitoring and alerts: which metrics we collect, threshold values, health checks, integration with your alerting environment.
- Procedures for typical failures: growing wait queue, connection leak, pooler node restart, rollback to the previous configuration version.
- Connection flow map and responsible parties: who grants access, how secret rotation works, whom to contact when the data schema changes.
We separately document responsibility boundaries: what you maintain yourself and what remains with us under the support agreement. Describe the task — we will come back with an estimate and a handover plan.
Case study: eliminated connection failures in a heavily loaded service
A service with an “API plus background tasks” profile ran on a single PostgreSQL node. At peak, the application opened a connection for every request, and the database hit the maximum allowed connections: some clients got a failure, while others waited for a slot to free up.
What we configured
Connection management was moved to a separate layer — Odyssey between the application and the database.
Pools were separated: short API transactions and long background queries no longer compete for the same set of slots, the server connection returns to the pool immediately after commit, and the pool size is set strictly — the database sees a predictable number of clients during any burst.
Metrics before and after
The share of failed requests at peak dropped from 4–6% to 0.1%; concurrent database connections — about 120 instead of 900. Failures of the “connection limit exhausted” class disappeared entirely.
The application stopped paying for connection establishment and permission checks on every request — the pool keeps connections open. Response time at peak no longer grows in steps because requests do not queue up for a slot.
A separate effect is recovery time. Previously, a disconnect caused an avalanche of reconnections from all worker threads, and the service returned to normal in about 40 seconds.
Now the pool handles the disconnect: it checks connection liveness, serves requests only on working connections, and restores the full set of slots in 3–5 seconds — imperceptible to the client.
How an external pooler differs from an application’s built-in pool
The built-in pool lives inside the application process: it holds connections for one instance and closes them along with it. As long as the service is alone and runs in several copies, connection establishment costs are amortized — that is enough.
The boundary becomes apparent when several applications are connected to the database. Each instance holds its own minimum of connections; when scaling, the sum grows and hits the server limit: new connections are rejected even though the load has not increased. Restarting the service takes the entire pool with it, and some requests wait during that time.
An external pooler is a separate layer between applications and the database. It maintains a common set of connections and hands them out on demand, and the application connects to it just as it would to the database itself.
The number of connections to the server is set by a single setting, not by the sum of processes, and it survives the restart of any of them.
Beyond that, the external layer takes on what the built-in pool cannot do: switching reads to replicas, limits by users and databases, statistics, and gracefully taking the server out for maintenance without dropping connections. For this, Odyssey is placed between applications and PostgreSQL.
The external layer is needed when there are several applications, when processes are short-lived — queue handlers, functions, migrations — or when the database is maintained and restarted more often than desired.
If there is one application, connections are long-lived and live in one process, the built-in pool is sufficient: an extra layer adds a point of failure.
Common technical questions about connection pooling
Migrating applications to a common pool layer — in our case to Odyssey — changes not so much the load as the connection model: instead of a persistent session, a request gets a server connection for the duration of a transaction.
This is where almost all engineering team doubts come from: what will happen to prepared statements, locks, and how to maintain the pooler itself.
Will prepared statements stop working?
In transaction pooling mode, a named prepared statement created by one client may execute on another server connection where it does not yet exist.
This is resolved by caching prepared statements on the pooler side: it tracks them and creates them on the required connection before serving. We test the behavior in a staging environment before migrating traffic.
Will the pooler have to be restarted, and what will happen to the connections?
Restart is a routine operation: server connections are closed, and applications reconnect with retries and delay. To make the window imperceptible, we enable graceful shutdown with waiting for active transactions and update instances one by one.
What breaks with transactions and locks?
A transaction holds one server connection for its entire duration, so everything that lives outside its boundaries transfers poorly: session-level advisory locks, LISTEN/NOTIFY, held cursors, temporary tables.
The rule is simple — state must not outlive the transaction; where this is impossible, we move such connections to a separate pool with a persistent session.
How do you know the pool has become a bottleneck?
We look at two values: how many clients are waiting for a server connection and how many connections are idle. Growth of the first while the second is zero means the pool is too small; the opposite means connections are being held for nothing. Separately, we check server-side locks: long transactions produce the same waiting picture.
Let’s discuss the task and come back with an estimate and implementation plan
For the estimate and work plan to match reality rather than a generic template, we need a snapshot of your current connection scheme.
Describe which applications access the database, through which drivers and pools, how many connections they hold at peak, and what happens during mass reconnection after a database server restart.
This is enough to propose a scheme for migrating applications to the pool and to name the set of artifacts you will receive.
What is worth gathering before the conversation:
- connection inventory: services, libraries, built-in pools, manual limits in configurations
- workload profile: peak number of concurrent sessions, share of short and long transactions, seasonal bursts
- list of services to migrate to the pool — noting where changing the connection string is enough and where code changes are needed
- constraints: access, maintenance windows, rollback requirements, security procedures
- target metrics: connection wait time under peak, behavior during database degradation, the failure class to be eliminated
If some data is unknown, that is not a problem: we will take metrics on site and clarify the picture. The tool here is not an end in itself: we select the operating mode and parameters for your workload profile, not the other way around.
Based on the discussion, we come back with an estimate and work plan: stages of connection pool implementation, service switchover order, rollback points, and responsibility areas of both teams. After that — infrastructure support: monitoring, alerts, incident analysis, and change procedure.







