Prompt
How do I keep PostgreSQL connections under control in a Node app?
Latest observation
To keep PostgreSQL connections under control in a Node app, the main goal is: don’t create a new connection per request. Instead, reuse a small pool of connections and make sure every client is returned properly.
Core practices
1) Use a connection pool
With pg, use Pool, not Client per request.
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10, // max active connections in this process
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
max: upper bound of simultaneous connections per Node process- Keep this relatively low unless you know you need more
2) Always release clients
If you manually grab a client from the pool, release it in finally.
const client = await pool.connect();
try {
const res = await client.query('SELECT now()');
return res.rows;
} finally {
client.release();
}
If you forget release(), connections get leaked and the pool eventually stalls.
3) Prefer pool.query() for simple queries
If you don’t need a transaction or session-level state, this is safer and simpler:
const res = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
This automatically manages checkout/checkin of a client.
4) Use transactions only when needed
For multi-step operations:
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('UPDATE accounts SET balance = balance - 10 WHERE id = $1', [from]);
await client.query('UPDATE accounts SET balance = balance + 10 WHERE id = $1', [to]);
await client.query('COMMIT');
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
5) Don’t over-scale Node processes
Each process has its own pool.
If you run:
- 4 Node workers
pool.max = 10
That can mean up to 40 DB connections just from your app.
So tune:
- number of app instances
- pool size per instance
- PostgreSQL
max_connections
6) Add timeouts
Prevent stuck queries/connections:
connectionTimeoutMillis: wait time to get a connection from pool- query timeout (via SQL
statement_timeoutor client-level options)
Example:
await pool.query('SET statement_timeout = 5000');
Or set it in PostgreSQL config / role settings.
7) Close the pool on shutdown
So Node exits cleanly and doesn’t leave open connections:
process.on('SIGINT', async () => {
await pool.end();
process.exit(0);
});
Also handle SIGTERM in containers/kubernetes.
8) Consider PgBouncer for high scale
If your app servers scale out a lot, PostgreSQL can run out of connections even if each instance uses a pool.
PgBouncer sits between app and DB and multiplexes many short-lived app connections onto fewer DB connections.
Useful when:
- many app instances
- serverless or autoscaling
- connection spikes
Common anti-patterns
- Creating a new
Client()for every request - Forgetting
client.release() - Setting
pool.maxtoo high in every process - Long transactions holding connections idle
- Leaving interactive/background jobs without pool limits
Rule of thumb
A good starting point is:
pool.max = 5–10per Node process- keep transactions short
- measure actual DB wait time and adjust
- use PgBouncer if total app connections grow too large
If you want, I can show:
- a minimal Express + pg pool setup, or
- how to size pool limits based on your number of app instances and PostgreSQL
max_connections.