Applications and operations

Use a bounded connection pool

Budget connections across every process and keep all transaction statements on one checked-out connection with reliable cleanup.

Opening a MySQL connection costs a TCP handshake, a TLS negotiation, and authentication. Paying that on every query would dominate the query time itself. A pool avoids repeated connection setup and queues work when all its connections are busy — that queue is what makes the pool bounded, and the bound is what protects the server.

The server enforces its own limit: max_connections, 151 by default. When the combined demand of every client exceeds it, new connections fail with ERROR 1040 (HY000): Too many connections — including yours, when you try to connect and investigate.

So count the pool size across every application process, not per file. Ten replicas with a pool of ten can open one hundred connections, before workers, migrations, and administration. Check real usage on the server anytime:

SHOW STATUS LIKE 'Threads_connected';

Compare that number with max_connections and keep headroom for an emergency administrative session.

Transactions need one connection

A transaction must stay on one checked-out connection, because MySQL scopes the transaction to the connection that started it:

const connection = await pool.getConnection()

try {
  await connection.beginTransaction()

  const [note] = await connection.execute(
    'INSERT INTO notes (title) VALUES (?)',
    ['Plan the week']
  )

  await connection.execute(
    'INSERT INTO note_tags (note_id, tag_id) VALUES (?, ?)',
    [note.insertId, 1]
  )

  await connection.commit()
} catch (error) {
  await connection.rollback()
  throw error
} finally {
  connection.release()
}

Do not call pool.execute() inside that transaction. The pool may choose another connection, and that statement would run outside the transaction — committed immediately, invisible to your rollback.

The finally block is not decoration. Always roll back after an error and release in finally. A code path that returns or throws without connection.release() leaks that connection: the pool believes it is still busy forever. Leak a few and every request starts waiting on an empty pool, which looks exactly like a database outage. If the application slows down while Threads_connected sits at your pool limit and the database itself is idle, look for a missing release() before anything else.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →