Guide
WordPress "Error establishing a database connection" — detection and root cause
1. Problem
You open your WordPress site and the page is gone. Instead of your homepage, the browser shows a single white page with one line of text:
Error establishing a database connection
No header, no footer, no menu. Just that sentence. The admin URL /wp-admin returns the same thing. /wp-login.php returns the same thing. The REST API endpoint returns the same thing. Every URL on the site is dead.
Five minutes ago everything worked. Nothing was deployed. Nobody touched the server. The hosting dashboard says the site is "online". Uptime monitors are still happy because the server returns HTTP 200 with that error string in the body. This is the textbook WordPress can't connect to database scenario, and it's the moment people start Googling wordpress mysql connection error at 11pm hoping for a one-line fix.
There isn't one. There are five or six common root causes, all of which look identical from the browser. The only thing that separates them is what's in the logs.
2. Impact
A WordPress site that can't reach its database is not "degraded" — it is fully offline. Every request fails. Cached pages may keep serving for a few minutes if you have a page cache in front (Cloudflare, Varnish, WP Super Cache), but anything dynamic — checkout, login, comments, search, admin — is broken immediately.
For a content site, that's missed pageviews and ad revenue. For an ecommerce site running WooCommerce, every minute of downtime is abandoned carts, failed payments, and orders that may or may not have been written before MySQL went away. Customers who hit a broken checkout don't come back later — they buy from a competitor.
There's also a quieter cost: the longer the database is unreachable, the more php.fatal and db.error events stack up in error logs you're not reading. By the time someone notices, the logs have rotated and the original cause is gone. You end up restarting MySQL and "hoping it doesn't happen again," which is not a fix.
3. Why It’s Hard to Spot
WordPress hides this failure by design. dead_db() short-circuits the entire stack to avoid leaking schema or credentials in a stack trace, so the user-facing page contains no diagnostic information. There is no PHP exception bubbling up, no Apache 500, no useful HTTP status from the application's perspective.
Standard uptime monitors miss it for two reasons:
- The HTTP server (nginx/Apache) is still healthy and returns the request quickly. Only the page body changed.
- Most uptime checks only validate status codes (200/3xx is "up"). They don't grep the response body for "Error establishing a database connection."
Hosting control panels are even less helpful. cPanel, Plesk, and most managed-WordPress dashboards check that MySQL the process is running on the server, not that this site's user can authenticate. If MySQL is up but max_connections is exhausted, the dashboard shows green while every site on the box is down.
The result is a silent failure: visitors see a broken page, the monitoring stack sees green, and the only place the truth lives is in MySQL's own error log and PHP-FPM's error log — files most people only open after a customer complains.
4. Cause
When WordPress boots, wp-config.php is loaded and wp-settings.php calls require_wp_db(), which instantiates wpdb and immediately tries to connect to MySQL using the credentials in DB_HOST, DB_USER, DB_PASSWORD, and DB_NAME. If that connection fails, wpdb::db_connect() calls dead_db(), which renders the static error page and exits.
The Logystera WP plugin emits a db.error signal at this point. The payload includes the MySQL error code, the error message returned by the driver, and the host WordPress tried to reach. There are three common shapes:
- Connection refused / timeout — TCP can't reach MySQL at all. Either the database server is down, the hostname doesn't resolve, or a firewall is blocking the port. The error message looks like
Can't connect to MySQL server on 'db.internal' (110)or(2002) Connection refused. - Authentication failure — TCP connects, but MySQL rejects the credentials. Error code 1045:
Access denied for user 'wp_user'@'1.2.3.4' (using password: YES). This usually means the password changed, the user was dropped, or the connection is coming from a host that isn't in the user's grant list. - max_connections exceeded — Error code 1040:
Too many connections. MySQL is alive, your credentials work, but the server has hit itsmax_connectionslimit and is refusing new sessions until existing ones close.
Each of these surfaces in WordPress as the same generic error page, but they emit db.error with different mysql_error_code values. That's the only mechanical difference between "MySQL is dead" and "you have a config bug" from inside the application.
A db.error is almost always paired with http.request events showing 5xx responses (or HTTP 200 with the error body, depending on how dead_db() is rendered) and php.fatal entries containing the literal string Error establishing a database connection when WP_DEBUG is on.
5. Solution
5.1 Diagnose (logs first)
The diagnosis path is mechanical. You correlate three log sources and the answer falls out.
1. PHP error log — confirms WordPress saw the failure.
tail -n 200 /var/log/php-fpm/error.log | grep -i "Error establishing a database connection"
tail -n 200 /var/log/php-fpm/error.log | grep -iE "mysqli_real_connect|wpdb"
This is what produces the php.fatal signal and confirms WordPress reached dead_db(). If WP_DEBUG_LOG is on, wp-content/debug.log will also contain WordPress database error lines.
2. MySQL error log — produces the db.error signal with the real cause.
The location depends on distribution: /var/log/mysql/error.log on Debian/Ubuntu, /var/log/mysqld.log on RHEL/CentOS, or wherever log_error points in my.cnf.
grep -iE "aborted connection|access denied|too many connections|max_connections" /var/log/mysql/error.log | tail -n 50
What you see here decides everything:
[Warning] Access denied for user 'wp_user'@'10.0.1.5'→ authentication failure →db.errorwith code 1045.[ERROR] Too many connections→ connection pool exhausted →db.errorwith code 1040.[Warning] Aborted connection N to db: 'wordpress' user: 'wp_user' host: '...' (Got timeout reading communication packets)→ network orwait_timeoutissue →db.errorwith connection-lost code.- Nothing at all → MySQL never received the connection. The problem is between WordPress and MySQL: DNS, firewall, or MySQL is down.
3. Test the connection from PHP's perspective.
This is the single most useful command in this whole guide:
wp db check --path=/var/www/html
# or, if WP-CLI isn't installed:
mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" -e "SELECT 1;" "$DB_NAME"
If mysql -h ... fails with (2002) Connection refused, the DB host is unreachable — produces db.error with a connection-refused code. If it fails with Access denied, credentials are wrong — produces db.error with code 1045. If it succeeds, the problem is intermittent (likely max_connections or wait_timeout) and you need to watch the log live:
tail -f /var/log/mysql/error.log &
tail -f /var/log/php-fpm/error.log | grep -i "database"
4. Correlate with http.request 5xx.
In Logystera, filter http.request events on the entity for the affected window, and you'll see a wall of 500/503 responses that line up exactly with the db.error spike. That correlation is what turns "the site feels broken" into "MySQL refused 412 connections in a 90-second window starting 22:14:03 UTC."
5.2 Root Causes
(see root causes inline in 5.3 Fix)
5.3 Fix
Fix the cause that the logs actually show. Don't restart MySQL until you've checked.
Cause A: Wrong credentials in wp-config.php → produces db.error (code 1045) and php.fatal. Common after migrations, after a plugin "rewrites" wp-config, or after a hosting provider rotates passwords.
grep -E "DB_(USER|PASSWORD|HOST|NAME)" /var/www/html/wp-config.php
mysql -h DB_HOST -u DB_USER -pDB_PASSWORD -e "SELECT 1;"
If the second command fails with Access denied, fix wp-config.php or reset the user with ALTER USER 'wp_user'@'%' IDENTIFIED BY 'new_pass'; and FLUSH PRIVILEGES;.
Cause B: MySQL is down or unreachable → produces db.error (connection refused/timeout) with no MySQL log entry at all. Check the service:
systemctl status mysql # or mariadb
journalctl -u mysql --since "30 minutes ago" | tail -n 100
If MySQL is dead, the journal almost always tells you why — OOM kill, disk full, corrupt InnoDB log. Restart only after reading. If MySQL is on a separate host, also check telnet DB_HOST 3306 and the security group / iptables rules.
Cause C: max_connections exhausted → produces db.error (code 1040). Usually caused by a slow query holding connections open or a sudden traffic spike.
SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';
SHOW PROCESSLIST;
Kill long-running queries with KILL , raise max_connections if the server has the RAM (each connection costs ~3–10MB), and find the slow query in the slow-query log. A common WordPress pattern: a misbehaving plugin runs WP_Query with 'orderby' => 'rand' on a huge wp_posts table and locks connections for tens of seconds.
Cause D: Disk full / InnoDB can't write → produces db.error plus [ERROR] InnoDB: ...write... in the MySQL log.
df -h
grep -i "innodb" /var/log/mysql/error.log | tail -n 30
Free space, then restart MySQL. Common on small VPS instances where wp_options autoloaded data, binary logs, or wp_actionscheduler_logs quietly fills the disk.
Cause E: DNS / hostname change → DB_HOST resolves to nothing or to the wrong IP. Produces db.error with Unknown MySQL server host or connection-refused. dig $DB_HOST and fix /etc/hosts or DNS.
Cause F: Corrupt database tables → MySQL is reachable, credentials work, but wp_options or wp_users is corrupt. Produces db.error only on specific queries. wp db repair or mysqlcheck --auto-repair --all-databases.
5.4 Verify
You're looking for two things: db.error events stop appearing, and the http.request 5xx rate drops back to the baseline.
# Should be empty for at least 15 minutes under normal traffic:
grep "Error establishing a database connection" /var/log/php-fpm/error.log | tail -n 5
grep -iE "access denied|too many connections|aborted connection" /var/log/mysql/error.log | tail -n 5
# Should return 1 instantly:
wp db check --path=/var/www/html && echo "OK"
In Logystera's entity view, healthy state looks like: zero db.error events for 30 minutes, http.request 5xx rate under 0.5% of total requests, and no new php.fatal entries with the database string in the message. Watch a full traffic peak (lunch, evening — whatever your peak is) before declaring it fixed. Many DB issues only manifest under load — a server that's fine at 50 req/s will hit max_connections at 200 req/s.
If db.error reappears within an hour, you addressed a symptom, not the cause. Go back to the MySQL error log.
6. How to Catch This Early
Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all.
This issue surfaces as db.error.
The hard part of this failure isn't fixing it — most causes have a one-line fix once you know which one you have. The hard part is knowing the database started failing before customers told you.
WordPress will not email you when dead_db() fires. PHP-FPM does not have an alerting layer. Uptime checks see HTTP 200. The MySQL error log is a file on a server you only look at after something has already gone wrong.
The Logystera WP plugin emits a db.error signal the instant wpdb fails to connect, with the MySQL error code attached. The processor matches that signal against rules — db.error rate above zero is itself an alert condition for production sites — and you get a notification while the page is still broken for the first visitor, not the hundredth. Combined with http.request 5xx correlation and php.fatal deduplication, the same database outage that used to surface as "a customer emailed support" surfaces as a single alert with the MySQL error code in the title.
This is what "log-first" means in practice: the failure already exists in logs the moment it happens. The only question is whether anyone is reading them.
7. Related Silent Failures
- WordPress 500 error after plugin update —
php.fatalfrom a fatal PHP error, not a DB error. Same blank-page symptom, completely different root cause. - WordPress white screen of death (WSOD) —
php.fatalwithAllowed memory size exhausted, often correlated withmemory_near_limitwarnings beforehand. - WordPress slow admin / slow queries —
db.slow_querysignals onwp_optionsautoload bloat or unindexedwp_postmetalookups; precursor tomax_connectionsexhaustion. - WooCommerce checkout intermittently fails —
db.errorwithDeadlock foundorLock wait timeout exceededonwp_woocommerce_order_items, not connection errors. - WordPress site goes down at the same time every day — almost always
wp.cronrunning an expensive task that holds DB connections, eventually triggeringdb.error(code 1040, too many connections).
See what's actually happening in your WordPress system
Connect your site. Logystera starts monitoring within minutes.