PostgreSQL Connection Pooling for Serverless Containers

Size PostgreSQL connection pools across serverless replicas, choose direct or transaction endpoints, and run a bounded Node.js pool with timeout checks.

Adrian Silaghi

8 min read

0 views

PostgreSQLServerlessConnection PoolingPgBouncerNode.js
PostgreSQL Connection Pooling for Serverless Containers

Serverless PostgreSQL applications need a connection budget across every replica and process. Start with one small application pool per process, cap the replica count, and reserve database capacity for jobs and operations. A transaction pooler can reuse database backends across many clients, but it changes the session features your application can rely on.

The practical problem is multiplication. A pool that looks small on one laptop can become hundreds of database connections when HTTP replicas, worker processes, old deployment revisions and batch jobs run together. The right starting point is a bounded total, followed by measurement, rather than the driver’s default multiplied by an unknown number of replicas.

Calculate the maximum before changing a pool

Use an illustrative database budget of 100 available client connections after accounting for system needs. Reserve 20 for administration, migrations, monitoring and scheduled work. If the HTTP application can run eight replicas with two Node processes per replica, a pool maximum of four produces 8 × 2 × 4 = 64 connections. That leaves 16 within the illustrative application allocation. These numbers explain the arithmetic; they are not a DanubeData plan limit or sizing recommendation.

HTTP ceiling = maximum replicas × processes per replica × pool maximum
Total demand = HTTP ceiling + workers + Runs + migrations + rollout overlap

Deployment overlap matters. An old and new revision may both serve traffic or drain requests during a rollout. An independent scheduled report can connect while all HTTP replicas are busy. Multiple services pointed at the same database must share the budget; each service cannot allocate the entire server maximum to itself.

A pool ceiling controls connections, not request admission. Hundreds of waiting HTTP requests can still consume memory and exceed user timeouts. Set a finite acquisition timeout, use short queries, cap HTTP concurrency appropriately, and return a controlled temporary error under saturation. If latency rises when you enlarge the pool, the database may already be overloaded rather than starved of clients.

Application pools and transaction pools solve different problems

Connection pathWhat it reusesMain constraint
Application pool to direct PostgreSQLConnections within one application processEvery open client occupies a database backend
Application pool to transaction poolerClient sockets locally; database backends across completed transactionsSession state cannot be assumed between transactions
Dedicated direct connectionOne intentionally long-lived database sessionConsumes a backend and needs reconnect handling

Application pooling removes repeated connection setup from normal queries and gives each process a known bound. node-postgres recommends a limited number of pools; creating a pool inside every request defeats that purpose. An idle timeout can release unused clients after a burst, but a cold replica will still need to establish its first connection.

PgBouncer transaction pooling assigns a database server connection for a transaction and returns it afterward. The next transaction may use a different backend. Do not depend on a session-level SET, a temporary table that survives commits, a session advisory lock or LISTEN carrying over. Use a direct endpoint for a notification listener. Transaction-local settings and short ordinary transactions are a better fit for pooled request traffic. Check the PgBouncer feature matrix for your exact usage.

Prepared statements are version- and configuration-sensitive. Do not repeat the blanket advice that transaction pooling always supports or never supports them. Protocol-level prepared statement support depends on the pooler configuration; SQL PREPARE has different semantics. Test the driver and ORM you actually ship, especially after schema changes that alter selected result types.

A complete bounded Node.js HTTP example

Download the runnable example and local checks. It needs Node.js 22 or newer and pins pg@8.16.3. Run npm ci, set DATABASE_URL securely, and start node server.mjs. The endpoint returns database time and creates no tables, so it is suitable for a synthetic connectivity exercise.

import http from 'node:http';
import {readFileSync} from 'node:fs';
import {pathToFileURL} from 'node:url';
import {Pool} from 'pg';

if(!process.env.DATABASE_URL) throw Error('DATABASE_URL is required');
const ssl=process.env.PG_CA_FILE
 ?{ca:readFileSync(process.env.PG_CA_FILE,'utf8'),rejectUnauthorized:true}
 :undefined;
export const pool=new Pool({
 connectionString:process.env.DATABASE_URL,ssl,
 max:2, connectionTimeoutMillis:2000,
 idleTimeoutMillis:10000, statement_timeout:3000,
 application_name:'rapids-pool-demo'
});
pool.on('error',()=>console.error('idle database connection failed'));
export const server=http.createServer(async(req,res)=>{
 if(req.method==='GET' && req.url==='/healthz') {
  res.writeHead(200);return res.end('ok');
 }
 if(req.method!=='GET'||req.url!=='/db-time') {res.writeHead(404);return res.end();}
 try {
  const {rows}=await pool.query('SELECT CURRENT_TIMESTAMP AS db_time');
  res.writeHead(200,{'Content-Type':'application/json'});
  res.end(JSON.stringify(rows[0]));
 } catch {
  console.error('database request failed');
  res.writeHead(503,{'Retry-After':'2'});res.end('Database temporarily unavailable');
 }
});
server.requestTimeout=10000;
if(process.argv[1] && import.meta.url===pathToFileURL(process.argv[1]).href) {
 server.listen(Number(process.env.PORT??8080),'0.0.0.0');
 process.on('SIGTERM',()=>server.close(()=>pool.end()));
}

The pool is created once when the module loads and starts empty. Each process can open at most two clients. A simple pool.query automatically checks out and returns a client; the pool can close clients that sit unused. Acquisition and statement limits stop common waits from continuing indefinitely. They do not bound every possible application operation or replace HTTP admission control.

For a multi-statement transaction, acquire one client and use it until commit or rollback. Never mix BEGIN on one pool.query call with later queries that may run on another connection. Release the client in finally, including error paths, following the driver’s transaction guidance. A leaked checked-out client eventually exhausts even a correctly sized pool.

The example does not print connection strings or raw database errors. Its health endpoint checks the HTTP process without making the database a prerequisite for process liveness. The database request returns 503 on failure. In a real authenticated application, protect business routes with your existing authentication and rate controls.

Connect Rapids to PostgreSQL with explicit limits

With DanubeData Rapids, deploy the included Dockerfile through your chosen image or source workflow. Set port 8080, provide the database URL through the container environment, and choose a replica maximum that fits your calculation. Do not copy a maximum from an unrelated benchmark: your process count, queries and database capacity determine the usable limit.

Use the endpoint and port shown for the database connection you selected. A direct writer endpoint and a connection proxy endpoint are not interchangeable names for the same connection semantics. If a transaction proxy is available and enabled on your managed PostgreSQL instance, use it for compatible short request transactions; keep an intentional direct connection path for tools that need session features.

For network connections, verify TLS against the actual DNS name. The code accepts a PEM CA file through PG_CA_FILE. When using that option, omit sslmode, sslcert, sslkey and sslrootcert URI parameters, which can replace the explicit SSL object in node-postgres. For a publicly trusted CA, use the provider’s verified TLS connection settings. Do not disable certificate verification to make a failing connection appear healthy.

A scaled-to-zero HTTP application has no persistent process-level pool to keep warm. After idle shutdown it reconnects. A minimum replica count can trade additional compute usage for fewer idle-to-active starts, but it does not guarantee latency or prevent database failover. Measure the whole request path. Review the current Rapids limits and deployment status and diagnostics before rollout.

Verify limits and recovery, not just a successful SELECT

The downloadable local test sends 24 concurrent HTTP requests to an isolated PostgreSQL fixture, asserts that the process stays within two database clients, deliberately holds both clients to exercise the acquisition timeout, then releases them and checks recovery. It requires a guarded, dedicated loopback database URL. This verifies application pooling; it does not benchmark throughput or exercise a live PgBouncer service or TLS endpoint.

Before using a real transaction pooler, repeat an integration test through that endpoint with your application’s actual transactions, migrations and prepared-statement configuration. Include a rolling deployment, a deliberately slow query, lost idle connections and a database restart in a non-production environment. Confirm that failures become controlled application errors and that clients recover without an uncontrolled reconnect storm.

Read the symptom before increasing a limit

  • Pool acquisition timeouts: inspect active requests, leaked clients and long transactions. Increasing the pool may hide the leak temporarily.
  • Too many database connections: sum every application’s replica and process counts, including overlapping revisions and jobs.
  • Slow queries with spare connections: inspect query plans, indexes, locks and I/O; connection capacity is not query performance.
  • Errors only through the transaction endpoint: audit session state and prepared-statement compatibility before changing database limits.
  • TLS errors: check endpoint hostname, CA chain and validity. Keep verification enabled.

For deeper query diagnosis use PostgreSQL performance tuning. For a broader explanation of client and server pooling, see database connection pooling explained. Track pool wait time and database connection usage alongside application error rate; a low connection count with high waiting time can still mean users are stuck.

Frequently asked questions

Should every serverless replica use a pool?

A reusable bounded pool is a sensible default for a long-lived HTTP process. A short one-off script may need only one client, which it closes when finished. In both cases, include the maximum in the shared database budget.

Does PgBouncer remove the need for an application pool?

No. A small local pool still bounds client sockets and waiting work. The transaction pooler manages database backends, and its client and backend limits must also fit the workload.

Can I use transaction pooling for LISTEN?

A long-lived listener depends on session affinity. Use a dedicated direct connection or a compatible session-mode endpoint, and implement reconnect and resubscription behaviour.

Is a larger pool faster?

Only when the database has useful parallel capacity and connection scarcity is the bottleneck. Too many concurrent queries can increase contention and memory use. Change one limit at a time and measure tail latency and error rate.

Sources

Related tutorials

Browse all articles
Databases10 min read

PostgreSQL Storage Autoscaling: Never Run Out of Disk Again (2026)

A full PostgreSQL disk is one of the worst database outages there is. Learn how storage autoscaling prevents it, how DanubeData grows your managed Postgres volume automatically with a spend cap you control, and how it compares to manual resizing and AWS RDS-style autoscaling — all EU-hosted.

Adrian Silaghi501 views
Databases6 min read

Introducing SQL Studio: Your PostgreSQL Database, Right in the Browser

Every managed PostgreSQL database on DanubeData now ships with SQL Studio — a full data studio in your dashboard. Browse and filter tables, edit rows with staged transactional commits, run SQL with a read-only safe mode, build tables visually, and map your schema as a live ER diagram. No client to install, no credentials to copy, no extra cost.

Adrian Silaghi576 views

Ready to get started?

Deploy your infrastructure in minutes with DanubeData's managed services.