# MySQL server has gone away (error 2006) in Laravel

> MySQL closed the connection: an idle queue worker passed wait_timeout, a query hit max_allowed_packet or the server restarted. Fixes for Laravel.

```text
SQLSTATE[HY000]: General error: 2006 MySQL server has gone away
```

The connection your PHP process had open was closed by the other side. The server timed it out, refused a packet that was too big, restarted, or something in between (a proxy, a load balancer) dropped it. The close of a connection mid-query shows up as `2013 Lost connection to MySQL server during query` instead; the causes are the same.

## Why Laravel usually hides it, and when it doesn't

Laravel already reconnects for you. When a query fails with a lost-connection error, `Illuminate\Database\Connection` reconnects once and runs the query again. You see "gone away" only when that retry isn't allowed or doesn't help:

- **Inside a transaction.** Laravel never retries a query inside `DB::transaction()` or after `DB::beginTransaction()`, because the earlier statements of that transaction are gone with the connection. The exception reaches your code.
- **The retry fails the same way.** A packet that's too large is too large on the new connection too.
- **The server is really gone**, so the reconnect fails with [SQLSTATE[HY000] [2002]](/errors/sqlstate-hy000-2002/).

So the question is what kills your connections, and whether you hold a transaction open while it happens.

## A long-running process sat idle past `wait_timeout`

The classic cause. A web request opens a connection and closes it within a second. A queue worker, `php artisan schedule:work`, an Octane worker or a long Artisan command keeps one connection open for hours. MySQL closes connections that have been idle longer than `wait_timeout`:

```sql
SHOW VARIABLES WHERE Variable_name IN ('wait_timeout', 'interactive_timeout', 'max_allowed_packet');
```

The MySQL default is 28800 seconds (8 hours), but managed and shared hosts often set 60 to 600 seconds. A worker on a quiet queue, or a job that spends 10 minutes on an API call between two queries, crosses that easily.

The dangerous pattern is a transaction around slow work:

```php
// Breaks when the API call takes longer than wait_timeout
DB::transaction(function () use ($order) {
    $order->update(['status' => 'exporting']);
    $file = Http::timeout(900)->get($this->exportUrl)->body(); // minutes of idle connection
    $order->update(['status' => 'exported', 'file' => $file]);
});
```

Do the slow part outside the transaction, and keep transactions to the queries that must succeed together:

```php
$order->update(['status' => 'exporting']);

$file = Http::timeout(900)->get($this->exportUrl)->body();

DB::transaction(function () use ($order, $file) {
    $order->update(['status' => 'exported', 'file' => $file]);
    $order->events()->create(['type' => 'exported']);
});
```

If you can't avoid a long idle gap before a transaction, reconnect explicitly before you start it:

```php
use Illuminate\Support\Facades\DB;

DB::reconnect();

DB::transaction(function () {
    // ...
});
```

For workers, also make the processes themselves short-lived. They restart cleanly and pick up new code and config:

```bash
php artisan queue:work redis --max-time=3600 --max-jobs=1000
```

Run them under Supervisor or Forge's daemons so they come back after each exit. When a worker does hit a lost connection, Laravel stops it on purpose, so a worker without a process manager stays down.

Raising `wait_timeout` on the server works too, but only if you control it, and it treats the symptom.

## A query or row is larger than `max_allowed_packet`

Inserting a large blob, a big JSON column or a bulk insert of thousands of rows in one statement can exceed `max_allowed_packet`. MySQL then drops the connection, and PHP reports it as gone away. Because the retry sends the same packet, it fails every time, for the same data.

Confirm it: the error comes from the same query every time, with large input. Fix it by inserting in smaller batches:

```php
foreach (array_chunk($rows, 500) as $chunk) {
    DB::table('order_lines')->insert($chunk);
}
```

Or raise the limit in the server config (`max_allowed_packet = 64M` under `[mysqld]`, which is the MySQL 8 default; MariaDB defaults to 16M) and restart MySQL. Large files belong in storage, with the path in the database.

## The server restarted, failed over or ran out of memory

A MySQL restart, a managed database failover or maintenance window, or the Linux OOM killer stopping `mysqld` closes every connection at once. You see a burst of "gone away" across workers and requests at the same minute. Check the MySQL error log and `dmesg` for `Out of memory: Killed process … (mysqld)`, and the provider's event log for maintenance.

## A proxy or load balancer drops idle connections

ProxySQL, RDS Proxy and cloud load balancers have their own idle timeouts, often shorter than MySQL's. Azure's load balancer, for example, drops idle TCP connections after 4 minutes by default ([Azure docs](https://learn.microsoft.com/azure/load-balancer/load-balancer-tcp-idle-timeout), checked 10 Nov 2026). The fixes are the same as for `wait_timeout`: shorter-lived workers, no idle transactions, and an explicit `DB::reconnect()` after a long pause.

## Catch it in production

"Gone away" in a queue job usually means the job fails or is released, and it often happens at night on a quiet queue. With the [Laravel package](/docs/laravel/), Lampwick shows each job class under **Jobs** with its processed, released and failed counts, and every `QueryException` becomes an issue in **Issues** with the job it broke. A burst of the same issue across many jobs at one minute points at the server; the same job failing every time points at `max_allowed_packet` or a transaction around slow work. [Why Laravel queued jobs fail](/guides/laravel-failed-jobs/) covers what happens to the job after that, and the [SQLSTATE overview](/errors/sqlstate/) covers the other database errors.
