</>CodeWithKarani

MySQL server has gone away: four unrelated causes, one message

Karani GeoffreyKarani Geoffrey8 min read

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

CauseSignature symptomCheck
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
Did every other connection fail at the same moment? Yes: the server restarted or was killed Check Uptime, the mysqld error log, dmesg for the OOM killer No: only this connection died Keep going down the tree Was the connection idle before the failing query? Yes: wait_timeout. Fix the pool, not the timeout Was the statement or row unusually large? Yes: max_allowed_packet, on server and client Otherwise: a read/write timeout somewhere on the path Answer the questions in this order. Three of the four causes are ruled out in about two minutes, and you avoid changing settings that were never the problem in the first place.
Diagnosis before configuration. Most of the wasted time on this error comes from fixing the cause somebody else had.

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

  1. Compare Aborted_clients and Aborted_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.
  2. Look for a killer. A KILL statement, a supervisory script, or a tool like pt-kill terminating long queries produces exactly this error on the client. Check for anything on the box whose job is to end long-running queries.
  3. Check for a crash loop. If Uptime keeps 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.
  4. 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.
#MySQL#MariaDB#Connection Pooling#SQLAlchemy#Troubleshooting
Keep reading

Related articles