MySQL — Aborted connection ... Got timeout reading communication packets
Investigate MySQL aborted connection warnings, separate client-side abandonment from network issues, and align wait_timeout with pooled connection lifecycles.
Symptoms
- [Note] Aborted connection 1234 to db: got timeout reading communication packets floods the error log
- Aborted_clients climbs while connections seem healthy from the application perspective
- Intermittent query failures correlate with connection pool recycling or idle periods
Possible causes
- Pooled connections idle longer than wait_timeout, so the server closes them while the pool still believes they are alive
- Clients abandon queries mid-response (user canceled, request deadline hit) without closing cleanly
- Network middleboxes (NAT, firewalls, LBs) silently drop idle TCP streams, breaking the next packet exchange
- Oversized packets or max_allowed_packet mismatches abort mid-transfer
Diagnosis steps
Quantify and classify the aborts
mysql -e "SHOW GLOBAL STATUS LIKE 'Aborted%';" && tail -100 /var/log/mysql/error.log | grep "Aborted connection" | head -5Interpretation
Aborted_clients counts server-side closes (often timeout); Aborted_connects counts failed handshakes (credentials, limits) — different problems.
Compare idle settings with pool behavior
mysql -e "SHOW VARIABLES LIKE 'wait_timeout';" && grep -Rn "idleTimeout\|maxLifetime\|keepalive" app-config/Interpretation
A pool idle timeout longer than wait_timeout guarantees the server reaps live pooled connections; maxLifetime shorter than wait_timeout avoids it.
Check for middleboxes on the path
conntrack -S 2>/dev/null | head; ss -tan state established "( dport = :3306 )" | headInterpretation
Long-lived silent streams through NAT or firewalls get dropped between the endpoints; TCP keepalive on the path prevents it.
Root cause
The server closes connections that exceed wait_timeout without an application-visible reset until the next use. Pools holding such connections then send a query into a dead stream and receive a broken-pipe error, while the server logs an aborted connection note. The two sides disagree about who owns the lifecycle.
Fix
Set pool maxLifetime below wait_timeout so the client always retires first, enable pool keepalive pings under the idle threshold, align application deadlines with query timeouts so clients cancel server-side, and keep TCP keepalive sane on paths crossing stateful middleboxes.
Risk note
Raising wait_timeout to a huge value converts idle capacity into DoS surface and stale connections. Prefer retiring on the client side.
Verify the fix
After alignment, Aborted_clients growth stops under steady load, pool metrics show orderly retirement, and error log noise disappears.
Symptom walkthrough
Classify before debugging — the two counters mean different failures:
SHOW GLOBAL STATUS LIKE 'Aborted%';
Aborted_clients: 18402 <- server closed established connections (timeout class)
Aborted_connects: 3 <- handshakes failed (auth/limits class)
For the timeout class, the log note names the phase:
[Note] Aborted connection 1234 to db: 'shop' user: 'app'
host: '10.0.4.21' (Got timeout reading communication packets)
Reading timeout means the connection sat idle past wait_timeout. The pattern to check: does the growth rate track pool recycling cadence? When the pool idle timeout exceeds the server timeout, the server reaps connections the pool still trusts, and the next borrower pays with a broken pipe.
Fix in practice
Make the client always retire first:
# HikariCP example
spring:
datasource:
hikari:
max-lifetime: 480000 # 8 min < wait_timeout default
keepalive-time: 120000 # ping idle connections
minimum-idle: 4
# SQLAlchemy example
create_engine(url, pool_recycle=280, pool_pre_ping=True)
And align the cancel path: when an application deadline fires, cancel the server-side query too, rather than abandoning the socket mid-result — otherwise every timeout becomes one aborted connection note and one wasted server thread.