How to Fix "SQLSTATE[HY000] [2002] Connection Refused" Error in Laravel 13 (2026 Guide)
Few Laravel errors are as blunt as "SQLSTATE[HY000] [2002] Connection refused". Your app asked the operating system to open a connection to the database, and the OS came back with a flat no. Nothing Laravel-specific has even happened yet: no query ran, no credentials were checked. The network connection itself never got off the ground.
In this guide, I'll walk you through what causes the "Connection refused" error, how to diagnose it quickly, and the various ways to fix it in Laravel 13. Whether you're on a fresh local install, inside Docker, deploying to a VPS, or watching a CI pipeline fail, I've got you covered in this article.
What is the "SQLSTATE[HY000] [2002] Connection Refused" Error?
This error surfaces as an Illuminate\Database\QueryException wrapping a PDOException, and in Laravel 13 the message includes the connection and the query that triggered it:
1SQLSTATE[HY000] [2002] Connection refused2(Connection: mysql, SQL: select * from `sessions` where `id` = ... limit 1)
The [2002] is the MySQL client's "can't connect" error code, and "Connection refused" is the operating system's verdict: a TCP connection was attempted to the configured host and port, and nothing was listening there. That means the cause is almost always one of two things:
- The database server isn't running (or isn't ready yet)
- Laravel is connecting to the wrong place (wrong host, wrong port, or the right place from the wrong perspective, which is the classic Docker trap)
It's worth knowing the sibling messages too, because they point at slightly different problems:
SQLSTATE[HY000] [2002] No such file or directorymeans PHP tried to use a Unix socket (this happens when the host islocalhost) and the socket file wasn't where it expectedSQLSTATE[HY000] [2002] Operation timed outusually means a firewall silently dropped the connection instead of refusing it (a firewall configured to reject rather than drop shows up as "Connection refused" too, which is the rare third cause beyond the two above)php_network_getaddresses: getaddrinfo failedmeans the hostname didn't resolve at allSQLSTATE[HY000] [1045] Access deniedis a different beast entirely: you reached the server but the credentials were wrongConnection refused [tcp://127.0.0.1:6379]is the same failure but for Redis, not MySQL: usuallyCACHE_STOREorQUEUE_CONNECTIONwas switched torediswith no Redis server running. Same diagnosis applies (nc -zv 127.0.0.1 6379)- PostgreSQL wears a different code for the same problem:
SQLSTATE[08006] [7] ... Connection refused, withlisten_addressesandpg_hba.confplaying the role MySQL'sbind-addressplays below
One more thing that makes this error feel worse than it is in Laravel 13: sessions, cache, and queues all default to the database driver. So when the database is unreachable, every single page errors, including pages that don't query any of your own tables. Don't let that spook you. It's still just one connection problem.
Common Causes of the Connection Refused Error
The Database Server Isn't Running
The most common cause by a wide margin, especially locally. MySQL crashed, was never started after a reboot, the Docker container exited, or DBngin is sitting there with the service stopped.
localhost vs 127.0.0.1
These are not interchangeable for MySQL. When DB_HOST=localhost, the MySQL client tries to connect via a Unix socket file rather than TCP. When DB_HOST=127.0.0.1, it makes a TCP connection to that IP and port. If your MySQL only listens on TCP (or the socket file lives somewhere PHP doesn't expect), localhost fails while 127.0.0.1 works, and vice versa. This single distinction explains a huge share of 2002 errors.
The Wrong Port
MySQL defaults to 3306 and PostgreSQL to 5432, but local tooling loves nonstandard ports. DBngin lets you run multiple MySQL versions on ports like 3307 or 3308, MAMP historically used 8889, and Docker maps container ports to whatever the compose file says. If DB_PORT doesn't match where the server actually listens, connection refused.
Docker: localhost Points at the Wrong Container
Inside a Docker container, 127.0.0.1 means that container, not your machine and not the database container. So a Laravel container with DB_HOST=127.0.0.1 is asking itself whether it runs MySQL, and the answer is no. Containers reach each other by service name on the shared network. With Laravel Sail that's DB_HOST=mysql, because mysql is the service name in docker-compose.yml.
The mirror image applies from your host machine: GUI tools like TablePlus connect to 127.0.0.1 on the published port, not the service name.
A Stale Config Cache
If you've run php artisan config:cache, Laravel reads config exclusively from the cached file and ignores .env entirely. Fix your .env all you like, the app keeps connecting to the old host until the cache is rebuilt. The same trap applies to long-running processes: php artisan serve, queue workers, Octane, and Horizon all read the environment at boot, so they keep the old values until restarted.
Production: MySQL Only Listens Locally
On Ubuntu and Debian MySQL packages, bind-address = 127.0.0.1 is the shipped default, meaning the server only accepts connections from the same machine. If your app server and database server are separate hosts, the connection is refused until MySQL is told to listen on the private network interface (and the firewall allows it).
CI Pipelines: The Database Isn't Ready Yet
In GitHub Actions and similar CI environments, the MySQL service container takes a few seconds to initialise. If the test job starts querying before MySQL finishes booting (or the service was never declared, or the port isn't mapped), you get connection refused only in CI while everything works locally.
How to Fix the Connection Refused Error
Fix 1: Confirm the Database Is Actually Running and Listening
Before touching Laravel config, verify there's a server to connect to. Check whether anything is listening on the port:
1# Is anything listening on 3306?2nc -zv 127.0.0.1 3306
"Connection succeeded" means the server is up and the problem is in your Laravel config. "Connection refused" means the server side is the problem. Depending on your setup:
1# Homebrew on macOS 2brew services list 3brew services start mysql 4 5# Linux (systemd) 6sudo systemctl status mysql 7sudo systemctl start mysql 8 9# Docker10docker ps # is the database container in the list, and is it "Up"?11docker compose up -d
If you can connect with the CLI client, the server is fine:
1mysql -h 127.0.0.1 -P 3306 -u root -p
Fix 2: Set the Correct Host and Port in .env
For a typical local setup (Herd with DBngin, Homebrew MySQL, or similar):
1DB_CONNECTION=mysql2DB_HOST=127.0.0.13DB_PORT=33064DB_DATABASE=myapp5DB_USERNAME=root6DB_PASSWORD=
Check the port your tool actually uses. DBngin shows it next to each service, and a second MySQL version will be on 3307 or higher. If you're using PostgreSQL, the default port is 5432:
1DB_CONNECTION=pgsql2DB_HOST=127.0.0.13DB_PORT=5432
After any .env change, restart php artisan serve if you're using it. It doesn't pick up environment changes mid-flight.
Fix 3: Switch localhost to 127.0.0.1 (or Point at the Right Socket)
If your .env says DB_HOST=localhost and you're getting 2002 errors (particularly the "No such file or directory" variant), the quickest fix is forcing TCP:
1DB_HOST=127.0.0.1
If you specifically want the socket connection (it's marginally faster and some shared hosts require it), find the real socket path and tell Laravel about it:
1-- Run inside the mysql client2SHOW VARIABLES LIKE '%socket%';
Then in .env:
1DB_HOST=localhost2DB_SOCKET=/tmp/mysql.sock
The DB_SOCKET value flows into the unix_socket option in config/database.php, which Laravel supports out of the box.
Fix 4: Use Service Names Inside Docker and Sail
For Laravel Sail, the database host must be the compose service name:
1DB_HOST=mysql2DB_PORT=3306
For a custom docker-compose.yml, the same rule applies: whatever your database service is called is what goes in DB_HOST:
1services: 2 app: 3 build: . 4 depends_on: 5 db: 6 condition: service_healthy 7 db: 8 image: mysql:8.4 9 ports:10 - "3306:3306"11 environment:12 MYSQL_DATABASE: myapp13 MYSQL_ROOT_PASSWORD: secret14 healthcheck:15 test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]16 interval: 5s17 timeout: 5s18 retries: 10
Here the app container connects with DB_HOST=db. The healthcheck plus condition: service_healthy combination matters: a bare depends_on only waits for the MySQL container to start, not for MySQL itself to be ready for connections, so without it the app boots first and throws exactly this error on the first request after docker compose up. Both services also need to be on the same Docker network (compose puts them on a default shared network unless you've overridden it).
Three directions people mix up constantly:
- From inside a container to another container: use the service name (
db,mysql) - From your host machine to a container (Artisan commands run outside Docker, GUI clients): use
127.0.0.1and the published port from theports:mapping - From inside a container to a database on your host machine (DBngin, Homebrew MySQL): use
DB_HOST=host.docker.internal. On Linux that name isn't automatic; addextra_hosts: ["host.docker.internal:host-gateway"]to the service, and make sure the host's MySQL isn't bound to127.0.0.1only, or the container's connection gets refused
If you run php artisan migrate from the host while the app expects to run inside Docker, you'll need host-appropriate values, which is exactly what Sail solves by running everything inside the container: ./vendor/bin/sail artisan migrate.
Fix 5: Clear the Config Cache and Restart Long-Running Processes
Whenever the database settings look right but Laravel behaves like they're wrong:
1php artisan config:clear
Or clear everything:
1php artisan optimize:clear
Then restart anything long-running that captured the old config:
1# Local dev server2# (Ctrl+C and restart php artisan serve)3 4# Queue workers pick up fresh config after a restart signal5php artisan queue:restart
In production, the correct sequence in your deploy script is to update .env first, then run php artisan config:cache, then restart workers. Caching before the .env update bakes in the stale values.
Fix 6: Allow Remote Connections in Production
When the app and database live on separate servers, three things must line up.
1. MySQL must listen on the private network interface. Edit the config (commonly /etc/mysql/mysql.conf.d/mysqld.cnf on Ubuntu):
1bind-address = 10.0.0.5 # the DB server's private IP, not 127.0.0.1
Then restart MySQL.
2. The firewall must allow the app server in. For example with ufw:
1sudo ufw allow from 10.0.0.4 to any port 3306
3. The MySQL user must be allowed to connect from that host:
1CREATE USER 'myapp'@'10.0.0.4' IDENTIFIED BY 'strong-password';2GRANT ALL PRIVILEGES ON myapp.* TO 'myapp'@'10.0.0.4';3FLUSH PRIVILEGES;
Keep this on the private network. Never bind MySQL to 0.0.0.0 on a server with a public interface unless you have firewall rules that block 3306 from the internet; an exposed database port gets found by scanners within hours.
Fix 7: Declare the Service and Wait for It in CI
For GitHub Actions, the job needs a MySQL service with a health check so tests don't start before the server is ready:
1jobs: 2 tests: 3 runs-on: ubuntu-latest 4 5 services: 6 mysql: 7 image: mysql:8.4 8 env: 9 MYSQL_DATABASE: testing10 MYSQL_ROOT_PASSWORD: password11 ports:12 - 3306:330613 options: >-14 --health-cmd="mysqladmin ping"15 --health-interval=10s16 --health-timeout=5s17 --health-retries=518 19 steps:20 # ...21 - name: Run tests22 env:23 DB_HOST: 127.0.0.124 DB_PORT: 330625 DB_DATABASE: testing26 DB_USERNAME: root27 DB_PASSWORD: password28 run: php artisan test
The options block makes the runner wait until MySQL answers pings before starting your steps, which eliminates the flaky "refused in CI only" failures. Alternatively, sidestep the service entirely by testing against SQLite in memory (DB_CONNECTION=sqlite, DB_DATABASE=:memory: in phpunit.xml), which is also faster.
Debugging the Error
Ask Laravel What Config It's Actually Using
Don't trust your .env file; trust what Laravel resolved from it. This shows the effective database config, cached or not:
1php artisan config:show database
Look at connections.mysql.host and port in the output. If they don't match your .env, you've got a cached config or an environment variable set at the server level overriding the file.
Test the Connection From Tinker
1php artisan tinker
1DB::connection()->getPdo();
If this returns a PDO instance, Laravel can connect and your problem is elsewhere. If it throws, you get the raw error without any page noise around it. The built-in php artisan db:show command gives a friendlier version of the same check, printing the connection details and table list when it succeeds.
Work Out Which Layer Refused
A useful mental checklist, from the bottom up:
nc -zv <host> <port>fails: the server isn't running, isn't listening on that interface/port, or a firewall is in the wayncsucceeds butmysqlCLI fails: check the client config and credentials (a 1045 error here confirms you're reaching the server)- CLI works but Laravel fails: the values Laravel is using differ from the ones you just tested, which is nearly always config cache, a stale process, or Docker perspective
Special Cases
It Works in the Browser but Fails in Queue Workers (or Vice Versa)
Different processes can see different environments. A queue worker started before your .env change keeps the old connection details until php artisan queue:restart. Supervisor-managed workers and Horizon behave the same way. If the web app works and only background jobs throw 2002, a worker restart is almost always the fix.
It Fails Only When Running Tests
Your phpunit.xml can override database env vars. If tests suddenly can't connect, check what's in there:
1<env name="DB_CONNECTION" value="sqlite"/>2<env name="DB_DATABASE" value=":memory:"/>
If the test suite is meant to hit MySQL, make sure the host and port in phpunit.xml (or .env.testing) point somewhere that's actually running.
Managed Platforms (Forge, Laravel Cloud, etc.)
On managed platforms the database credentials are injected for you, and connection refused usually means the database service is still provisioning, was paused, or the environment variables in the dashboard don't match reality. Check the platform's database status page before debugging your own config. On Forge specifically, remember the .env is edited through the dashboard, and you need to restart PHP-FPM or redeploy for cached config to refresh.
After a Server Reboot
If everything worked yesterday and 2002s appeared overnight, the server probably rebooted and MySQL isn't set to start on boot:
1sudo systemctl enable --now mysql
The enable flag is the part people miss: start fixes it now, enable fixes it after the next reboot too.
Summary
"SQLSTATE[HY000] [2002] Connection refused" means nobody answered at the host and port Laravel dialled. Here's a quick checklist:
- Confirm the server is running and listening with
nc -zv 127.0.0.1 3306and your service manager - Use
127.0.0.1, notlocalhost, unless you deliberately want a socket connection (then setDB_SOCKET) - Check the port matches your tool (DBngin, MAMP, and Docker mappings all vary)
- In Docker, use the service name from inside containers,
127.0.0.1plus the published port from the host, andhost.docker.internalto reach a database running on the host machine - Clear the config cache with
php artisan config:clearand restartartisan serveand queue workers - For remote databases, line up
bind-address, the firewall, and the MySQL user's allowed host - In CI, declare the database service with a health check so tests wait for it
Once you know the error is purely "nothing was listening there", the fix is rarely more than one config line or one service start away. And if you get past the connection only to hit missing tables or columns, my SQLSTATE[42S22] column not found guide picks up where this one leaves off.
Having trouble with database connections, deployments, or other Laravel issues? I specialise in Laravel development and debugging complex application issues. Get in touch to discuss your project.
Topics
Related services
Syntax highlighting by Torchlight
More articles
How to Fix "The GET Method Is Not Supported for This Route" Error in Laravel 13 (2026 Guide)
The "GET method is not supported for this route" error means the URL matched a route, but the HTTP method did not. This guide covers all the common causes and how to fix them in Laravel 13, including form method spoofing with @method, logout links, redirects that silently turn POST into GET, API verb mismatches, and stale route caches.
Read articleHow to Hire a Laravel Developer in 2026 (Complete Guide)
How to hire a Laravel developer in 2026: what to look for, the questions to ask, the red flags to avoid, and what it actually costs in Australia, from a developer shipping production Laravel since 2013.
Read article