MySQL2 Connection Pooling: What Happens Without It at Scale
Per-request connections worked fine in development. In production with 32 PM2 workers across 8 brands, connection overhead became the bottleneck — not CPU or memory. The createPool settings that eliminated connection errors under peak concurrent load.
When I first moved from Prisma to MySQL2 for data-intensive operations, I set up a basic connection without pooling. Create a connection, run the query, close the connection. Straightforward for development. A real problem in production.
Problem
Each new database connection has overhead: authentication handshake, session setup, configuration negotiation. On a local dev machine with low concurrency this is not noticeable. On a production server handling 100+ concurrent store sessions across 8 brands, it adds up fast.
Without a connection pool, when two requests arrive simultaneously, both open new connections. At 32 PM2 worker processes across 8 brands, each handling concurrent requests, you can easily hit MySQL max_connections. When that happens, new connection attempts fail. Requests error out with connection refused.
I saw this during peak morning hours when all store managers were logging in around the same time. The server had capacity. MySQL had capacity. But connection overhead was the bottleneck.

Solution
MySQL2 createPool with explicit configuration.
The key settings:
- connectionLimit: 20 per brand instance. Enough for concurrent requests without hitting server-level limits.
- waitForConnections: true. Requests that cannot get a connection immediately wait rather than fail.
- queueLimit: 0. No queue limit so requests wait as long as needed.
- enableKeepAlive: true. Prevents MySQL from closing idle connections before the pool expects.
- keepAliveInitialDelay: 10000. Keeps idle connections alive to avoid cold reconnection on the first request after a quiet period.
The pool is created once at application startup and reused for the lifetime of the process. Connection acquisition from the pool is cheap. Release happens via connection.release() after each operation.
For batch operations: get a connection from the pool, run the full transaction, release it. The connection stays open and returns to the pool for the next request.

Result
Connection errors went to zero after implementing the pool.
Peak-hour request handling stabilized. Concurrent operations across brands stopped competing for connection resources.
The pool also improved average response time by eliminating per-request connection setup overhead. Requests that previously spent 10 to 50ms on connection setup now pull from a ready pool.
Connection pooling is not optional for a production Node.js app with real concurrent load. It is the baseline.
Found this useful?
Share it with someone who'd appreciate it.
https://wardvisual.com/blogs/connection-pooling-mysql2-production

@wardvisual · 🇵🇭 Dasmarinas City, Cavite PH
Full-stack engineer. Business systems, database optimization, and operations software.