The page arrives looking the same every time:
ERROR 1040 (HY000): Too many connections
Someone raises max_connections, the errors stop for a week, and then the same incident returns with a larger blast radius. The setting was never the constraint. max_connections is a guard rail; when you hit it, something else — a slow query, a lock wait, a connection pool sized by arithmetic nobody checked — has already gone wrong upstream, and the guard rail is the first place it became visible.
Here is the order we work through it.
First: is the server busy, or is it stuck?
These look identical from the application and need opposite responses.
SELECT VARIABLE_VALUE AS threads_connected
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Threads_connected';
SELECT VARIABLE_VALUE AS threads_running
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Threads_running';
Threads_connected is how many sessions exist. Threads_running is how many are actually executing at this instant. A healthy OLTP server usually runs single digits to low tens of running threads even with hundreds connected; most connections are idle between statements, which is what a pool is for.
- Connected high, running low. The application is holding connections it is not using. Pool sizing or connection leaks. Nothing is wrong inside MySQL.
- Connected high, running also high (say 100+). Real concurrency, and probably a pile-up: one bad plan or one lock holder is making every caller wait, so the next request arrives before the last one finished. Look at what is running, not at
max_connections.
For the second case, the list of offenders is one query away:
SELECT id, user, host, db, command, time, state, LEFT(info, 120) AS stmt
FROM information_schema.processlist
WHERE command <> 'Sleep'
ORDER BY time DESC
LIMIT 30;
If thirty sessions are all in Sending data on the same statement, you have one missing index, not a connection problem. If they are in updating or waiting on a lock, see the lock-wait evidence trail: a single long transaction can convert into hundreds of queued connections in under a minute.
The arithmetic nobody checks
Most connection exhaustion in fleets we review is not caused by traffic. It is caused by multiplication.
Each application instance carries a pool. HikariCP defaults to 10. Say 40 service pods, three services sharing the database, plus workers, plus a cron box, plus two ad-hoc BI connections. That is comfortably over a thousand possible connections against a max_connections of 500, and the day it all scales up together is the day you find out.
Write the number down explicitly:
(instances x pool_max) summed over every service
+ background workers
+ migrations and deploy tooling
+ human and BI clients
+ a reserve for your own emergency session
That total must sit under max_connections, and max_connections must sit under what the server's memory can support. Two habits make the reserve real: keep max_connections above your computed ceiling by a margin, and know that MySQL reserves one extra slot for a SUPER/CONNECTION_ADMIN user precisely so you can log in during an outage. On MySQL 8.0 and later you can also set admin_address and admin_port for a dedicated administrative interface with its own connection allowance — worth configuring before you need it, not during.
Why raising max_connections has a cost
MySQL allocates per-connection buffers lazily but genuinely: sort_buffer_size, join_buffer_size, read_rnd_buffer_size, tmp_table_size, plus the net and thread stacks. A session that runs a filesort with a 4 MB sort buffer and a couple of joins can hold tens of megabytes. Multiply by 2,000 connections and you are in territory where the OOM killer, not the optimizer, decides your availability.
The trade-off is concrete: memory spent on per-connection buffers is memory not in the InnoDB buffer pool, and buffer pool hit ratio is usually worth more than connection headroom. If you must raise max_connections, lower the session-level buffer defaults at the same time and set them per-query in the few places that genuinely need a big sort.
There is a scheduling cost too. Beyond roughly the number of CPU threads, more concurrently running threads does not increase throughput — it increases context switching and contention while every query gets slower. That is the shape of the classic throughput cliff: fine up to a point, then latency goes vertical while completed transactions per second fall. MySQL Enterprise and Percona Server offer a thread pool to cap executing threads and queue the rest; in Community MySQL, the equivalent is a properly sized application pool or a proxy layer that does the queuing for you.
Where a pooler belongs
A middle-tier pooler — ProxySQL, RDS Proxy, or a language-native pool used well — is the right fix for specific shapes of this problem:
- Serverless or very high instance counts (Lambda, autoscaled pods) where each instance's pool is small but the instance count is unbounded. Multiplexing many client connections onto a smaller set of backend connections is exactly what this is for.
- Connection storms after a failover, where every application instance reconnects at once. A proxy absorbs the reconnect and keeps the database from being the thundering-herd target.
- Read/write split and query routing, where ProxySQL's rule engine sends reads to replicas without application changes, and can mirror, throttle, or rewrite a specific bad query while you ship a real fix.
What a pooler does not do: make a slow query fast. Put a proxy in front of a database whose problem is a table scan and you convert Too many connections into growing queue latency inside the proxy — the same outage with a less informative error message. Fix the plan first; add the proxy for connection economics.
Two implementation notes that catch teams out. Transaction-level multiplexing means a session variable, a temporary table, or a prepared statement set on one backend connection may not be there on the next statement — RDS Proxy pins the session when it detects these, and pinning quietly erases the benefit, so check your pinning metrics. And the proxy is now in the availability path: it needs its own redundancy, its own monitoring, and a tested answer to "what happens when the proxy restarts mid-transaction".
A short runbook
When 1040 fires:
- Get in on the reserved or admin connection. Check
Threads_running. - If running is high, read the processlist and kill or fix the dominant statement. Capture it before killing it.
- If running is low and connected is high, the problem is upstream: leaked connections, a pool without a max, or a deploy that doubled instance count.
- Only then consider
max_connections, and raise it with the memory arithmetic done, not as a reflex. - Afterwards, alert on
Threads_connectedas a percentage ofmax_connections(70% is a reasonable first threshold) and onThreads_running, so the next occurrence is a warning rather than an outage.
Connection limits are one of the few database failures that are almost always a symptom. Treated that way, they are also one of the quickest to resolve for good: the arithmetic takes an afternoon, and the query that caused the pile-up was going to need fixing anyway.
If you are staring at a connection graph that climbs every afternoon and nobody can say which service owns the connections, that is a good thing to work through with someone who has mapped this before. Send us the pool configuration and an hour of Threads_connected and Threads_running, and the shape is usually clear from the two lines together.