MySQL server has gone away: four unrelated causes, one message
Two different mornings, same error message. On the first, a 4GB dump import dies at 38 percent after twenty minutes of steady progress. On the second, a background worker that has been idle since the previous evening throws on the very first query of the day, before doing any work at all.
Both say MySQL server has gone away. They have nothing in common.
That is the thing to understand about this error before you change a single setting. It is not a
diagnosis, it is a shrug. The client wrote to a socket, the socket was not there any more, and the
client has no idea why. At least four unrelated failures produce it, and the fix for each one is
different. Search results will tell you to raise max_allowed_packet, which addresses
exactly one of them, which is why so many people raise it, redeploy, and hit the same error the next
morning.
identify which of the four causes you have before changing anything.
- Idle, then failed on the first query to
wait_timeout. Fix it in the connection pool, not by raising the timeout. - Failed on a big statement, a BLOB, or a dump import to
max_allowed_packet, on both the server and the client. - Failed part-way through a long query to a read or write timeout, on the client, the server, or a proxy in the middle.
- Failed at a random point, and other connections died at the same instant to the
server restarted or was killed. Check the error log and
dmesg.
The exact errors
ERROR 2006 (HY000): MySQL server has gone away
ERROR 2013 (HY000): Lost connection to MySQL server during query
These are client-side error numbers, CR_SERVER_GONE_ERROR and
CR_SERVER_LOST. The difference is genuinely useful: 2006 means the client could not even
send the statement, so the connection was already dead when it tried. 2013 means the statement went
out and the answer never came back, so the connection died mid-flight. In Python you will see it
like this:
pymysql.err.OperationalError: (2006, "MySQL server has gone away")
django.db.utils.OperationalError: (2013, 'Lost connection to MySQL server during query')
The four causes, and how to tell them apart
| Cause | Signature symptom | Check |
|---|---|---|
Idle connection reaped by wait_timeout |
First query after a quiet period. Reliable at the same time each morning. Second attempt works | SHOW VARIABLES LIKE 'wait_timeout' |
Statement larger than max_allowed_packet |
Always the same row, the same BLOB, or the same point in a dump. Perfectly reproducible | SHOW VARIABLES LIKE 'max_allowed_packet' on server and client |
| Read/write timeout on the client or a proxy | Long-running query dies after a suspiciously round number of seconds | Client driver timeout, net_read_timeout, load balancer idle timeout |
| The server actually died or restarted | Every connection fails at the same instant, then recovers | SHOW GLOBAL STATUS LIKE 'Uptime', error log, dmesg |
Step 1: Rule out a server restart in thirty seconds
SHOW GLOBAL STATUS LIKE 'Uptime';
SHOW GLOBAL STATUS LIKE 'Aborted_clients';
Uptime is in seconds. If it is smaller than the time since your application started,
the server restarted underneath you and nothing else you read here applies. On Linux, confirm what
happened:
sudo journalctl -u mysql --since "2 hours ago" | tail -40
sudo dmesg -T | grep -i -E 'killed process|out of memory'
If the OOM killer took mysqld, the fix is memory sizing, not connection settings. On the small
VPS instances a lot of us run, the usual culprit is an innodb_buffer_pool_size copied
from a tutorial written for a machine four times larger, with no swap configured to absorb the
overshoot.
Step 2: Check the timeouts and packet size, and note they differ per install
SHOW VARIABLES WHERE Variable_name IN (
'wait_timeout', 'interactive_timeout', 'max_allowed_packet',
'net_read_timeout', 'net_write_timeout');
Typical output on a stock MySQL 8.0:
+--------------------+----------+
| Variable_name | Value |
+--------------------+----------+
| interactive_timeout| 28800 |
| max_allowed_packet | 67108864 |
| net_read_timeout | 30 |
| net_write_timeout | 60 |
| wait_timeout | 28800 |
+--------------------+----------+
wait_timeout of 28800 seconds is the documented eight hours. Do not assume the packet
size, though: the 64MB default is MySQL 8.0's, and MySQL 5.7 and MariaDB ship different values. Read
it, do not remember it.
One subtlety that costs people hours: wait_timeout applies to non-interactive
connections and interactive_timeout to interactive ones such as the
mysql client. Testing with the CLI can therefore behave completely differently from your
application, and a managed database provider may have set them far lower than the defaults above.
Step 3: Fix cause A, the idle connection, in the pool
The server closed an idle connection after eight hours, exactly as documented. Your pool then handed that dead connection to the next request. The bug is the pool believing a connection is alive because it was alive when it was put away.
SQLAlchemy, which covers most Python applications:
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True, # cheap liveness check before handing out a connection
pool_recycle=1800, # never reuse a connection older than 30 minutes
pool_size=10,
max_overflow=5,
)
The principle is identical everywhere else, only the names change: a validation query or ping on
borrow, plus a maximum connection lifetime comfortably below the server's
wait_timeout. In HikariCP that is maxLifetime, and if yours is longer than
the server timeout you will keep seeing this error no matter what else you tune. Related pool
misconfiguration shows up as
connection timeouts that are not a pool
size problem.
Step 4: Fix cause B, the oversized packet, on both ends
Both server and client enforce their own max_allowed_packet, and raising it on the
server alone is the classic half-fix. For a large import, pass it to the client explicitly:
mysql --max-allowed-packet=512M -u root -p mydb < dump.sql
mysqldump --max-allowed-packet=512M -u root -p mydb > dump.sql
On the server, set it in the configuration file so it survives a restart:
[mysqld]
max_allowed_packet = 256M
You can raise it live with SET GLOBAL max_allowed_packet = 268435456;, but existing
sessions keep the value they connected with, so reconnect before testing. Do not simply set it to the
maximum and forget it: the value is an upper bound on a buffer the server may allocate per
connection, so an enormous limit combined with a lot of connections is a memory problem waiting for a
busy afternoon.
Step 5: Fix cause C, the timeout on a long query
If a query consistently dies after 30, 60, 120 or 300 seconds, something is enforcing that number.
Work outwards from the client. Driver-level read timeouts are the first suspect
(read_timeout in PyMySQL, socketTimeout in Connector/J,
default_socket_timeout in PHP). Then net_read_timeout and
net_write_timeout on the server. Then anything sitting between the two.
That last one is the cause almost no article mentions and I have hit repeatedly: a load balancer, NAT gateway or ProxySQL instance with an idle timeout shorter than your query. It silently drops the TCP connection, the client discovers this only when it next tries to read, and you get "gone away" with a perfectly healthy database. If your app and your database are in different networks, or you are reaching the database over a VPN or a managed proxy endpoint, check the timeout on that hop before you touch MySQL at all.
Verification
Reproduce the cause deliberately, then prove your fix removes it. The idle case is the easiest to force:
SET SESSION wait_timeout = 10;
SELECT SLEEP(12);
SELECT 1;
Expected result on the last statement:
ERROR 2006 (HY000): MySQL server has gone away
Now run the same shape of test through your application: open a pooled connection, hold it idle
past the timeout, and issue a query. With pool_pre_ping and a sane recycle time, the
request should succeed, and your pool metrics should show one connection discarded and replaced. If
you have no metric for that, add one. It is the number that tells you whether your fix is working
three months from now.
For the packet case, confirm the effective value from the same connection the application uses, not from a root session on the server:
SELECT @@session.max_allowed_packet, @@global.max_allowed_packet;
What people get wrong
Setting max_allowed_packet to 1G as a first move. If your failure is
an idle connection, this does nothing except add a memory risk. Fix what you measured.
Setting wait_timeout to a year. The timeout exists because clients
leak connections. Disable it and every abandoned connection stays forever, sleeping threads
accumulate, and eventually you hit max_connections and nothing can connect at all. You
have traded a clear error for an outage. Keep the timeout and make the pool honest.
Enabling automatic reconnection. MySQL Connector/J's
autoReconnect=true and its equivalents in other drivers are actively dangerous: on
reconnect you get a brand new session, so any open transaction is gone, temporary tables are gone,
session variables and locks are gone, and your code carries on as though nothing happened. The MySQL
documentation warns against it for exactly this reason. Retry an idempotent unit of work at the
application level instead, where you control what gets replayed.
Blaming the network reflexively. Sometimes it genuinely is the network, but "gone away" on its own is not evidence of that. Rule out the three cheap causes first, because they account for most occurrences and take two minutes to check.
When it is still happening
- Compare
Aborted_clientsandAborted_connects. The first counts connections dropped after being established, which points at timeouts and clients that vanish without closing. The second counts failures during connection setup, which points at authentication, DNS or firewall problems. They send you to different places. - Look for a killer. A
KILLstatement, a supervisory script, or a tool likept-killterminating long queries produces exactly this error on the client. Check for anything on the box whose job is to end long-running queries. - Check for a crash loop. If
Uptimekeeps resetting, the server is restarting repeatedly and the error log will show InnoDB recovery on each start. That is a storage or memory problem, and the connection errors are a symptom. - Watch for MTU and packet loss on tunnelled links. Traffic crossing a VPN or between regions with a smaller path MTU can stall large result sets specifically, so small queries succeed and big ones die. It looks like a packet size problem and is not one.
If the connections that survive are also slow, that is a separate investigation and the
slow query log is where it starts. And if
you are seeing Deadlock found when trying to get lock alongside this, that one is
an expected event you retry, not a
connection problem at all.
Frequently asked questions
- What causes the MySQL error 'server has gone away'?
- It means the client found the connection closed when it tried to use it, and there are at least four unrelated reasons for that: an idle connection closed by wait_timeout after the default eight hours, a statement larger than max_allowed_packet, a read or write timeout on the client or a proxy in the path, or the MySQL server itself restarting or being killed. The error text is identical in all four cases, so you have to diagnose which one you have before changing settings.
- What is the difference between MySQL error 2006 and 2013?
- Error 2006, CR_SERVER_GONE_ERROR, means the client could not even send the statement because the connection was already dead. Error 2013, CR_SERVER_LOST, means the statement was sent but the answer never arrived, so the connection died mid-query. The first points at an idle or previously closed connection, the second at something interrupting an in-flight query.
- Should I increase wait_timeout to stop MySQL connections being closed?
- No. The timeout exists to reclaim connections that clients abandon, and disabling it lets sleeping threads accumulate until you exhaust max_connections and nothing can connect at all. Fix it in the connection pool instead: validate connections before handing them out (pool_pre_ping in SQLAlchemy) and set a maximum connection lifetime comfortably below the server's wait_timeout.
- Why does my large MySQL import fail part way through with 'server has gone away'?
- Almost always because a single statement in the dump exceeds max_allowed_packet, typically a wide row or a BLOB. Both the server and the client enforce their own limit, so raise it in both places: pass --max-allowed-packet=512M to the mysql client and set max_allowed_packet in the [mysqld] section of the server config. Avoid setting it to an enormous value permanently, since it bounds a per-connection buffer.