Fix MySQL Error 1040 "Too Many Connections" in Django
Fix MySQL error 1040 Too many connections from Django: count connections per Gunicorn worker, Celery process, and thread, tune max_connections and wait_timeout, set CONN_MAX_AGE sensibly, and add ProxySQL or RDS Proxy when apps scale out.
Problem statement
Under load, or right after scaling out, requests start failing:
django.db.utils.OperationalError: (1040, 'Too many connections')
MySQL accepts at most max_connections client connections at once (151 by default on a self-managed server). Every Django process that talks to the database holds its own connection, and with persistent connections it keeps it between requests. Add up web workers, threads, Celery processes, cron jobs, and admin tools across every server, and the total can pass the limit without any single process doing anything wrong.
- Count current connections by source:
SELECT user, host, COUNT(*) FROM information_schema.PROCESSLIST GROUP BY user, host; - Calculate the demand: (Gunicorn workers × threads + Celery concurrency + other processes) × number of servers.
- Raise
max_connectionsif the server has memory for it, and lowerwait_timeoutso abandoned sessions close. - Set
CONN_MAX_AGEto a modest value (60 s) and enableCONN_HEALTH_CHECKS. - When the math still does not fit, add a pooler: ProxySQL on your own servers, RDS Proxy on AWS.
Step-by-step solution
1. See who holds the connections
SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';
SELECT user, SUBSTRING_INDEX(host, ':', 1) AS client, command, COUNT(*) AS n
FROM information_schema.PROCESSLIST
GROUP BY user, client, command
ORDER BY n DESC;
Many Sleep connections from app servers mean persistent connections are idling. Many Query connections mean slow queries are piling up, which is a performance problem rather than a configuration one.
2. Do the arithmetic
For each server:
web: 3 Gunicorn workers x 4 threads = 12
celery: concurrency 8 = 8
beat, cron, shell = 3
total per server = 23
x 6 servers = 138
Add a deploy that briefly runs old and new processes side by side, and you exceed 151. Threads count separately: each thread in a gthread worker opens its own connection.
3. Tune the server
# my.cnf
[mysqld]
max_connections = 500
wait_timeout = 300
interactive_timeout = 300
Each connection costs memory for buffers (sort_buffer_size, join_buffer_size, thread stack), so check RAM before raising the limit greatly. wait_timeout closes connections idle longer than the value; keep CONN_MAX_AGE below it, or Django will reuse closed connections and hit "MySQL server has gone away". On RDS, max_connections is derived from instance memory by default; change it in the parameter group.
4. Tune Django
DATABASES["default"].update({
"CONN_MAX_AGE": 60,
"CONN_HEALTH_CHECKS": True,
})
CONN_MAX_AGE = None keeps connections forever, which maximizes idle connections. 0 closes after each request, which minimizes them at the cost of reconnect time. A short age with health checks is a good middle ground. Also reduce Gunicorn threads if they mostly sit idle.
5. Pool connections when you scale out
A pooler lets hundreds of client connections share a few dozen server connections:
- ProxySQL runs on each app server or as a central service and multiplexes connections.
- RDS Proxy does the same for RDS and Aurora, and also smooths failovers.
Point Django at the proxy instead of the database. Session state such as temporary tables or SET variables can pin connections and reduce multiplexing; avoid them in application code.
Explanation
MySQL uses a thread per connection, and max_connections protects the server from running out of memory or threads. Django opens a connection lazily on the first query in a thread and, with CONN_MAX_AGE, keeps it for reuse. That is efficient for one process, but connections are not shared between processes or threads. Horizontal scaling multiplies them, and so does any process type you add. The fix is either fewer connections per process, more capacity on the server, or a pooler that decouples the two.
Edge cases / notes
- Reserved connection. MySQL keeps one extra connection for a user with
CONNECTION_ADMIN(orSUPER) so an admin can log in when the limit is hit. Do not run the app with that privilege. - Per-user limits.
max_user_connectionscan produce error 1203 instead. Check both. - Leaking scripts. Long-running scripts that open connections in threads without closing them grow without bound. Call
connection.close()when threads finish.
Next steps
- If connections drop instead, see Fix MySQL Server Has Gone Away in Django.
- Choose connection lifetime with Use Persistent Database Connections in Django.
- Running PostgreSQL instead? Read Fix PostgreSQL Too Many Connections in Django.
FAQ
What is a safe max_connections value?
It depends on memory. A few hundred is common for small and medium servers. Beyond that, a pooler is usually better than a larger limit.
Does CONN_MAX_AGE cause too many connections?
Long or unlimited values keep idle connections open, which adds up across processes. A short value with health checks keeps reuse benefits without the buildup.
Why did the error start after scaling out?
Each new server adds its full set of worker connections. The total crosses the limit even though each server looks fine.
Is ProxySQL required?
Not for small deployments. It becomes useful when many servers or many Celery processes share one MySQL server.