Jonathan Bird Web Development

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 refused
2(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:

  1. The database server isn't running (or isn't ready yet)
  2. 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 directory means PHP tried to use a Unix socket (this happens when the host is localhost) and the socket file wasn't where it expected
  • SQLSTATE[HY000] [2002] Operation timed out usually 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 failed means the hostname didn't resolve at all
  • SQLSTATE[HY000] [1045] Access denied is a different beast entirely: you reached the server but the credentials were wrong
  • Connection refused [tcp://127.0.0.1:6379] is the same failure but for Redis, not MySQL: usually CACHE_STORE or QUEUE_CONNECTION was switched to redis with 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, with listen_addresses and pg_hba.conf playing the role MySQL's bind-address plays 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# Docker
10docker 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=mysql
2DB_HOST=127.0.0.1
3DB_PORT=3306
4DB_DATABASE=myapp
5DB_USERNAME=root
6DB_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=pgsql
2DB_HOST=127.0.0.1
3DB_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 client
2SHOW VARIABLES LIKE '%socket%';

Then in .env:

1DB_HOST=localhost
2DB_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=mysql
2DB_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: myapp
13 MYSQL_ROOT_PASSWORD: secret
14 healthcheck:
15 test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
16 interval: 5s
17 timeout: 5s
18 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.1 and the published port from the ports: 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; add extra_hosts: ["host.docker.internal:host-gateway"] to the service, and make sure the host's MySQL isn't bound to 127.0.0.1 only, 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 server
2# (Ctrl+C and restart php artisan serve)
3 
4# Queue workers pick up fresh config after a restart signal
5php 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: testing
10 MYSQL_ROOT_PASSWORD: password
11 ports:
12 - 3306:3306
13 options: >-
14 --health-cmd="mysqladmin ping"
15 --health-interval=10s
16 --health-timeout=5s
17 --health-retries=5
18 
19 steps:
20 # ...
21 - name: Run tests
22 env:
23 DB_HOST: 127.0.0.1
24 DB_PORT: 3306
25 DB_DATABASE: testing
26 DB_USERNAME: root
27 DB_PASSWORD: password
28 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:

  1. nc -zv <host> <port> fails: the server isn't running, isn't listening on that interface/port, or a firewall is in the way
  2. nc succeeds but mysql CLI fails: check the client config and credentials (a 1045 error here confirms you're reaching the server)
  3. 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:

  1. Confirm the server is running and listening with nc -zv 127.0.0.1 3306 and your service manager
  2. Use 127.0.0.1, not localhost, unless you deliberately want a socket connection (then set DB_SOCKET)
  3. Check the port matches your tool (DBngin, MAMP, and Docker mappings all vary)
  4. In Docker, use the service name from inside containers, 127.0.0.1 plus the published port from the host, and host.docker.internal to reach a database running on the host machine
  5. Clear the config cache with php artisan config:clear and restart artisan serve and queue workers
  6. For remote databases, line up bind-address, the firewall, and the MySQL user's allowed host
  7. 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 article

How 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

Your website should be a business asset, not a headache

For over 15 years I've helped Australian enterprise, government, and not-for-profit organisations ship websites that are fast, secure, and accessible. Get in touch to discuss your project today.