Error Establishing a Database Connection: Two MariaDB Failures, Diagnosed and Fixed
· 13 min read
"Error establishing a database connection" is the WordPress equivalent of a closed sign on the shop door. It's also one of the least helpful error messages in the ecosystem, because it covers everything from a crashed database server to a typo in wp-config.php.
This post covers two real incidents from servers I manage — same error message, completely different causes. The first was a WooCommerce store that hit MariaDB's connection ceiling in the middle of a flash sale. The second was a MariaDB version upgrade that took down all 47 WordPress sites on a WHM/cPanel server at once. Between them, they cover most of the ways MariaDB can produce this error while the database itself is running fine.
Incident One: 280 Connections and a Flash Sale
A client's WooCommerce store went down at 11am on the first day of their spring sale. 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 checked whether MariaDB was actually running:
systemctl status mariadb
It was. So the database server hadn't crashed — it was refusing new connections. That pointed to connection exhaustion.
Diagnosing the Exhaustion
First, the current connection count against the limit:
mysql -u root -e "SHOW STATUS LIKE 'Threads_connected';"
mysql -u root -e "SHOW VARIABLES LIKE 'max_connections';"
| Threads_connected | 152 |
| max_connections | 151 |
There it was. The default MariaDB max_connections is 151 — 150 regular connections plus 1 reserved for a SUPER-privileged user. The server was maxed out, and every new PHP request was being rejected.
Next question: what were 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 of 152 connections were in the Sleep state — doing absolutely nothing. Stale connections that PHP-FPM workers had opened, used for a page request, and left open. The oldest had been idle for over 7 hours, which told me wait_timeout was still at the default:
mysql -u root -e "SHOW VARIABLES LIKE 'wait_timeout';"
| wait_timeout | 28800 |
28,800 seconds — 8 hours. Every connection was allowed to sit idle for 8 hours before MariaDB cleaned it up.
The Root Cause Chain
- Default
wait_timeoutof 8 hours meant old connections were never cleaned up - PHP-FPM workers open a new MariaDB connection per request and may keep it open when reused
- The traffic spike 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 page load hit the database for transients, sessions, and option lookups — multiplying the connection load
- At 11am the 151-connection ceiling was hit and requests started failing
The server had 4GB of RAM, so with 35 workers and 151 potential connections each holding thread buffers, it was under memory pressure too.
The Immediate Fix
Get the site back up first. I raised the limit temporarily:
mysql -u root -e "SET GLOBAL max_connections = 250;"
Then cleared out everything that had been sleeping for more than 5 minutes:
mysql -u root -e "SELECT GROUP_CONCAT('KILL ', id SEPARATOR '; ') FROM information_schema.processlist WHERE command = 'Sleep' AND time > 300;" | mysql -u root
The site came back immediately.
The Permanent Fix
Tune the timeouts. PHP requests typically complete in under 5 seconds; there's no reason for a connection to idle for 8 hours. I set wait_timeout to 120 seconds — enough for slow WooCommerce operations like bulk order exports, short enough to clean up stale connections quickly.
Right-size max_connections. The formula I use:
max_connections = (PHP-FPM pm.max_children * 2) + 10
The multiplier of 2 covers cron jobs, WP-CLI, and WooCommerce's Action Scheduler background processes; the +10 is headroom for admin sessions, monitoring, and the reserved SUPER connection. For this server with 35 workers, that's 80. Lower than the default 151, but realistic — if you're genuinely consuming 80 simultaneous connections on a single WordPress server, the problem is query performance or capacity, not the limit.
The full config in /etc/mysql/mariadb.conf.d/50-server.cnf:
[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
The buffer settings matter on small servers: each connection can consume 2-4MB with defaults, so 151 connections could theoretically need 600MB+ just for connection buffers, on top of the InnoDB buffer pool.
Add Redis object caching. This was the single biggest impact change. Without an object cache, WordPress hits the database for every get_option() call, transient lookup, and session check on every page load — on a WooCommerce store with 50+ plugins, easily 200-400 queries per page. I installed redis-server, 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);
Query count per page load dropped from ~350 to ~40. Fewer connections held open, each one finishing faster.
Check for persistent connections. WordPress supports persistent database connections via WP_MYSQL_USE_PERSISTENT in wp-config.php. It wasn't enabled here, but I've seen it on other servers. Combined with an 8-hour wait_timeout, it's one of the fastest paths to connection exhaustion. If you find it enabled while hitting connection limits, remove it.
Monitoring Going Forward
A simple cron job logs connection stats every 5 minutes, so problems show up before they cause 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
SHOW STATUS LIKE 'Max_used_connections'; gives the peak since the last restart at any time. If it's consistently close to your max_connections, investigate before you hit the ceiling.
The sale ran for another 6 days with no further outages. Peak Max_used_connections was 38 — well within the new limit of 80.
Incident Two: A MariaDB Upgrade Took Down 47 Sites
A different client messaged me on a Friday afternoon: "All our sites are down." Not one — all 47 WordPress sites on a WHM/cPanel server, every one returning the same "Error establishing a database connection" page.
The hosting company had pushed a MariaDB upgrade from 10.5 to 10.11 during a maintenance window. MariaDB was running. Command-line connections worked. But every WordPress site was dead. It turned out to be four separate problems stacked on top of each other.
Problem 1: Socket Path Mismatch
When DB_HOST is localhost, PHP doesn't connect over TCP/IP — it uses a Unix socket file. The location of that file is the first thing that breaks in a major version upgrade.
mysql -u root -e "SHOW VARIABLES LIKE 'socket';"
# socket => /var/lib/mysql/mysql.sock
php -i | grep mysql.default_socket
# mysqli.default_socket => /var/run/mysqld/mysqld.sock
MariaDB 10.11 put the socket at /var/lib/mysql/mysql.sock, but PHP's compiled default still pointed to /var/run/mysqld/mysqld.sock. Before the upgrade the paths had been symlinked; after it, the symlink was gone. The quick fix:
mkdir -p /var/run/mysqld
ln -s /var/lib/mysql/mysql.sock /var/run/mysqld/mysqld.sock
chown mysql:mysql /var/run/mysqld
That's a sticking plaster. The proper fix is aligning the configurations — socket path declared in /etc/my.cnf.d/server.cnf under both [mysqld] and [client], and in php.ini:
mysqli.default_socket = /var/lib/mysql/mysql.sock
pdo_mysql.default_socket = /var/lib/mysql/mysql.sock
After restarting PHP-FPM, about 30 of the 47 sites came back. The remaining 17 had a different problem.
Problem 2: Authentication Plugin Change
From MariaDB 10.4, the default authentication for the root user changed from mysql_native_password to unix_socket — root authenticates based on the operating system user, not a password. Fine for command-line access as root; a problem for PHP running as nobody or www-data.
A few legacy sites on this server were still connecting as the root MySQL user (don't do this, but it happens), and their password-based authentication stopped working entirely:
SELECT user, host, plugin FROM mysql.user WHERE user = 'root';
-- root | localhost | unix_socket
For those sites I temporarily re-enabled password authentication:
ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING PASSWORD('secure_password_here');
FLUSH PRIVILEGES;
But the real fix was creating dedicated database users per site, which they should have had all along:
mysql -u root <<EOF
CREATE USER 'wp_sitename'@'localhost' IDENTIFIED BY 'strong_random_password';
GRANT ALL PRIVILEGES ON wp_sitename_db.* TO 'wp_sitename'@'localhost';
FLUSH PRIVILEGES;
EOF
Then updating each site's wp-config.php. For 17 sites, I scripted it with a loop over the cPanel account list.
Problem 3: Legacy Collation Warnings
Once the sites were back, WordPress Site Health on several started warning about database collation. MariaDB 10.6 renamed utf8 to utf8mb3, and by 10.11 the old utf8_general_ci references were generating deprecation warnings in the logs. I converted the affected tables to utf8mb4 with WP-CLI:
wp db query "SHOW TABLES;" --path=/home/account/public_html | tail -n +2 | while read table; do
wp db query "ALTER TABLE $table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" --path=/home/account/public_html
done
wp core update-db --path=/home/account/public_html
Problem 4: SELinux
Three sites on a separate AlmaLinux 9 server with CloudPanel had the same upgrade applied, but the socket fix didn't help — localhost was refused while 127.0.0.1 worked. After the upgrade, the socket file had the wrong SELinux context: PHP-FPM under httpd_t couldn't connect to a socket owned by unconfined_service_t. The audit log confirmed it:
ausearch -m AVC -ts recent | grep mysql
The fix:
semanage fcontext -a -t mysqld_var_run_t "/var/lib/mysql/mysql.sock"
restorecon -v /var/lib/mysql/mysql.sock
systemctl daemon-reexec
systemctl restart mariadb
The Pre-Upgrade Checklist I Now Use
After the 47-site incident, I built a checklist I run before every MariaDB upgrade on any server I manage:
- Record the current socket path:
mysql -u root -e "SHOW VARIABLES LIKE 'socket';" - Record PHP's expected socket:
php -i | grep mysql.default_socket - Dump all user authentication methods:
SELECT user, host, plugin FROM mysql.user; - Check for legacy collations:
SELECT table_name, table_collation FROM information_schema.tables WHERE table_schema NOT IN ('mysql','information_schema','performance_schema') AND table_collation LIKE 'utf8_%'; - Full database dump:
mysqldump --all-databases --routines --triggers > /root/pre-upgrade-$(date +%F).sql - Note the current MariaDB version:
mysql -V - Check SELinux status:
getenforce - Put all WordPress sites in maintenance mode:
for dir in /home/*/public_html; do wp maintenance-mode activate --path="$dir" 2>/dev/null; done
After the upgrade, reverse through the list — verify socket paths match, test a connection from PHP, check authentication for each database user, and bring sites out of maintenance mode one by one.
One version note: if you're still on MariaDB 10.5 or older, you're on borrowed time — 10.5 reaches end of life in July 2026. The recommended minimum for WordPress hosting is 10.6, and the latest LTS is 11.8. The 10.x to 11.x jump is bigger than a minor bump: plan it, test it in staging, and don't let your host surprise you with it on a Friday afternoon.
What Both Incidents Have in Common
Neither server had a broken database. In the first, MariaDB's general-purpose defaults — an 8-hour wait_timeout designed for long-lived sessions — let dead connections pile up under a PHP workload that connects and disconnects in milliseconds. In the second, an upgrade shifted socket paths, authentication defaults, character sets, and SELinux contexts underneath 47 working sites.
The fix for each individual issue is straightforward once you know what to look for. The hard part is that WordPress reports them all identically. When you see "Error establishing a database connection", work through it in order: is MariaDB running, is the connection count at the ceiling, does the socket path PHP expects match where MariaDB is listening, and does the database user still authenticate the way wp-config.php assumes.
This is exactly the kind of invisible work covered by my WordPress server management plans — MariaDB tuning, upgrade planning, and connection monitoring are standard, and database performance optimisation is where the Redis and query-load work lives. 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 — or one automatic upgrade — 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.
