The symptom
The application log shows one of these, often at random and with no obvious pattern:
ERROR 2006 (HY000): MySQL server has gone away
ERROR 2013 (HY000): Lost connection to MySQL server during query
ERROR 4031 (HY000): The client was disconnected by the server because of inactivity.
All three mean the same thing at the TCP level: the connection the client was using is no longer there. The error number tells you when the client noticed, not why it happened:
- 2006 - the client tried to send a statement on a connection that was already closed.
- 2013 - the connection dropped while a statement was running or results were streaming back.
- 4031 - newer servers (8.0.24 and later) tell the client it was closed for inactivity, which points straight at
wait_timeout.
Your job is to find which of five usual causes closed it.
Quick map: symptom to cause
| What you see | Likely cause | Fix |
|---|---|---|
| 2006 or 4031 after the app sat idle | wait_timeout closed an idle pooled connection | Pool maxLifetime / recycle shorter than wait_timeout, validate on borrow |
| 2006/2013 on a large INSERT, BLOB or import | Packet bigger than max_allowed_packet | Raise it on server and client |
| 2013 on a long result set or slow client | net_write_timeout / net_read_timeout expired | Raise for that session, or stream faster |
| Drops after a fixed idle time, server shows nothing | Firewall, NAT or load balancer idle timeout | TCP keepalive, shorter pool lifetime |
| Every client drops at the same moment | Server restarted, crashed or failed over | Check Uptime, error log, OOM killer |
| One session drops mid-query | Someone or a tool ran KILL | Check audit/tooling (pt-kill, scripts) |
| 2013 "at 'reading initial communication packet'" | Connect stage: connect_timeout, DNS, firewall | See the connect section below |
Check 1: did the server restart?
Rule this out first, because it changes everything else.
SHOW GLOBAL STATUS LIKE 'Uptime';
SELECT NOW() - INTERVAL VARIABLE_VALUE SECOND AS started_at
FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Uptime';
If started_at matches the error time, read the error log and the kernel log:
sudo tail -200 /var/log/mysql/error.log # path varies; check log_error
sudo dmesg -T | grep -iE 'killed process|out of memory'
An OOM kill of mysqld is a memory sizing problem, not a connection problem. On RDS, check the instance events (aws rds describe-events --source-identifier mydb --source-type db-instance --duration 1440); on Azure, check the server's Activity log and Resource Health.
Check 2: read the aborted-connection notes
The server records why it dropped a connection, but only at log_error_verbosity = 3 (notes). Turn it on temporarily:
SELECT @@log_error_verbosity;
SET GLOBAL log_error_verbosity = 3;
Then look for lines like:
[Note] Aborted connection 1234 to db: 'shop' user: 'app' host: '10.20.4.17' (Got timeout reading communication packets).
[Note] Aborted connection 1235 to db: 'shop' user: 'app' host: '10.20.4.17' (Got a packet bigger than 'max_allowed_packet' bytes).
[Note] Aborted connection 1236 to db: 'shop' user: 'app' host: '10.20.4.17' (Got an error reading communication packets).
| Log reason | Meaning |
|---|---|
Got timeout reading communication packets | Idle past wait_timeout, or net_read_timeout mid-statement |
Got a packet bigger than 'max_allowed_packet' bytes | Packet limit |
Got an error reading communication packets | Client or something in between closed the socket without a clean QUIT |
The counters tell you the rate:
SHOW GLOBAL STATUS WHERE Variable_name IN
('Aborted_clients','Aborted_connects','Connection_errors_max_connections',
'Connection_errors_internal','Connection_errors_peer_address');
Aborted_clients rising steadily is the classic idle-timeout or "app never closes connections" pattern. Set log_error_verbosity back to 2 when you are done if notes are too noisy.
Check 3: the timeout settings
SELECT @@global.wait_timeout, @@global.interactive_timeout,
@@global.net_read_timeout, @@global.net_write_timeout,
@@global.connect_timeout, @@global.max_allowed_packet;
Then check what the application's sessions actually got. Some drivers and frameworks set wait_timeout per session:
SELECT t.PROCESSLIST_ID, t.PROCESSLIST_USER, v.VARIABLE_VALUE AS session_wait_timeout
FROM performance_schema.threads t
JOIN performance_schema.variables_by_thread v ON v.THREAD_ID = t.THREAD_ID
WHERE v.VARIABLE_NAME = 'wait_timeout' AND t.PROCESSLIST_USER = 'app';
Fix: idle connections closed by wait_timeout
This is the most common cause. A connection pool keeps connections open for hours; the server closes any connection idle longer than wait_timeout; the next borrower gets 2006 or 4031.
Do not just set wait_timeout to a huge value. Abandoned connections then pile up until you hit max_connections. Fix the pool instead:
- HikariCP (Java):
maxLifetimea minute or more below the lowest timeout in the path (wait_timeout, proxy and firewall idle timeouts). SetkeepaliveTimeif a network device drops idle flows. - SQLAlchemy (Python):
pool_pre_ping=Trueandpool_recyclebelowwait_timeout.
from sqlalchemy import create_engine
engine = create_engine(
"mysql+pymysql://app:***@db.internal/shop",
pool_pre_ping=True, # test the connection before handing it out
pool_recycle=1800, # seconds; keep below wait_timeout
)
- Long batch jobs that sleep between steps: reconnect on 2006, or set a longer
wait_timeoutfor that session only:
SET SESSION wait_timeout = 7200;
Fix: max_allowed_packet
A single statement or row bigger than the limit closes the connection. The server and the client each have their own limit; the smaller one wins.
-- Self-managed, survives restart (8.0+)
SET PERSIST max_allowed_packet = 268435456; -- 256 MB
New connections pick up the global value; existing pooled connections keep the old one until they reconnect.
For managed services, change the parameter rather than using SET PERSIST:
# Amazon RDS / Aurora MySQL (custom parameter group required)
aws rds modify-db-parameter-group \
--db-parameter-group-name mysql84-app \
--parameters "ParameterName=max_allowed_packet,ParameterValue=268435456,ApplyMethod=immediate"
# Azure Database for MySQL Flexible Server
az mysql flexible-server parameter set \
--resource-group rg-db --server-name mysql-prod-01 \
--name max_allowed_packet --value 268435456
And raise the client side for imports and dumps:
mysql --max_allowed_packet=256M -h db.internal -u app -p shop < big_import.sql
mysqldump --max_allowed_packet=256M --single-transaction shop > shop.sql
Warning
max_allowed_packetis a per-connection buffer ceiling. Raising it to the maximum on a server with thousands of connections increases worst-case memory use. Raise it to what the largest real row or statement needs, and prefer batching huge multi-row INSERTs into smaller chunks.
Fix: network timeouts during long transfers
When a client reads a large result set slowly (for example, processing each row before fetching the next), the server can hit net_write_timeout while waiting to send. When a client uploads slowly, the server can hit net_read_timeout. Raise them for the session that needs it, not globally:
SET SESSION net_write_timeout = 600;
SET SESSION net_read_timeout = 600;
Fix: proxies, load balancers and firewalls
If the server log shows Got an error reading communication packets (or nothing) and the drops happen after a consistent idle period, something in the network path is timing out idle TCP flows. Check the idle timeout of your load balancer, NAT gateway, firewall or ProxySQL/MySQL Router, then make the pool lifetime shorter than the smallest of them. Enabling TCP keepalive in the driver also helps; check your driver's option name.
Connect-stage 2013
Lost connection to MySQL server at 'reading initial communication packet' happens before authentication. Check reachability and the host cache:
nc -vz db.internal 3306
SELECT IP, HOST, SUM_CONNECT_ERRORS, COUNT_HANDSHAKE_ERRORS
FROM performance_schema.host_cache ORDER BY SUM_CONNECT_ERRORS DESC LIMIT 10;
Slow reverse DNS can push the handshake past connect_timeout; skip_name_resolve = ON removes that dependency (accounts must then use IP addresses or %, not host names).
Prevent it next time
- Set pool lifetime below
wait_timeoutand every network idle timeout, and validate connections on borrow. Write the numbers down in the service runbook. - Alert on the rate of
Aborted_clientsandAborted_connects, not the absolute value. - Alert on
Uptimeresetting, so restarts are never mistaken for a "network blip". - Size
max_allowed_packetfrom the largest real row or BLOB, and test bulk loads against the same setting production uses. - Keep application retry logic for 2006/2013 idempotent: a 2013 during a COMMIT means you do not know whether it committed.