Database error tells you almost nothing on its own, and the useful move is to work out which of six categories you are in before changing anything. The single highest-value action is finding the actual driver error rather than the message your application decided to show the user, because those two are rarely the same thing.
Applications routinely catch a specific database exception and replace it with a friendly sentence, discarding the part that would have told you the answer. So the first section is about recovering that, and the rest is the order to check things in.
Table of contents
- Get the real error first
- Connection refused, timed out, and the difference
- Access denied, which is usually not the password
- Too many connections, the one that arrives under load
- Schema, locks, and disk
- Reducing the odds
- How this fits the rest of the stack
- FAQ
Get the real error first
Before anything else, find the driver-level message. It is in the application log rather than the page, and it names the problem directly.
# Application logs, wherever they land.
journalctl -u myapp --since "10 min ago" -p err
# Database server logs.
sudo tail -100 /var/log/mysql/error.log
sudo tail -100 /var/log/postgresql/postgresql-16-main.log
# Prove the connection independently of the application.
mysql -h db.example.com -u appuser -p -e "SELECT 1"
psql -h db.example.com -U appuser -d appdb -c "SELECT 1"
That last pair is the fastest way to split the problem in half. If the command-line client connects and the application does not, the database is fine and the problem is in the application’s configuration or its connection handling. If neither connects, it is network, credentials, or the server itself.
The categories the real error will put you in:
- Connection refused or timed out: the server is unreachable, or not running.
- Access denied or authentication failed: credentials or host-based rules.
- Too many connections: the pool or the server limit is exhausted.
- Unknown database or relation does not exist: a schema or migration problem.
- Deadlock or lock wait timeout: concurrency.
- Disk full or read-only: the server is out of space.
Each has a different fix, and the reason database problems take so long is usually that someone started applying fixes before establishing which category they were in.
Connection refused, timed out, and the difference
These two words point in different directions and it is worth being precise.
Connection refused means something answered and declined. The host is reachable and nothing is listening on that port, so the database is not running, or it is bound to localhost only while you are connecting from elsewhere.
Connection timed out means nothing answered at all. That is a firewall or security group silently dropping packets, or the wrong host entirely.
# Is the port open at all?
nc -zv db.example.com 3306
# Is the process running and what is it bound to?
sudo systemctl status mysql
sudo ss -tlnp | grep -E '3306|5432'
# 127.0.0.1:3306 means local only. 0.0.0.0:3306 accepts remote.
The bind address is the classic. MySQL and Postgres both default to local-only in many packages, and the fix is bind-address in the MySQL configuration or listen_addresses in postgresql.conf, followed by a restart.
Postgres additionally requires a matching rule in pg_hba.conf permitting the user, database, and source address. Its absence produces a clear message about no pg_hba.conf entry, which is one of the more helpful errors in this space.
Access denied, which is usually not the password
The message says authentication failed, and the cause is frequently that the user is not permitted from the host you are connecting from.
In MySQL, a user is the pair of name and host. An account created as appuser@localhost simply does not exist when the connection arrives from an application server, and the error is identical to a wrong password.
-- What accounts actually exist?
SELECT user, host FROM mysql.user WHERE user = 'appuser';
-- Permit the application server's range.
CREATE USER 'appuser'@'10.0.%' IDENTIFIED BY 'password';
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'appuser'@'10.0.%';
FLUSH PRIVILEGES;
The other frequent cause is a configuration mismatch rather than a wrong value: an environment variable that is not set in production, so the application falls back to a default; a password containing a character that needs escaping in a connection URL; or trailing whitespace from a copy and paste.
Check what the application is actually using rather than what you believe it is using. Printing the host, user, and database name at startup, with the password redacted, resolves this class of problem faster than any amount of reasoning.
Too many connections, the one that arrives under load
This appears when traffic increases and disappears when it drops, which makes it look intermittent and unrelated to code.
The cause is nearly always connection handling in the application rather than a database that is too small. Each connection consumes memory on the server, so the limit exists for good reason, and raising it is usually the wrong first move.
-- MySQL: what is the limit and what is in use?
SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Threads_connected';
SHOW PROCESSLIST;
-- Postgres: same questions.
SHOW max_connections;
SELECT count(*), state FROM pg_stat_activity GROUP BY state;
-- Connections idle in a transaction are the ones holding locks.
SELECT pid, state, query_start, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY query_start;
Three underlying causes, in order of frequency. Connections are opened and never returned to the pool, usually a missing close in an error path. The pool is sized larger than the server limit, which guarantees exhaustion under load. Or transactions stay open across slow external calls, so each request holds a connection for far longer than it needs it.
That third one is worth checking specifically for idle in transaction, because those connections hold locks as well as slots and can block other work while appearing to do nothing.
The arithmetic that prevents this: total pool size across every application instance, plus a margin for administrative connections, must be below the server’s limit. Four instances with a pool of 50 each need 200 connections plus headroom, and a default limit of 151 in MySQL will not survive it.
Schema, locks, and disk
Unknown database or relation does not exist during normal operation almost always means migrations did not run, or ran against a different database than the application is using. Check the migration state table and the connection string in the same breath.
Deadlock and lock wait timeout mean two transactions are contending. The durable fix is ordering: acquire locks on rows in a consistent order everywhere, and keep transactions short. Retrying on deadlock is legitimate and expected, since deadlocks are a normal outcome under concurrency rather than a bug.
-- MySQL: what happened in the most recent deadlock?
SHOW ENGINE INNODB STATUS;
-- Postgres: what is blocking what, right now?
SELECT blocked.pid AS blocked_pid, blocking.pid AS blocking_pid,
blocked.query AS blocked_query, blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid));
Disk full produces alarming and varied errors because a database that cannot write cannot do much of anything. Check df -h before believing any other theory when errors are widespread and sudden. On Postgres, watch for WAL accumulating due to an inactive replication slot, which fills a disk quietly and is not obvious from table sizes.
Reducing the odds
- Log the driver error, not a friendly message. Keep the original message and code in the log even when the user sees something polite.
- Size the pool against the server limit, counting every instance, and leave headroom for administrative access.
- Set connection and statement timeouts, so a hung query releases its slot instead of holding it forever.
- Retry on transient failures with backoff, and only on genuinely transient categories such as deadlock, never on authentication failure.
- Monitor connection count as a percentage of the limit, and alert well before it is reached.
- Test a restore. A backup nobody has restored is not a backup, and disk-full incidents are where that gets discovered.
The first item is the one that pays back fastest. Most of the time spent on database incidents goes into recovering information that was available at the moment of failure and thrown away.
How this fits the rest of the stack
A managed database removes a category of these problems rather than all of them: patching, backups, connection limits, and network security are configured for you, while the schema, the queries, and the pool sizing in your application remain yours. Managed MySQL and Postgres on RunxBuild come with connection limits, backups, user management, and private networking, and the service that queries them keeps its runtime logs alongside its deploy logs. The RunxBuild hosting calculator shows the database and the service as separate line items.
Useful related references:
- Error Establishing a Database Connection: The WordPress Fault Tree
- Redis vs DynamoDB: Cache, Database, or Both
- MySQL to MySQL: Migrating a Database Between Servers
- Databases on RunxBuild
FAQ
What does connection refused mean versus connection timed out?
Refused means the host is reachable and nothing is listening on that port, so the database is down or bound to localhost only. Timed out means nothing answered, which points to a firewall dropping packets or the wrong host.
Why do I get access denied when the password is correct?
In MySQL a user is the combination of name and host, so an account created for localhost does not exist for connections from an application server. The error is identical to a wrong password. Check the user and host pairs that actually exist.
How do I fix too many connections?
Usually by fixing the application rather than raising the limit. Confirm connections are returned to the pool on error paths, size the total pool across all instances below the server limit, and keep transactions from spanning slow external calls.
Should I retry after a database deadlock?
Yes. Deadlocks are a normal outcome under concurrency, and the losing transaction is rolled back safely, so retrying with backoff is correct. Do not retry authentication failures or schema errors, which will never succeed.
Why did my database suddenly start throwing unrelated errors?
Check disk space first. A database that cannot write produces varied and confusing errors. On Postgres, an inactive replication slot causing WAL to accumulate is a common cause that is not visible from table sizes.