If this error brought you here — you'll be back up in 3 minutes
You'll see the following verbatim message in your application logs or console.
ERROR 1040 (HY000): Too many connectionsThis is not a syntax error or a query error. It means the MySQL/MariaDB server has exhausted every connection slot (max_connections) and is refusing new connections. PostgreSQL's FATAL: sorry, too many clients already is a backend-process ceiling; MySQL is different in that it manages connections as threads inside a single server process. The server itself is still alive — the slots are just full. Cleaning up idle Sleep connections recovers the service immediately.
Scope: MySQL 5.7 / 8.0, MariaDB 10.x, AWS RDS/Aurora MySQL-compatible. This post is structured as "commands first, explanation later." If production is down right now, copy-paste the steps below in order.
- Step 1: Diagnose (30 seconds) → confirm slot exhaustion
- Step 2: Recover → connect via the reserved root slot + kill + temporary raise
- Step 3: Prevent recurrence → my.cnf tuning + connection pool settings
30-second confirmation: three commands to tell if the slots are full
If you can connect at all (a monitoring connection, an admin session, etc.), run the following. Even if the app account is being refused, root usually still gets in because of a reserved slot (see the next section).
-- Current open connection count
SHOW STATUS LIKE 'Threads_connected';
-- Highest connection count reached since server start
SHOW STATUS LIKE 'Max_used_connections';
-- Allowed ceiling
SHOW VARIABLES LIKE 'max_connections';
-- Every session occupying a slot (who's the culprit)
SHOW FULL PROCESSLIST;
-- Cumulative connects aborted by auth/network failure
SHOW STATUS LIKE 'Aborted_connects';Read the output like this.
| Item | Healthy | Exhausted | Meaning |
|---|---|---|---|
Threads_connected | Headroom below max_connections | At or equal to max_connections | Connections open right now |
Max_used_connections | Below max_connections | Equal to max_connections | Proof the ceiling was hit at least once |
max_connections | Adequate for the workload | Stuck at the default of 151 | Allowed slot ceiling |
Aborted_connects | Stays low | Climbing fast | Auth failures / timeouts (a separate cause from slot exhaustion) |
If Max_used_connections equals max_connections, slot exhaustion is confirmed.
In SHOW FULL PROCESSLIST output, look at the Command and Time columns.
+-----+--------+-----------------+------+---------+------+-------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+-----+--------+-----------------+------+---------+------+-------+------------------+
| 812 | appuser| 10.0.1.20:51022 | shop | Sleep | 240 | | NULL |
| 813 | appuser| 10.0.1.20:51044 | shop | Sleep | 238 | | NULL |
| 820 | appuser| 10.0.1.21:33002 | shop | Query | 0 | ... | SELECT ... |
+-----+--------+-----------------+------+---------+------+-------+------------------+If you see a pile of connections with Command = Sleep and a large Time, that's a leak signal: the application connection pool is holding connections instead of returning them. If connections are concentrated on a particular Host (pod IP or server IP), that app is the culprit.
Cause branching: is this really slot exhaustion, or something else?
| Observed signal | Actual cause | Response |
|---|---|---|
Threads_connected ≈ max_connections, lots of Sleep | Connection pool leak / oversized pool | kill + adjust pool size and idle timeout |
Aborted_connects spiking, Threads_connected has headroom | Bad password, firewall, or network drops | Not a slot problem. Check auth/network |
| Spike right after a pod scale-out | pods × pool size > ceiling | Shrink pool size or introduce RDS Proxy/ProxySQL |
| Low ceiling on RDS/Aurora | Default tied to instance class | Adjust max_connections in the parameter group |
If only Aborted_connects is climbing while Threads_connected has headroom, 1040 is a secondary symptom and the real issue is likely an auth storm or a network problem — change direction.
Immediate recovery: get in via the reserved root slot and kill
MySQL reserves one slot for accounts with CONNECTION_ADMIN (8.0) or SUPER (5.7). That's why root often still connects even when the app account is refused with 1040. Get in first.
mysql -u root -pOnce connected, clean up Sleep connections. Individual kills look like this.
KILL 812;
KILL 813;If there are dozens and you don't want to type them one by one, auto-generate the kill statements and run them in a batch.
-- Generate KILL statements for connections that have been Sleep for 10+ minutes (600 seconds)
SELECT CONCAT('KILL ', id, ';') AS kill_stmt
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 600;Copy the printed KILL ...; statements and execute them. For a shell one-liner, mysqladmin is convenient.
# Inspect the current process list
mysqladmin -u root -p processlist
# Force-terminate a specific id
mysqladmin -u root -p kill 812
# Pull Sleep connection ids and kill them in one shot (shell one-liner)
mysql -u root -p -N -e \
"SELECT id FROM information_schema.processlist WHERE command='Sleep' AND time>600" \
| while read id; do mysqladmin -u root -p'YOUR_PW' kill "$id"; done⚠️ Putting the password in
-p'PW'leaves it in shell history. After the emergency recovery, clear it withhistory -cor use.my.cnf.
If traffic refills the slots as soon as you free them, raise the ceiling temporarily without a restart.
SET GLOBAL max_connections = 500;
-- Confirm it took effect
SHOW VARIABLES LIKE 'max_connections';This is a runtime-only temporary measure. A MySQL restart reverts to the my.cnf value. Use it to put out the fire, then you must still do Step 3 below: persist the setting and fix the root cause (the pool leak). Also remember that blindly raising the ceiling increases OOM risk because of per-connection memory (sort/join buffers, etc.).
Recurrence prevention 1: my.cnf server tuning
Set permanent values in the [mysqld] section of /etc/my.cnf or /etc/mysql/my.cnf.
[mysqld]
# Concurrent connection ceiling (size it against workload and memory)
max_connections = 500
# Per-account ceiling so one account cannot monopolize slots
max_user_connections = 200
# Max seconds a non-interactive (app) connection may stay idle (default 28800s = 8 hours is the problem)
wait_timeout = 600
# Idle ceiling for interactive sessions such as the mysql client
interactive_timeout = 600Dropping wait_timeout from 8 hours to 10 minutes lets the server reclaim idle connections the pool failed to return. Restart after applying, then verify.
sudo systemctl restart mysqldSHOW VARIABLES LIKE 'max_connections';
SHOW VARIABLES LIKE 'max_user_connections';
SHOW VARIABLES LIKE 'wait_timeout';If each value matches what you set, you're done. If they didn't take effect, you may have edited a file that isn't actually loaded — check the load path with mysqld --help --verbose | grep -A1 "Default options". On RDS/Aurora you adjust a parameter group instead of my.cnf, and Aurora's default max_connections is computed from instance-class memory, so you either scale the instance or explicitly override the parameter (check the official parameter docs).
Recurrence prevention 2: framework-specific connection pool settings
The root cause is almost always the application. Core rule: set the connection max lifetime (maxLifetime) shorter than the server wait_timeout, so the app cleans up before the server cuts the connection.
HikariCP (Spring Boot)
# application.properties
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=300000 # 5 minutes
spring.datasource.hikari.max-lifetime=570000 # 9.5 minutes < wait_timeout(600s)
spring.datasource.hikari.connection-timeout=3000Keeping max-lifetime strictly shorter than wait_timeout is the key. Otherwise the pool tries to reuse a connection the server already closed, and you get errors.
Django
# settings.py
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"CONN_MAX_AGE": 60, # max seconds a connection may be reused. shorter than wait_timeout
"OPTIONS": {"connect_timeout": 5},
}
}Django closes connections at the end of a request, but setting CONN_MAX_AGE to infinity (None) or too high lets workers × connections accumulate. Keep it smaller than wait_timeout.
Laravel / PDO
// config/database.php
'mysql' => [
// ...
'options' => [
PDO::ATTR_PERSISTENT => false, // ★ persistent connections leak and linger; keep the default false
PDO::ATTR_TIMEOUT => 5,
],
],PDO::ATTR_PERSISTENT => true pins connections to the process and easily causes slot exhaustion. Unless you have a specific reason, leave it off.
Node.js (mysql2)
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: 'db.internal',
user: 'appuser',
database: 'shop',
connectionLimit: 15, // max connections per instance
waitForConnections: true,
queueLimit: 0,
idleTimeout: 60000, // reclaim idle connections (60 seconds)
enableKeepAlive: true,
});The container / serverless trap
If Kubernetes scales to 10 pods and each pod has connectionLimit=15, you instantly demand 150 connections. Always size the ceiling as (pod/worker count) × (pool size). Serverless (Lambda, etc.) explodes connections with concurrency, so in practice the standard is increasingly to put connection-pooling middleware such as RDS Proxy or ProxySQL in front of the app pool and control the actual DB connection count there.
Wrap-up: diagnose → recover → prevent checklist
One-screen summary.
-- ① Diagnose
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Max_used_connections';
SHOW VARIABLES LIKE 'max_connections';
SHOW FULL PROCESSLIST;
-- ② Immediate recovery (after connecting as root)
SELECT CONCAT('KILL ', id, ';') FROM information_schema.processlist
WHERE command='Sleep' AND time>600; -- copy the output and run it
SET GLOBAL max_connections = 500; -- temporary (resets on restart)
-- ③ Permanent prevention: max_connections/wait_timeout in my.cnf,
-- maxLifetime < wait_timeout on the app pool- Confirm exhaustion by whether
Max_used_connectionshit the ceiling - Free slots immediately by killing Sleep connections
- Treat
SET GLOBALas temporary and persist it in my.cnf - Recalculate that the sum of every app pool size does not exceed the ceiling
- Set monitoring alerts on
Threads_connectedandAborted_connects(alert at 80% of the ceiling)
The real key to preventing recurrence is monitoring. If you alert when Threads_connected exceeds 80% of max_connections, you can act before you ever see 1040 again.
FAQ
Q. I raised it with SET GLOBAL max_connections, but after a restart it went back.
A. That's expected. SET GLOBAL is a runtime-only value and reverts to the my.cnf setting on restart. To persist it, put max_connections in [mysqld], restart, then confirm with SHOW VARIABLES LIKE 'max_connections';.
Q. Even root can't connect — I get ERROR 1040.
A. The reserved slot is also exhausted (another admin session is occupying it), or the account lacks CONNECTION_ADMIN/SUPER. Retry over the local socket (mysql -u root -p --socket=/var/run/mysqld/mysqld.sock). If that still fails, try terminating with mysqladmin, and as a last resort restart the service to reset the slots.
Q. I killed the Sleep connections and they filled right back up.
A. The application connection pool is still opening new connections and not returning them — a leak. Recalculate whether pods/workers × pool size exceeds the ceiling, set HikariCP max-lifetime shorter than wait_timeout, and if needed concentrate connections through RDS Proxy or ProxySQL.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.