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. It would commit 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