Fix Matomo database errors: access denied, connection refused, and missing privileges

Matomo's database errors look cryptic, but the SQLSTATE code in the message tells you exactly which half of the problem you're in. Here's how to read [1045], [2002], and the missing-privilege warnings, and the specific fix for each in Matomo 5.x.

If Matomo just stopped talking to its database, you're looking at one of a small set of SQLSTATE errors. They show up on the installer's database step, on a normal page load, or in the system check on a site that was fine yesterday. They read like noise. They aren't.

text
SQLSTATE[28000] [1045] Access denied for user 'matomo'@'localhost' (using password: YES)
SQLSTATE[HY000] [2002] No connection could be made / Connection refused
System Check: CREATE TEMPORARY TABLES / LOAD DATA INFILE not available

Before you touch anything, read the bracketed number. [1045] means the database server answered and rejected you: an authorization problem. [2002] means the server never answered at all: a connectivity problem. The privilege warnings split in two: CREATE TEMPORARY TABLES is a required grant, while LOAD DATA INFILE is optional performance plumbing. Get the error into the right bucket and you've done most of the work.

ErrorMeaningFirst checkLikely fix
[1045]MySQL rejected the user, host, password, or grantmysql -h ... -u ... -p dbname from the Matomo runtimeFix password, user host, or grants
[2002]No MySQL connection or handshake happenedmysqladmin status, then host, port, socket, firewall, diskStart MySQL or fix reachability
CREATE TEMPORARY TABLESRequired database-level grant is missingSHOW GRANTSAdd the required grant on the Matomo database
LOAD DATA INFILEOptional archiving performance check failedSystem Check detailsEnable it safely or disable the feature

[1045] Access denied: the server answered and said no

This error is MySQL's, not Matomo's. The connection succeeded, MySQL looked up the user, host, and password you sent, and turned them down. So the fix lives on the database side every time. Here's the order I'd work through it, roughly sorted by how often each one turns out to be the culprit.

Start with stray whitespace. A trailing space in the username or password, usually pasted in by browser autofill on the installer, is the single most common cause. Re-type the credentials by hand, and check that nothing is hiding inside the quotes in config/config.ini.php:

ini
[database]
host     = "127.0.0.1"
username = "matomo"
password = "secret"
dbname   = "matomo_db"

That's "secret", not " secret". MySQL treats those as two different passwords.

Next, confirm the login on its own, with Matomo mostly out of the picture. Run the exact credentials from the same runtime that executes Matomo's PHP, not from your laptop and not necessarily from the database host. If this fails there, Matomo was never going to succeed:

bash
mysql -h 127.0.0.1 -u matomo -p matomo_db
Run this from Matomo's runtime

On a VM, SSH into the app server. In Docker Compose, use docker compose exec matomo sh or a temporary MySQL client on the same Compose network. In Kubernetes, kubectl exec into the Matomo pod or run a temporary client pod in the same namespace. The point is to test the path MySQL sees from Matomo.

Once you are connected, ask MySQL which account it actually matched:

sql
SELECT CURRENT_USER(), USER();
SHOW GRANTS;

Then check the user's host, which is the part that catches containerised and cloud setups. In MySQL, 'matomo'@'localhost', 'matomo'@'10.0.2.%', and 'matomo'@'%' are separate accounts with their own passwords and grants. On a single host the first one is fine. Split Matomo and the database across two servers, Docker containers, Kubernetes pods, or an RDS instance, and MySQL sees the connection coming from the app server's address rather than localhost, then matches it against an account that might not exist.

MySQL user host matching

Prefer a specific host, a private subnet such as 'matomo'@'10.0.2.%', or the managed database's recommended private-network rule. Use 'matomo'@'%' only when firewall rules, security groups, or network policy already restrict who can reach MySQL.

Those grants are the next thing to get right. This is the set Matomo's install docs ask for:

sql
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, DROP, ALTER,
      CREATE TEMPORARY TABLES, LOCK TABLES
ON matomo_db.* TO 'matomo'@'localhost';
FLUSH PRIVILEGES;

Finally, if a working install throws [1045] out of nowhere, Matomo didn't do it. It never edits the database password itself. Something moved on the database side: a password reset, a user or host change, a FLUSH PRIVILEGES that picked up a stricter grant table, a server upgrade. Diff config/config.ini.php against the live credentials and they'll disagree somewhere.

[2002] Connection refused: the server never answered

[2002] is a different animal. Matomo's MySQL client opened a connection and got nothing back: no rejection, no handshake. Credentials don't matter here, because the conversation never started. You're debugging whether the server is reachable at all.

Start with the obvious one, since it's free to check and it's often the answer: is the database even running?

Run fromDatabase host or database container
mysqladmin -u root -p status
Expect: An Uptime line with thread and query counts
If it fails: Start MySQL or MariaDB, then read the database server's own error log before changing Matomo credentials.

If mysqladmin itself can't reach the server, check the service directly:

bash
systemctl status mariadb   # or: systemctl status mysql

If it's down, start it. If it won't start, its own error log is your next stop, not Matomo's.

The next trick fixes [2002] more often than it has any right to: swap localhost for 127.0.0.1. On a lot of Linux systems the literal string localhost tells the MySQL client to connect over a Unix socket, while 127.0.0.1 forces a TCP connection to port 3306. If the socket path is wrong or the server isn't listening where you think, localhost fails and 127.0.0.1 quietly works. Change host in config/config.ini.php and try again.

If that doesn't do it, work outward: confirm the port (3306 by default) and check that nothing between Matomo and the server is dropping packets. For a remote or containerised database that's usually a host firewall or a security group. One cause people forget, and one Matomo's own FAQ calls out: a database server that has run out of disk space starts refusing connections. Run df -h on the database host.

If you actually want a socket connection, separate the installer from an already-installed Matomo. During installation, Matomo still accepts a socket path in the database host field, such as /var/run/mysqld/mysqld.sock. In config/config.ini.php after install, prefer the dedicated key; when it's set, it overrides whatever host and port you've configured:

ini
[database]
unix_socket = "/var/run/mysqld/mysqld.sock"
Config valueWhat it usually doesUse it when
host = "localhost"May use the default MySQL socket on Unix-like systemsMatomo and MySQL are on the same host and the socket path is correct
host = "127.0.0.1"Forces TCP to the local MySQL listenerlocalhost is trying the wrong socket
unix_socket = "/path/to/sock"Uses the explicit socket and overrides host/portYou know the socket path after installation

This same connectivity layer is what trips up scheduled archiving. If your cron job can reach the database from the shell but archiving still fails, the problem is usually downstream of the connection itself, which we pulled apart in why Matomo's cron archiving returns an invalid response.

Treat missing grants and load-data checks separately

If the system check is waving red flags about CREATE TEMPORARY TABLES or LOAD DATA INFILE, don't treat them as the same class of problem. CREATE TEMPORARY TABLES belongs in Matomo's required database grants. LOAD DATA INFILE is a performance feature; enabling it can speed archiving, but it comes with security and hosting constraints.

CREATE TEMPORARY TABLES is already in the grant statement above, and in any GRANT ALL ... ON matomo_db.*. If you ran that, you have it.

CheckRequired for Matomo?ScopeFixSecurity note
CREATE TEMPORARY TABLESYesMatomo databaseInclude it in the database-level grant statementDo not ignore this one
LOCK TABLESYesMatomo databaseInclude it in the same grant statementUsually safe when scoped to the Matomo database
LOAD DATA INFILENo, but useful for performanceServer-side MySQL file importMake MySQL able to read Matomo's tmp/assets files and grant FILE if neededFILE is global and should be weighed carefully
LOAD DATA LOCAL INFILENo, but useful for performanceClient-side upload from the Matomo/PHP sideEnable MySQL client/server local-infile support and mysqli.allow_local_infile = On when using mysqliOften the better route when MySQL is remote

LOAD DATA INFILE is the fussy one. Matomo checks both LOAD DATA INFILE and LOAD DATA LOCAL INFILE, and either one working is enough. The server-side form may need the global FILE privilege, and that one, unlike the Matomo database grants, can't be granted at the database level:

sql
GRANT FILE ON *.* TO 'matomo'@'localhost';

Matomo's own docs note that handing out FILE is generally not recommended for security reasons, so weigh it before you do. Server-side loading also means the database server process has to read the files Matomo creates under tmp/assets, which can collide with AppArmor, secure_file_priv, or a separate database host that cannot see the Matomo filesystem.

The local form is different. It can work when the database lives on another server, but it needs local-infile support in the MySQL configuration and, for the mysqli driver, mysqli.allow_local_infile = On in php.ini.

Two more things worth knowing. There's a known false positive where some versions report a LOAD DATA INFILE warning even when the privilege is correctly in place (matomo-org/matomo issue #19267). And if you accept the performance tradeoff and want Matomo to stop trying this feature, turn it off:

ini
[General]
enable_load_data_infile = 0

Matomo keeps working. Archiving is a little slower on large data sets, and that's the whole cost.

Confirm the fix

Don't trust the absence of an error. Re-run the check. In the UI that's Administration → Diagnostics → System Check; from the CLI:

bash
./console diagnostics:run

And to see what the database actually thinks your user can do:

sql
SELECT CURRENT_USER(), USER();
SHOW GRANTS;
-- From an admin account, replace this with the account CURRENT_USER() showed:
SHOW GRANTS FOR 'matomo'@'localhost';

A clean diagnostics page and a dashboard that loads means you're done.

If your error turned up right after a Matomo upgrade rather than at install time, rule out the other classic upgrade-time database failure too: a half-applied migration that leaves a table behind, which we covered in Matomo's "table doesn't exist" error after an upgrade.

What we'd actually do

Read the code, then fix the matching half. A [1045] is never a Matomo bug. It's credentials, host, or grants, and you fix it on the database side. A [2002] is reachability: service down, wrong host string, firewall, disk. Credentials are a red herring until the connection opens. For privilege rows, add required database grants first; decide separately whether LOAD DATA INFILE is worth enabling or whether enable_load_data_infile = 0 is the right tradeoff for your install.

To keep all three from coming back, create a dedicated database user scoped to the Matomo database with exactly the grant statement above, and run a current stack instead of the stale floor. Matomo's current docs recommend the latest PHP 8.x release and MySQL 8+ or MariaDB for new or maintained installs. Persist config/config.ini.php too, so a container restart doesn't drop you back into the installer pointing at the wrong host.

The error already told you which half you're in. For a simple single-server install, this is often a five-minute fix. For Docker, managed databases, or Kubernetes, the same buckets apply, but the important part is testing from the Matomo runtime and granting the narrowest host that actually connects.

Martez connects Matomo with Meta Ads and Google Ads so ROAS, CLV, and attribution sit next to your web analytics instead of in a separate spreadsheet. It's in private beta. Join the waitlist if that's relevant.