How 280 Sleeping Database Connections Took Down a WooCommerce Store Mid-Sale
· 9 min read
The Outage
A client's WooCommerce store went down at 11am on the first day of their spring sale. Customers were seeing "Error establishing a database connection" — the WordPress equivalent of a closed sign on the shop door. The store processes around 200 orders a day normally, and the sale had been promoted to their full email list the night before.
I SSHed in and the first thing I checked was whether MariaDB was actually running:
systemctl status mariadb
It was running. So the database server hadn't crashed — it was refusing new connections. That pointed to one thing: connection exhaustion.
Diagnosing the Problem
Step 1: Check Current Connection Count
mysql -u root -e "SHOW STATUS LIKE 'Threads_connected';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 152 |
+-------------------+-------+
Then I checked the limit:
mysql -u root -e "SHOW VARIABLES LIKE 'max_connections';"
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 151 |
+-----------------+-------+
There it was. The default MariaDB max_connections value is 151 — 150 regular connections plus 1 reserved for a SUPER-privileged user. The server was completely maxed out. Every new PHP request trying to connect to the database was being rejected.
Step 2: What Are Those Connections Doing?
mysql -u root -e "SELECT command, COUNT(*) as count FROM information_schema.processlist GROUP BY command ORDER BY count DESC;"
+---------+-------+
| command | count |
+---------+-------+
| Sleep | 142 |
| Query | 7 |
| Connect | 3 |
+---------+-------+
142 out of 152 connections were in the Sleep state — doing absolutely nothing. They were stale connections that PHP-FPM workers had opened, used for a page request, and then left open.
Step 3: How Long Have They Been Sleeping?
mysql -u root -e "SELECT user, host, time, state, LEFT(info, 50) as query FROM information_schema.processlist WHERE command = 'Sleep' ORDER BY time DESC LIMIT 10;"
The oldest sleeping connections had been idle for over 7 hours. That told me the wait_timeout was set to the default:
mysql -u root -e "SHOW VARIABLES LIKE 'wait_timeout';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout | 28800 |
+---------------+-------+
28,800 seconds. That's 8 hours. Every database connection opened by a PHP process was allowed to sit idle for 8 hours before MariaDB would clean it up. On a server where PHP-FPM workers are constantly recycling and creating new connections, this is a recipe for connection exhaustion.
The Root Cause Chain
Here's what happened in sequence:
- Default
wait_timeoutof 8 hours meant old connections were never cleaned up - PHP-FPM workers create a new MariaDB connection on each request. When the worker finishes, the PHP process may keep the connection open (especially with persistent connections or if the worker is reused)
- Traffic spike from the sale pushed PHP-FPM to its
pm.max_childrenlimit (35 workers on this server) - Each worker accumulated stale connections over the course of the morning
- No object cache meant every single page load was hitting the database for transient data, session data, and option lookups — multiplying the connection load
- At 11am, the 151-connection ceiling was hit and new requests started failing
The server had 4GB of RAM. With 35 PHP-FPM workers and 151 potential MariaDB connections (each consuming memory for thread buffers), it was also under significant memory pressure.
The Immediate Fix
First, I needed the site back up. I killed the sleeping connections and raised the limit temporarily:
mysql -u root -e "SET GLOBAL max_connections = 250;"
Then I cleared out the sleeping connections:
mysql -u root -e "SELECT GROUP_CONCAT('KILL ', id SEPARATOR '; ') FROM information_schema.processlist WHERE command = 'Sleep' AND time > 300;" | mysql -u root
That killed every connection that had been sleeping for more than 5 minutes. The site came back immediately.
The Permanent Fix
1. Tune wait_timeout
For a WordPress/WooCommerce server, there's no reason for a database connection to sit idle for 8 hours. PHP requests typically complete in under 5 seconds. I set wait_timeout to 120 seconds — generous enough to handle slow WooCommerce operations like bulk order exports, but short enough to clean up stale connections quickly.
In /etc/mysql/mariadb.conf.d/50-server.cnf:
[mysqld]
wait_timeout = 120
interactive_timeout = 180
2. Right-Size max_connections
The formula I use:
max_connections = (PHP-FPM pm.max_children * 2) + 10
The multiplier of 2 accounts for cron jobs, WP-CLI commands, and WooCommerce background processes (Action Scheduler) that also need database connections. The +10 is headroom for admin sessions, monitoring, and the SUPER user reserved connection.
For this server with 35 PHP-FPM workers:
[mysqld]
max_connections = 80
That might seem low compared to the default 151, but it's realistic. If you're genuinely consuming 80 simultaneous database connections on a single WordPress server, the problem isn't your connection limit — it's your query performance or your traffic needs more infrastructure.
3. Tune Thread and Buffer Settings
Each MariaDB connection allocates memory for thread-specific buffers. With the default settings, each connection could consume 2-4MB. On a 4GB server, 151 connections could theoretically require 600MB+ just for connection buffers, on top of the InnoDB buffer pool.
[mysqld]
# Connection management
max_connections = 80
wait_timeout = 120
interactive_timeout = 180
# Thread handling
thread_cache_size = 16
thread_handling = pool-of-threads
thread_pool_size = 4
# Per-connection buffers (keep conservative on small servers)
sort_buffer_size = 2M
read_buffer_size = 1M
join_buffer_size = 1M
tmp_table_size = 32M
max_heap_table_size = 32M
4. Add Redis Object Cache
This was the single biggest impact change. Without an object cache, WordPress hits the database for every get_option() call, every transient lookup, and every session check — on every single page load. On a WooCommerce store with 50+ plugins, that's easily 200-400 database queries per page.
apt install redis-server
Then I installed the Redis Object Cache plugin and added to wp-config.php:
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_DATABASE', 0);
After enabling Redis, the database query count per page load dropped from ~350 to ~40. That directly translates to fewer connections being held open and each connection finishing faster.
5. Check for Persistent Connections
WordPress supports persistent database connections via a constant in wp-config.php. I checked whether it was enabled:
grep -r "WP_MYSQL_USE_PERSISTENT" /var/www/site/wp-config.php
It wasn't on this server, but I've seen it enabled on others. Persistent connections tell PHP not to close the MariaDB connection when the script finishes, reusing it for the next request handled by the same PHP-FPM worker. In theory this reduces connection overhead. In practice, on a WordPress server with a default 8-hour wait_timeout, it's one of the fastest paths to connection exhaustion. If you find it enabled and you're hitting connection limits, remove it.
Monitoring Going Forward
I set up a simple cron job to log connection stats every 5 minutes, so we'd see problems building before they caused an outage:
# /etc/cron.d/mysql-connection-monitor
*/5 * * * * root mysql -u root -e "SELECT NOW() as timestamp, (SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME = 'Threads_connected') as connected, (SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME = 'Max_used_connections') as max_used;" >> /var/log/mysql-connections.log 2>&1
You can also check the high-water mark at any time:
mysql -u root -e "SHOW STATUS LIKE 'Max_used_connections';"
This tells you the peak number of simultaneous connections since MariaDB was last restarted. If it's consistently close to your max_connections value, you need to investigate before you hit the ceiling again.
After the Fix
The sale ran for another 6 days with no further outages. Peak Max_used_connections was 38 — well within the new limit of 80, and well below the old default of 151 that had been exhausted by sleeping connections.
The real lesson: MariaDB's defaults are designed for general-purpose use, not for WordPress. A wait_timeout of 8 hours makes sense for an application with long-lived database sessions. For PHP applications that connect, query, and disconnect in milliseconds, it just lets dead connections pile up until you hit the wall.
If you're running WordPress on a VPS and have never touched your MariaDB configuration, check your sleeping connections today. You might be one traffic spike away from the same outage.
Stop Firefighting. Start Maintaining.
I manage 70+ WordPress sites for UK agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.
