| |

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)

OptionDefaultPurpose
max10Maximum connections
min0Minimum connections
idleTimeoutMillis10000Close idle connections after
connectionTimeoutMillis0Wait time for a connection
maxUsesInfinityClose after N uses
allowExitOnIdlefalseAllow exit when idle

The Pool Configuration (mysql2)

OptionDefaultPurpose
connectionLimit10Maximum connections
maxIdle10Maximum idle connections
idleTimeout60000Close idle connections after
queueLimit0Max queued requests
waitForConnectionstrueQueue when exhausted
enableKeepAlivefalseKeep connections alive

The Two APIs

APIUse 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

EventPurpose
errorAn idle client error
connectA new client connected
acquireA client was checked out
releaseA client was released
removeA client was removed

The Pool Metrics

MetricMeaning
totalCountTotal connections
idleCountIdle connections
waitingCountWaiting 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

PitfallWhy It HappensFix
Transaction does not workpool.query uses different connectionsUse pool.connect
Connection leakclient.release() not calledUse try/finally
Process crashes on idle errorNo pool.on('error')Add the handler
Pool exhaustedConnections leaked or max too lowRelease connections, tune max
ECONNREFUSEDWrong config or database downCheck the config and the database
timeout exceeded when trying to connectPool exhausted, no timeoutIncrease max, set connectionTimeoutMillis
Stale connection errorFirewall or wait_timeoutEnable keepalive, validate the connections
Slow shutdownpool.end() not calledCall 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

ItemValue
Connection poolA set of reusable connections
pool.query()Checkout, query, release
pool.connect()Explicit checkout
client.release()Return the connection
max / connectionLimitMaximum connections
idleTimeoutMillis / idleTimeoutClose idle connections
connectionTimeoutMillisWait time for a connection
pool.on('error')Idle client error handler
pool.end()Close all connections
Transactionpool.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_connections is 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 with client.release() .
  • A transaction requires a checked-out connection. The BEGIN, the statements, and the COMMIT must run on the same connection. The pool.query() method does not guarantee this. The pool.connect() method is the correct choice .
  • The client.release() must be in a finally block. Without the release, the connection leaks, the pool is exhausted, and the next connect() waits until the connectionTimeoutMillis expires and then rejects.
  • The pool emits an error event 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. The SIGTERM and SIGINT handlers are the standard places to call it.
  • The pool’s state is observable. The totalCount, idleCount, and waitingCount properties show the pool’s health. A growing waitingCount indicates the pool is exhausted, and a growing totalCount indicates 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!