max_connections. Sometimes the error goes away for a day. Then it comes back, or the server starts swapping, or MySQL gets killed by the kernel. That pattern usually means the limit was never the real problem.
The misconceptions below come up often around this error. For each one: why it feels reasonable, and what is usually going on underneath.
Myth 1: The limit is simply too low for my traffic
- A missing index, so a query scans a large table.
- Lock waits. A long write or an
ALTER TABLEholds a lock, and everything behind it queues up. In the process list this often shows as 'Waiting for table metadata lock'. - Slow storage, where every query waits on disk.
Myth 2: A higher max_connections only costs a few kilobytes
sort_buffer_size, join_buffer_size, read_buffer_size, and read_rnd_buffer_size. In-memory temporary tables can grow up to the smaller of tmp_table_size and max_heap_table_size. Global memory like innodb_buffer_pool_size is shared. These per-connection pieces are not.
The usual worst-case estimate is global buffers plus max_connections times the per-session buffers. That formula overestimates, since not every connection allocates every buffer at once. The direction is still right. The moment connections pile up is exactly when queries are heavy, so it is also when per-connection memory peaks.
On a small VPS this plays out in a predictable way. Memory runs out, the system starts swapping, and eventually the kernel's OOM killer picks a process to stop. mysqld is often the largest process, so it gets chosen. A connection error turns into a full database outage, followed by InnoDB crash recovery on restart.
One more detail: the value you set is not always the value MySQL uses. If the open file limit is too low, MySQL may lower the effective max_connections at startup and log a warning. After any change, check the running value instead of trusting the config file.
Myth 3: Every connection in the process list is doing something
Sleep in the Command column. Those connections are open and idle, and each still takes a slot.
Idle connections pile up for a few common reasons:
- Code that opens a connection and never closes it, especially on error paths, or in long-running workers that open a fresh connection per job.
- PHP persistent connections, enabled with the
p:host prefix in mysqli orPDO::ATTR_PERSISTENT. Each PHP worker keeps its own connection open between requests. The connection count then follows the number of PHP workers, not your traffic. - Connection pools that are sized larger than needed, or that leak connections which are never returned. If the pool sits behind a proxy, socket handling adds another layer. That is covered in this piece on socket leaks and pools behind Envoy.
pm.max_children across all pools that talk to that database. If that sum is higher than max_connections, the error is a sizing mismatch. The cleaner fix is often to lower worker counts to what the server can serve, not to raise the database limit.
wait_timeout controls how long an idle connection lives before the server closes it. In many MySQL builds the default is 8 hours, so a slow leak has plenty of time to fill every slot. Lowering it cleans up leftovers. Treat it as a backstop, though, not the fix. An app that keeps a connection around and expects it to still be alive will start logging 'MySQL server has gone away' instead.
Myth 4: The error always means the server-wide limit was hit
max_user_connections, and it produces a different error: 'User ... has exceeded the 'max_user_connections' resource' (error 1203). The server as a whole may have plenty of room left.
This gets confused a lot because applications rarely show the raw MySQL error. WordPress, for example, shows its generic 'Error establishing a database connection' page, and that is what ends up in a support chat. Before changing anything, find the actual MySQL error in the application log or the PHP error log. On a shared server the account owner usually cannot change either limit. Fixing the workload is the only lever available anyway.
It also helps to know that MySQL reserves one extra connection beyond max_connections for an administrative account, one with SUPER or, in MySQL 8.0, CONNECTION_ADMIN. When the app is locked out, that slot is often how you get in to look around. Do not let your application connect with that account, or the spare slot is gone when you need it.
Deciding whether the limit or the workload should change
PROCESS privilege to see other users' connections in the process list.
SQL
SHOW GLOBAL VARIABLES LIKE 'max_connections';
SHOW GLOBAL VARIABLES LIKE 'wait_timeout';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW FULL PROCESSLIST;Max_used_connections is the peak since the server last started. If it equals or sits right at max_connections, you have hit the ceiling at some point. The more useful readings come from catching the problem while it is happening:
- If
Threads_runningis high and the process list shows many queries with large values in the Time column, queries are stuck. Read the State column for lock waits. Turn on the slow query log (slow_query_logwith a sensiblelong_query_time) to find what is slow. The fix is an index, a rewritten query, or moving heavy jobs away from peak hours. - If
Threads_connectedis high butThreads_runningis low, and most rows showSleep, the problem is connection handling. Look at persistent connections, worker counts, pool sizes, and code that does not close connections. Then consider a shorterwait_timeoutas cleanup. - If connections are short-lived, queries are fast, and you legitimately need more concurrency, for example because you deliberately added app servers or workers, raising
max_connectionsis the right move. Budget the memory first, and confirm the new value is actually in effect after the restart.
