Node.js 42 ๐ข Database Connection Pooling and Lifetime Management
Every database query needs a connection. The naive approach is to open a connection for each query, run the query, and close the connection. The problem is that opening a connection is expensive: the TCP handshake, the TLS negotiation, the authentication, and the database’s own session setup all take time. For a PostgreSQL connection, the round trips can take tens of milliseconds. For an application that serves hundreds of requests per second, the connection overhead dominates the query time. The solution is a connection pool: a set of connections that are opened once and reused across the queries. The pool amortizes the connection cost over many queries, and it bounds the number of connections the application opens, so the database’s max_connections is not exhausted.
The pool is more than a cache of connections. It is a stateful resource with its own lifecycle: connections are created on demand, reused when available, closed when idle for too long, and replaced when they fail. The pool has a maximum size, a queue for the pending requests, and a timeout for the acquisition. The application must release the connection when the query is done, or the pool leaks and the next request waits forever. The lifecycle is the part that most applications get wrong.
The distinction between pool.query() and pool.connect() is the foundation. pool.query() checks out a connection, runs the query, and releases the connection automatically. It is the correct choice for a single query. pool.connect() checks out a connection explicitly, and the application must release it with client.release(). It is the correct choice for a transaction or a series of queries that must run on the same connection. Using pool.query() for a transaction is the most common mistake: each query may run on a different connection, and the transaction boundaries are lost.
This chapter covers three areas. First, why connection pooling exists โ the connection-cost problem and the pool as a bounded resource. Second, how the pool works โ the pool size, the idle timeout, the acquisition timeout, the checkout and release, and the error handling. Third, the lifecycle management patterns โ the transaction pattern, the graceful shutdown, the pool error handler, the connection validation, and the observability of the pool’s state. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the pool’s lifecycle.
Key point: A connection pool opens a set of connections once and reuses them across the queries. pool.query() checks out a connection, runs the query, and releases it automatically. pool.connect() checks out a connection explicitly, and the application must release it with client.release(). The pool has a maximum size, an idle timeout, and an acquisition timeout. The pool must be ended on shutdown with pool.end(), and the idle client errors must be handled with pool.on('error', ...).
Why connection pooling exists
The connection-cost problem. Opening a database connection is a multi-step process. For PostgreSQL, the client opens a TCP socket, optionally negotiates TLS, sends the startup message, authenticates (often with SCRAM-SHA-256, which is a multi-round exchange), and waits for the server to spawn a backend process. The whole process takes tens of milliseconds on a local network and hundreds on a remote one. A query on a warm connection takes a fraction of a millisecond. The connection cost dominates the query time, and the overhead is repeated for every query in the naive approach.
The resource-bound problem. A database has a maximum number of connections, set by the max_connections parameter. PostgreSQL’s default is 100; MySQL’s is 151. If the application opens a connection per request and the request rate exceeds the limit, the database refuses the new connections. The application’s requests fail, and the failure cascades. A pool bounds the number of connections: the application never opens more than the pool’s maximum, and the requests queue when the pool is exhausted. The database’s limit is respected, and the failure mode is a queue rather than a refusal.
The reuse problem. A connection is a stateful resource. The prepared statements, the session variables, and the temporary tables are scoped to the connection. Reusing a connection means reusing the prepared statement cache, which can significantly improve the performance of repeated queries. A new connection has an empty cache, and the first execution of each query is a parse and a plan. A reused connection has the cache, and the execution reuses the plan.
The lifecycle problem. A pool is not a static set of connections. The connections are created on demand, and the pool grows up to the maximum. The idle connections are closed after a timeout, and the pool shrinks. The connections that fail are removed, and the pool creates new ones. The lifecycle is managed by the pool, and the application only sees the checkout and the release. The pool’s health is the application’s health: if the pool is exhausted, the requests queue; if the connections are stale, the queries fail.
The transaction problem. A transaction is scoped to a connection. The BEGIN, the statements, and the COMMIT must all run on the same connection, or they are not part of the same transaction. The pool.query() method does not guarantee this: it checks out a connection, runs the query, and releases it. The next pool.query() may check out a different connection. For a transaction, the application must check out a connection explicitly with pool.connect(), run the transaction statements on that connection, and release it when done. This is the most common mistake with connection pools.
The trade-off. The pool adds a layer of complexity. The application must acquire and release the connections, handle the acquisition timeouts, and manage the pool’s lifecycle. The alternative โ a connection per query โ is simpler but slower and unbounded. The trade-off is between the complexity of the pool and the performance and reliability it provides. For any application that serves more than a few requests per second, the pool is the correct choice.
a. The pool configuration
A pool is created with a configuration object. The configuration controls the pool’s size, the timeouts, and the behavior of the connections.
PostgreSQL (pg).
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
port: 5432,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 20,
min: 2,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
maxUses: 7500,
allowExitOnIdle: false,
});
The max is the maximum number of connections. The min is the minimum the pool keeps open. The idleTimeoutMillis closes a connection after it has been idle for the specified time. The connectionTimeoutMillis is the time pool.connect() waits for a connection before rejecting. The maxUses closes a connection after it has been used the specified number of times, which helps with the server-side memory leaks. The allowExitOnIdle allows the process to exit when all the connections are idle.
MySQL (mysql2).
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: 3306,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
connectionLimit: 20,
maxIdle: 20,
idleTimeout: 60000,
queueLimit: 0,
waitForConnections: true,
enableKeepAlive: true,
keepAliveInitialDelay: 0,
});
The connectionLimit is the maximum number of connections. The maxIdle is the maximum number of idle connections. The idleTimeout closes an idle connection after the specified time. The queueLimit is the maximum number of queued requests; 0 means unlimited. The waitForConnections makes the requests queue when the pool is exhausted, rather than throwing. The enableKeepAlive sends periodic packets to keep the connection alive through the firewalls and the server’s wait_timeout.
SQLite (better-sqlite3). SQLite is a file-based database, and it does not have a connection pool in the same sense. The better-sqlite3 driver uses a single connection, and the operations are synchronous.
const Database = require('better-sqlite3');
const db = new Database('app.db');
The driver does not have a pool, because the SQLite database is a single file and the concurrency is limited. The WAL mode improves the concurrency, and the busy_timeout handles the lock contention.
b. The checkout and release
The pool has two APIs: the automatic query and the explicit connect. The query method is the simpler one.
const result = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
console.log(result.rows);
The pool.query() method checks out a connection, runs the query, and releases the connection. The application does not need to release anything. The connection is returned to the pool after the query completes, whether it succeeded or failed.
The connect method checks out a connection explicitly. The application must release it.
const client = await pool.connect();
try {
const result = await client.query('SELECT * FROM users');
console.log(result.rows);
} finally {
client.release();
}
The client.release() method returns the connection to the pool. The finally block ensures the release happens even if the query throws. If the connection is not released, the pool leaks, and the next connect waits until the connectionTimeoutMillis expires and then rejects.
The connect method is the correct choice for a transaction. The transaction statements must run on the same connection, and the query method does not guarantee this.
const client = await pool.connect();
try {
await client.query('BEGIN');
const fromResult = await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2 RETURNING balance',
[amount, fromId]
);
if (fromResult.rows[0].balance < 0) {
throw new Error('Insufficient funds');
}
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toId]
);
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
The BEGIN starts the transaction, the two UPDATEs run on the same connection, and the COMMIT commits the changes. If an error occurs, the ROLLBACK undoes the changes, and the error is rethrown. The finally block releases the connection.
The pool.query() method is a shorthand for the checkout, the query, and the release. It is the correct choice for a single query, and it is the wrong choice for a transaction.
c. The lifecycle management
The pool’s lifecycle is managed by the application. The application must handle the errors, the shutdown, and the observability.
The pool error handler. The pool emits an error event when an idle connection fails. Without a handler, the event is unhandled, and the process crashes.
pool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
// The pool will remove the failed connection and create a new one.
});
The handler logs the error, and the pool continues. The failed connection is removed, and a new one is created when needed.
The graceful shutdown. The pool must be ended when the application stops. The pool.end() method closes all the connections and returns a promise that resolves when they are closed.
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down');
await pool.end();
console.log('Pool closed');
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('SIGINT received, shutting down');
await pool.end();
process.exit(0);
});
The SIGTERM and SIGINT handlers close the pool and exit the process. The pool.end() waits for the in-flight queries to complete, so the shutdown is graceful. The alternative โ process.exit() without the pool end โ leaves the connections open, and the database may keep the server-side sessions until they time out.
The connection validation. A connection can become stale: the network drops, the database restarts, or the server’s wait_timeout closes the session. The pool should validate the connections before the checkout. For PostgreSQL, the pg driver validates the connection on the checkout if the idleTimeoutMillis has passed.
const pool = new Pool({
// ...
idleTimeoutMillis: 30000,
// The connection is validated if it has been idle.
});
For MySQL, the enableKeepAlive option sends periodic packets to keep the connection alive, and the ping method validates the connection.
const conn = await pool.getConnection();
await conn.ping();
conn.release();
The pool observability. The pool’s state is observable through its properties. The totalCount is the total number of connections. The idleCount is the number of idle connections. The waitingCount is the number of requests waiting for a connection.
setInterval(() => {
console.log({
total: pool.totalCount,
idle: pool.idleCount,
waiting: pool.waitingCount,
});
}, 10000);
The metrics are useful for the monitoring: a growing waitingCount indicates the pool is exhausted, and a growing totalCount indicates the connections are leaking. The metrics are the starting point for the tuning.
The pool size tuning. The pool size is a trade-off. A larger pool handles more concurrency but uses more database resources. A smaller pool uses fewer resources but queues the requests sooner. The formula depends on the workload: the CPU-bound queries benefit from a pool size close to the number of CPU cores, and the I/O-bound queries benefit from a larger pool. The pg documentation recommends starting with a pool size of (2 * cpu_cores) + effective_spindle_count for a single database, and adjusting based on the measurements.
Complete Example Session
// ============================================
// PART 1: THE POOL CONFIGURATION
// ============================================
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 20,
min: 2,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// ============================================
// PART 2: THE POOL ERROR HANDLER
// ============================================
pool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
});
// ============================================
// PART 3: THE SIMPLE QUERY
// ============================================
async function findUser(id) {
const result = await pool.query(
'SELECT id, name, email FROM users WHERE id = $1',
[id]
);
return result.rows[0];
}
// pool.query() checks out a connection, runs the query,
// and releases the connection automatically.
// ============================================
// PART 4: THE TRANSACTION
// ============================================
async function transferFunds(fromId, toId, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const fromResult = await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2 RETURNING balance',
[amount, fromId]
);
if (fromResult.rows[0].balance < 0) {
throw new Error('Insufficient funds');
}
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toId]
);
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
}
// The transaction runs on the same connection.
// The client.release() returns it to the pool.
// ============================================
// PART 5: THE LEAK
// ============================================
// โ The connection is never released.
async function leakyFunction() {
const client = await pool.connect();
const result = await client.query('SELECT * FROM users');
return result.rows;
// The client is not released.
// The next connect() waits until the timeout.
}
// โ
The connection is released in the finally block.
async function safeFunction() {
const client = await pool.connect();
try {
const result = await client.query('SELECT * FROM users');
return result.rows;
} finally {
client.release();
}
}
// ============================================
// PART 6: THE GRACEFUL SHUTDOWN
// ============================================
async function shutdown() {
console.log('Shutting down');
await pool.end();
console.log('Pool closed');
process.exit(0);
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
// ============================================
// PART 7: THE POOL OBSERVABILITY
// ============================================
setInterval(() => {
console.log({
total: pool.totalCount,
idle: pool.idleCount,
waiting: pool.waitingCount,
});
}, 10000);
// ============================================
// PART 8: THE MYSQL2 POOL
// ============================================
const mysql = require('mysql2/promise');
const mysqlPool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectionLimit: 20,
maxIdle: 20,
idleTimeout: 60000,
queueLimit: 0,
waitForConnections: true,
enableKeepAlive: true,
keepAliveInitialDelay: 0,
});
async function findUserMySQL(id) {
const [rows] = await mysqlPool.execute(
'SELECT * FROM users WHERE id = ?',
[id]
);
return rows[0];
}
// ============================================
// PART 9: THE MYSQL2 TRANSACTION
// ============================================
async function transferFundsMySQL(fromId, toId, amount) {
const conn = await mysqlPool.getConnection();
try {
await conn.beginTransaction();
const [fromRows] = await conn.execute(
'SELECT balance FROM accounts WHERE id = ? FOR UPDATE',
[fromId]
);
if (fromRows[0].balance < amount) {
throw new Error('Insufficient funds');
}
await conn.execute(
'UPDATE accounts SET balance = balance - ? WHERE id = ?',
[amount, fromId]
);
await conn.execute(
'UPDATE accounts SET balance = balance + ? WHERE id = ?',
[amount, toId]
);
await conn.commit();
} catch (e) {
await conn.rollback();
throw e;
} finally {
conn.release();
}
}
// ============================================
// PART 10: THE COMPLETE APPLICATION
// ============================================
const express = require('express');
const app = express();
app.use(express.json());
app.get('/users/:id', async (req, res) => {
try {
const user = await findUser(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (e) {
console.error(e);
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/transfer', async (req, res) => {
const { fromId, toId, amount } = req.body;
try {
await transferFunds(fromId, toId, amount);
res.json({ success: true });
} catch (e) {
console.error(e);
res.status(400).json({ error: e.message });
}
});
const server = app.listen(3000);
async function gracefulShutdown() {
console.log('Shutting down');
server.close(() => {
console.log('HTTP server closed');
});
await pool.end();
console.log('Pool closed');
process.exit(0);
}
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
The ten parts show the pool configuration, the pool error handler, the simple query, the transaction, the leak, the graceful shutdown, the pool observability, the mysql2 pool, the mysql2 transaction, and the complete application.
Quick Reference
The Pool Configuration (pg)
| Option | Default | Purpose |
|---|---|---|
max | 10 | Maximum connections |
min | 0 | Minimum connections |
idleTimeoutMillis | 10000 | Close idle connections after |
connectionTimeoutMillis | 0 | Wait time for a connection |
maxUses | Infinity | Close after N uses |
allowExitOnIdle | false | Allow exit when idle |
The Pool Configuration (mysql2)
| Option | Default | Purpose |
|---|---|---|
connectionLimit | 10 | Maximum connections |
maxIdle | 10 | Maximum idle connections |
idleTimeout | 60000 | Close idle connections after |
queueLimit | 0 | Max queued requests |
waitForConnections | true | Queue when exhausted |
enableKeepAlive | false | Keep connections alive |
The Two APIs
| API | Use case |
|---|---|
pool.query(sql, params) | Single query |
pool.connect() | Transaction, session-scoped queries |
client.release() | Return the connection |
pool.end() | Close all connections |
The Pool Events
| Event | Purpose |
|---|---|
error | An idle client error |
connect | A new client connected |
acquire | A client was checked out |
release | A client was released |
remove | A client was removed |
The Pool Metrics
| Metric | Meaning |
|---|---|
totalCount | Total connections |
idleCount | Idle connections |
waitingCount | Waiting requests |
Best Practices
โ Do This:
// Use pool.query for a single query
await pool.query('SELECT * FROM users WHERE id = $1', [id]); // โ
// Use pool.connect for a transaction
const client = await pool.connect();
try { await client.query('BEGIN'); ... } finally { client.release(); } // โ
// Release the connection in the finally block
finally { client.release(); } // โ
// Handle the pool errors
pool.on('error', (err) => console.error(err)); // โ
// End the pool on shutdown
process.on('SIGTERM', async () => { await pool.end(); }); // โ
// Monitor the pool state
console.log(pool.totalCount, pool.idleCount, pool.waitingCount); // โ
โ Don’t Do This:
// Don't use pool.query for a transaction
await pool.query('BEGIN'); // different connection next query // โ
// Don't forget to release the connection
const client = await pool.connect(); // never released // โ
// Don't ignore the pool errors
// no pool.on('error') โ crash on idle client error // โ
// Don't call process.exit() without ending the pool
process.exit(0); // connections leak // โ
// Don't open a connection per query
const client = new Client(); await client.connect(); // slow // โ
// Don't set max too high
max: 1000 // exhausts the database's max_connections // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Transaction does not work | pool.query uses different connections | Use pool.connect |
| Connection leak | client.release() not called | Use try/finally |
| Process crashes on idle error | No pool.on('error') | Add the handler |
| Pool exhausted | Connections leaked or max too low | Release connections, tune max |
ECONNREFUSED | Wrong config or database down | Check the config and the database |
timeout exceeded when trying to connect | Pool exhausted, no timeout | Increase max, set connectionTimeoutMillis |
| Stale connection error | Firewall or wait_timeout | Enable keepalive, validate the connections |
| Slow shutdown | pool.end() not called | Call it in the signal handler |
Real-World Examples
1. The Pool Configuration
const pool = new Pool({ max: 20, idleTimeoutMillis: 30000 });
2. The Simple Query
const result = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
3. The Transaction
const client = await pool.connect();
try { await client.query('BEGIN'); ... } finally { client.release(); }
4. The Error Handler
pool.on('error', (err) => console.error(err));
5. The Graceful Shutdown
process.on('SIGTERM', async () => { await pool.end(); process.exit(0); });
6. The Pool Metrics
console.log(pool.totalCount, pool.idleCount, pool.waitingCount);
7. The mysql2 Pool
const pool = mysql.createPool({ connectionLimit: 20, waitForConnections: true });
8. The mysql2 Transaction
const conn = await pool.getConnection();
try { await conn.beginTransaction(); ... } finally { conn.release(); }
9. The Keepalive
mysql.createPool({ enableKeepAlive: true, keepAliveInitialDelay: 0 });
10. The Complete Application
app.get('/users/:id', async (req, res) => {
const user = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
res.json(user.rows[0]);
});
Visual
The Pool Lifecycle
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE POOL LIFECYCLE โ
โ โ
โ pool.query() โ
โ โ โ
โ โโโ Check for an idle connection โ
โ โ โ
โ โโโ If found: reuse it โ
โ โ โ
โ โโโ If not found and below max: create a new one โ
โ โ โ
โ โโโ If at max: wait for a connection to be released โ
โ โ โ
โ โผ โ
โ Run the query on the connection โ
โ โ โ
โ โผ โ
โ Release the connection to the pool โ
โ โ โ
โ โโโ If idle > idleTimeoutMillis: close it โ
โ โ โ
โ โโโ Otherwise: keep it for the next query โ
โ โ
โ The pool grows and shrinks between min and max. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Connection Leak
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE CONNECTION LEAK โ
โ โ
โ WITHOUT release: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Request 1: connect() โ client 1 (not released) โ โ
โ โ Request 2: connect() โ client 2 (not released) โ โ
โ โ Request 3: connect() โ client 3 (not released) โ โ
โ โ ... โ โ
โ โ Request 21: connect() โ wait (max=20) โ โ
โ โ Request 21: timeout โ reject โ โ
โ โ โ โ
โ โ The pool is exhausted. โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ WITH release: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Request 1: connect() โ client 1 โ โ
โ โ Request 1: release() โ client 1 is idle โ โ
โ โ Request 2: connect() โ reuse client 1 โ โ
โ โ Request 2: release() โ client 1 is idle โ โ
โ โ ... โ โ
โ โ โ โ
โ โ The pool is reused. โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Transaction
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE TRANSACTION โ
โ โ
โ const client = await pool.connect() โ
โ โ โ
โ โผ โ
โ client = the checked-out connection โ
โ โ โ
โ โโโ client.query('BEGIN') โ
โ โ โ
โ โโโ client.query('UPDATE ...') โ
โ โ โ
โ โโโ client.query('UPDATE ...') โ
โ โ โ
โ โโโ client.query('COMMIT') or client.query('ROLLBACK') โ
โ โ โ
โ โผ โ
โ client.release() โ
โ โ โ
โ โผ โ
โ The connection returns to the pool โ
โ โ
โ All the statements run on the SAME connection. โ
โ pool.query() does NOT guarantee this. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Graceful Shutdown
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE GRACEFUL SHUTDOWN โ
โ โ
โ SIGTERM received โ
โ โ โ
โ โผ โ
โ server.close() โ
โ โ Stop accepting new requests โ
โ โ Wait for the in-flight requests โ
โ โผ โ
โ pool.end() โ
โ โ Wait for the in-flight queries โ
โ โ Close all the connections โ
โ โผ โ
โ process.exit(0) โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Without pool.end(): โ โ
โ โ The connections are left open. โ โ
โ โ The database keeps the sessions until timeout. โ โ
โ โ โ โ
โ โ With pool.end(): โ โ
โ โ The connections are closed cleanly. โ โ
โ โ The database releases the sessions immediately. โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Connection pool | A set of reusable connections |
pool.query() | Checkout, query, release |
pool.connect() | Explicit checkout |
client.release() | Return the connection |
max / connectionLimit | Maximum connections |
idleTimeoutMillis / idleTimeout | Close idle connections |
connectionTimeoutMillis | Wait time for a connection |
pool.on('error') | Idle client error handler |
pool.end() | Close all connections |
| Transaction | pool.connect() + BEGIN/COMMIT/ROLLBACK |
Key takeaways:
- A connection pool opens a set of connections once and reuses them across the queries. The pool amortizes the connection cost and bounds the number of connections, so the database’s
max_connectionsis not exhausted . pool.query()checks out a connection, runs the query, and releases it automatically. It is the correct choice for a single query.pool.connect()checks out a connection explicitly, and the application must release it withclient.release().- A transaction requires a checked-out connection. The
BEGIN, the statements, and theCOMMITmust run on the same connection. Thepool.query()method does not guarantee this. Thepool.connect()method is the correct choice . - The
client.release()must be in afinallyblock. Without the release, the connection leaks, the pool is exhausted, and the nextconnect()waits until theconnectionTimeoutMillisexpires and then rejects. - The pool emits an
errorevent for idle client errors. Without a handler, the event is unhandled, and the process crashes. The handler logs the error, and the pool removes the failed connection. - The pool must be ended on shutdown with
pool.end(). The method closes all the connections and waits for the in-flight queries. TheSIGTERMandSIGINThandlers are the standard places to call it. - The pool’s state is observable. The
totalCount,idleCount, andwaitingCountproperties show the pool’s health. A growingwaitingCountindicates the pool is exhausted, and a growingtotalCountindicates the connections are leaking. - The pool size is a trade-off. A larger pool handles more concurrency but uses more database resources. A smaller pool uses fewer resources but queues the requests sooner. The size should be tuned based on the measurements.
Remember: A connection pool is a stateful resource with a lifecycle. The pool.query() method is the simple API for a single query, and the pool.connect() method is the explicit API for a transaction. The client.release() must be in a finally block, the pool.on('error') handler is required, and the pool.end() must be called on shutdown. The pool’s state is observable, and the pool size is a trade-off. The result is an application that reuses the connections, respects the database’s limits, and shuts down gracefully.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!