When MySQL hits its connection limit, it stops accepting new connections entirely. Every subsequent connection attempt fails with “Too many connections” and your application returns a database error. On a WordPress site, this usually surfaces as Error establishing a database connection because WordPress cannot open a new connection to MySQL.
This error does not mean your database is corrupt or your data is lost. It means the server has reached the maximum number of simultaneous connections it is configured to allow, and there are no free slots for new ones. The fix depends on whether the limit is too low, whether something is leaking connections, or whether you have a legitimate traffic spike that exceeds your server’s capacity.
What causes the error#
MySQL maintains a pool of connection slots controlled by the
max_connections
variable. The default is 151. Each PHP request that connects to MySQL occupies one slot for the duration of that request. When all slots are in use and a new request tries to connect, MySQL returns error 1040: Too many connections.
There are several reasons you can hit this limit.
Legitimate traffic exceeding capacity
If your site gets a traffic spike, more PHP-FPM workers spin up to handle the requests, and each one opens a MySQL connection. If you have 20 PHP-FPM workers configured and each request takes 500ms to complete, your site can handle roughly 40 requests per second with 20 simultaneous MySQL connections. Double the traffic and you need 40 connections. If
max_connections
is set to 30, you hit the wall.
This relationship between PHP-FPM workers and MySQL connections is the most important thing to understand about this error. Each PHP-FPM worker holds one MySQL connection for the entire duration of a request. More workers means more simultaneous connections. More traffic means more workers active at the same time.
Slow queries holding connections open
A MySQL connection is occupied for as long as the query is running. A query that takes 50ms releases its connection quickly. A query that takes 10 seconds holds that connection for 10 seconds. If multiple slow queries run simultaneously, connections stack up.
This is particularly common with unoptimized WooCommerce stores, sites with complex custom queries, or plugins that run heavy database operations on every page load. A single slow query might not cause problems, but ten of them running concurrently can consume a third of your connection pool.
Connection leaks
A connection leak happens when a PHP process opens a MySQL connection but does not close it properly. The connection stays open and occupied even though no query is running on it. Over time, leaked connections accumulate until they exhaust the pool.
Common causes include plugins that open direct MySQL connections (bypassing WordPress’s
$wpdb
object) and do not close them on error paths, long-running cron jobs that open connections and never release them, and PHP scripts that crash mid-execution without triggering the connection cleanup.
Idle connections from external tools
Database management tools like phpMyAdmin, MySQL Workbench, or DBeaver can hold connections open for long periods. If multiple team members leave these tools connected, each one consumes a slot. Remote MySQL connections from development tools are a common source of idle connections that are easy to forget about.
Cron jobs and background processes
WordPress cron jobs, WP-CLI commands, and background processing plugins (like Action Scheduler used by WooCommerce) all open MySQL connections. If multiple cron jobs fire simultaneously, or if a long-running background process overlaps with normal traffic, the combined connection count can exceed the limit.
Diagnosing the problem#
Check current connections vs limit
Connect to MySQL and run:
SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Threads_connected';
max_connections
shows the limit.
Threads_connected
shows how many connections are active right now. If
Threads_connected
is at or near
max_connections
, you are at the wall.
Check historical peak
SHOW STATUS LIKE 'Max_used_connections';
This shows the highest number of simultaneous connections since MySQL last started. If
Max_used_connections
is close to
max_connections
, you have been near the limit even if you are not there right now.
See what is connected right now
SHOW PROCESSLIST;
This shows every active connection: which user, which database, what query they are running, and how long they have been running it. Look for:
- Connections in
Sleepstate that have been idle for a long time (potential leaks or forgotten tools) - Connections running the same query for many seconds (slow queries)
- Connections from unexpected hosts (external tools or forgotten remote access)
- Multiple connections from the same user running simultaneously (cron overlap)
For a more detailed view:
SELECT user, host, db, command, time, state, info
FROM information_schema.PROCESSLIST
ORDER BY time DESC;
Connections with
Command: Sleep
and high
Time
values are idle. If you see dozens of sleeping connections, something is not releasing them.
Check PHP-FPM worker count
The number of PHP-FPM workers configured for your site sets an upper bound on how many MySQL connections that site can open simultaneously. Check your PHP-FPM pool configuration:
grep -E "pm\.(max_children|start_servers|min_spare|max_spare)" /etc/php-fpm.d/*.conf
If
pm.max_children
is 30 and
max_connections
is 50, you have 20 connection slots for everything else (admin tools, cron, replication, monitoring). That might be enough or it might not, depending on what else connects to MySQL.
Fixing the problem#
Kill idle connections immediately
If you are in an active outage and need to free connections right now:
-- Find sleeping connections older than 300 seconds
SELECT id, user, host, db, time
FROM information_schema.PROCESSLIST
WHERE command = 'Sleep' AND time > 300;
-- Kill a specific connection
KILL <connection_id>;
This is a temporary fix. If connections are leaking, they will fill up again.
Increase max_connections
If the current limit is genuinely too low for your workload:
SET GLOBAL max_connections = 300;
This takes effect immediately but does not survive a MySQL restart. To make it permanent, add to your MySQL configuration:
# /etc/my.cnf or /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
max_connections = 300
Then restart MySQL:
sudo systemctl restart mysqld
Be careful with this. Each connection consumes memory, roughly 10-20 MB depending on your configuration, sort buffers, and query complexity. Setting
max_connections
to 1000 on a server with 4 GB of RAM can cause MySQL to run out of memory and crash, which is worse than hitting the connection limit. A reasonable starting point for a small WordPress server is 100-300 depending on available RAM.
Reduce wait_timeout for idle connections
MySQL holds idle connections open until
wait_timeout
expires. The default is 28800 seconds (8 hours). On a web server where PHP connections should be short-lived, that is excessively long.
SET GLOBAL wait_timeout = 300;
SET GLOBAL interactive_timeout = 300;
This closes idle connections after 5 minutes instead of 8 hours. For WordPress hosting, 300 seconds is a reasonable value. Connections from PHP-FPM workers that finish their request and go idle will be cleaned up before they accumulate.
Make it permanent:
[mysqld]
wait_timeout = 300
interactive_timeout = 300
Fix slow queries
If
SHOW PROCESSLIST
reveals queries running for many seconds, those queries are the root cause. Identify them and optimize:
-- Enable the slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
Check the slow query log for the most frequent offenders. Common fixes include adding missing indexes, optimizing
wp_options
autoload queries, and deactivating plugins that run heavy queries on every page load. See WordPress database optimization for WordPress-specific optimization steps.
Fix connection leaks
If you see sleeping connections accumulating from the same user over time, something is leaking. Lower
wait_timeout
is the simplest mitigation, but the real fix is finding the leak:
- Check recently activated plugins. Deactivate them one at a time and monitor connection count
- Check custom code that opens direct
mysqlior PDO connections instead of using$wpdb - Check background processing plugins (Action Scheduler, WP Background Processing) for overlapping runs
Manage cron overlap
WordPress cron (
wp-cron.php
) runs on page load by default, which can cause multiple cron processes to fire simultaneously during traffic spikes. Switch to a system cron that fires at a controlled interval:
Add to
wp-config.php
:
define('DISABLE_WP_CRON', true);
Then add a system cron that fires once per minute:
* * * * * /usr/bin/php /path/to/wp-cron.php > /dev/null 2>&1
This ensures only one cron process runs at a time instead of multiple overlapping triggers. For a complete guide to cron syntax, see Cron job syntax: a practical guide with examples.
The "access denied for user" variant#
This error often gets grouped with “too many connections” in search results because they both prevent database access, but the cause is completely different.
“Access denied for user ‘wp_user’@’localhost'” means MySQL received the connection but rejected the credentials. The username, password, or host does not match what MySQL has on record.
Common causes:
Wrong password in wp-config.php. After a migration, password change, or manual database edit, the password in
wp-config.php
no longer matches what MySQL has for that user. Verify with:
-- Try connecting directly with the credentials from wp-config.php
mysql -u wp_user -p wordpress_db
If this fails, reset the password:
ALTER USER 'wp_user'@'localhost' IDENTIFIED BY 'new_password_here';
Then update
wp-config.php
to match.
Host mismatch. A user created as
'wp_user'@'localhost'
cannot connect from
127.0.0.1
or from a remote IP. The host component must match. If WordPress connects via TCP (some configurations use
127.0.0.1
instead of the Unix socket), you may need both:
CREATE USER 'wp_user'@'127.0.0.1' IDENTIFIED BY 'same_password';
GRANT ALL PRIVILEGES ON wordpress_db.* TO 'wp_user'@'127.0.0.1';
For a full explanation of how MySQL user hosts work, see How to show and manage MySQL users.
Authentication plugin mismatch. MySQL 8.0+ defaults to
caching_sha2_password
. Some older PHP versions and client libraries only support
mysql_native_password
. If your PHP application cannot authenticate despite correct credentials, check the user’s authentication plugin:
SELECT User, Host, plugin FROM mysql.user WHERE User = 'wp_user';
If it shows
caching_sha2_password
and your PHP version does not support it, switch the plugin:
ALTER USER 'wp_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password';
Privilege issues. The user exists and the password is correct, but they have no privileges on the database they are trying to access. Check with:
SHOW GRANTS FOR 'wp_user'@'localhost';
If only
USAGE
appears with no database-level grants, the user needs privileges granted on the specific database.
bind-address and remote access#
If you are getting “Can’t connect to MySQL server” from a remote machine, the issue is usually MySQL’s
bind-address
setting. By default, MySQL only listens on
127.0.0.1
, which means it only accepts connections from the same machine.
To allow connections from other servers, change
bind-address
in your MySQL configuration:
[mysqld]
bind-address = 0.0.0.0
This tells MySQL to listen on all network interfaces. After changing this, restart MySQL and make sure your firewall allows traffic on port 3306 only from the specific IPs that need access. Never leave port 3306 open to the internet.
Remote MySQL access requires three things to be correctly configured:
bind-address
, the MySQL user’s host permissions, and the firewall. See How to allow remote MySQL connections for the full setup walkthrough including SSL encryption and firewall configuration.
MySQL’s default port is 3306. You can change this in the configuration if needed, but most applications expect 3306 and changing it adds complexity without meaningful security benefit (port scanning finds non-standard ports quickly).
How Hostney handles this differently#
On traditional shared hosting, all sites on a server share the same MySQL instance and the same
max_connections
pool. One site with a traffic spike or a connection leak can exhaust the pool and take down database access for every other site on the server. This is one of the most common causes of “too many connections” on shared hosting.
Hostney’s per-container architecture eliminates this problem. Each hosting account runs in its own isolated container with its own PHP-FPM process pool. One site’s connection exhaustion cannot starve another site’s database access because the connection pools are isolated at the container level.
Database users and permissions are also scoped per account. One customer’s MySQL users cannot see or access another customer’s databases, regardless of MySQL privilege configuration. This is enforced at both the application and MySQL levels, which means even a misconfigured permission grant cannot cross account boundaries.
If you do encounter connection issues on Hostney, it is almost always specific to your site rather than a server-wide problem, which makes diagnosis faster because you only need to look at your own site’s traffic, plugins, and queries.
Quick diagnostic checklist#
- Is MySQL running?
sudo systemctl status mysqld. If it is down, the error is different (“Can’t connect to MySQL server”), not “too many connections” - What is the current connection count?
SHOW STATUS LIKE 'Threads_connected';vsSHOW VARIABLES LIKE 'max_connections'; - What is consuming the connections?
SHOW PROCESSLIST;– look for sleeping connections, slow queries, and unexpected hosts - Is
max_connectionstoo low? If your PHP-FPMpm.max_childrenis higher thanmax_connections, increase MySQL’s limit - Is
wait_timeouttoo high? Lowering it to 300 seconds cleans up idle connections faster - Are slow queries stacking up? Enable the slow query log and look for queries over 2 seconds
- Are cron jobs overlapping? Switch to system cron with
DISABLE_WP_CRON - Is it actually “access denied” instead? Different error, different fix – check credentials, host, and plugin
For the related error where MySQL drops an existing connection mid-query rather than refusing new ones, see MySQL server has gone away: what it means and how to fix it. For MySQL database management basics on Hostney, see the MySQL databases knowledge base.