It's 2 AM and your app is throwing errors
Your monitoring fires. The logs are full of Error: MySQL server has gone away. Queries that worked fine all day are now failing. The database is up — you can connect manually — so what's going on?
MySQL dropped the connection before your app finished using it. The client sent a query, but the server was no longer listening. Not a crash. Just a disconnect.
Quick diagnosis: find the real cause first
Before touching any config, figure out why the connection died. Three culprits account for 95% of cases:
- Connection timeout — MySQL closed an idle connection before the app reused it
- Packet too large — a query or blob exceeded
max_allowed_packet - MySQL crashed or restarted — the server itself went away mid-session
Check MySQL error logs
# Ubuntu/Debian
tail -100 /var/log/mysql/error.log
# CentOS/RHEL
tail -100 /var/log/mysqld.log
# Or ask MySQL directly
SHOW VARIABLES LIKE 'log_error';
Check current timeout settings
SHOW VARIABLES LIKE '%timeout%';
SHOW VARIABLES LIKE 'max_allowed_packet';
Focus on wait_timeout and interactive_timeout. Stock MySQL defaults are 28800 seconds (8 hours). Many cloud providers set them much lower — AWS RDS often defaults to 600 seconds, some managed databases go as low as 60.
Check if the server restarted
-- Time since last restart
SHOW STATUS LIKE 'Uptime';
# On Linux, check system logs
journalctl -u mysql --since "1 hour ago"
Fix 1: Connection timeout (most common cause)
Your app opened a connection, left it idle, and MySQL killed it. The next query fails with MySQL server has gone away because the app still thinks the connection is alive.
Option A — Increase MySQL timeouts (server-side)
Edit /etc/mysql/mysql.conf.d/mysqld.cnf (or /etc/my.cnf):
[mysqld]
wait_timeout = 3600
interactive_timeout = 3600
Apply without restarting:
SET GLOBAL wait_timeout = 3600;
SET GLOBAL interactive_timeout = 3600;
Option B — Fix connection pooling in your app (the right fix)
Bumping the timeout just delays the problem. Sooner or later, a connection will still go stale. The durable fix is teaching your app to detect a dead connection and reconnect automatically.
Python (SQLAlchemy):
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True, # Test connection before using it
pool_recycle=1800, # Recycle connections every 30 minutes
pool_timeout=30,
pool_size=10
)
Node.js (mysql2):
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
database: 'mydb',
waitForConnections: true,
connectionLimit: 10,
enableKeepAlive: true, // Send keepalive packets
keepAliveInitialDelay: 10000
});
// Always use pool.execute(), not a single persistent connection
const [rows] = await pool.execute('SELECT 1');
PHP (PDO):
// Avoid PDO::ATTR_PERSISTENT — it reuses connections across requests without checking if they're alive
// Catch the error and reconnect instead:
try {
$stmt = $pdo->query($sql);
} catch (PDOException $e) {
if (str_contains($e->getMessage(), 'server has gone away')) {
$pdo = new PDO($dsn, $user, $pass, $options); // reconnect
$stmt = $pdo->query($sql);
} else {
throw $e;
}
}
Fix 2: max_allowed_packet too small
Does the error happen when inserting large text, images, or JSON blobs — but not on idle connections? Packet size is the culprit, not timeout.
-- Check current limit
SHOW VARIABLES LIKE 'max_allowed_packet';
-- Default is 4MB (4194304) in MySQL 5.7, 64MB in MySQL 8.0
Increase it in my.cnf:
[mysqld]
max_allowed_packet = 64M
Or apply live (takes effect for new connections only):
SET GLOBAL max_allowed_packet = 67108864;
Restart MySQL to make the change survive reboots:
sudo systemctl restart mysql
Fix 3: MySQL crashed or was OOM-killed
Low Uptime or a restart entry in the error log means the server itself went down — typically due to memory pressure. The OS killed MySQL before it could close connections cleanly.
# Check if the OOM killer hit MySQL
dmesg | grep -i 'killed process'
grep -i 'oom' /var/log/syslog | tail -20
If MySQL was OOM-killed, cap innodb_buffer_pool_size at 70–75% of available RAM. On a 4 GB server, that's about 2–3 GB:
[mysqld]
# For a server with 4GB RAM:
innodb_buffer_pool_size = 2G
Verify the fix
After applying changes, confirm the new values are actually in effect:
-- Confirm new timeout values
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'max_allowed_packet';
-- Watch connection errors in real time
SHOW STATUS LIKE 'Aborted_clients';
SHOW STATUS LIKE 'Aborted_connects';
A rising Aborted_clients count means clients are dropping connections without closing them properly — your connection pool config needs attention. Aborted_connects climbing instead points to authentication or network issues.
Want to stress-test your app-side reconnect logic? Drop wait_timeout to 10 seconds, wait 15, then fire a query through your app:
SET GLOBAL wait_timeout = 10;
-- Wait 15 seconds, then run a query through your app
-- No error = pool_pre_ping / reconnect logic is working
What to take away from this
- Never hold idle DB connections open — always use a connection pool with
pre_pingor keepalive. Even if MySQL's timeout is low, the pool will handle reconnects transparently. - Set max_allowed_packet to 64M before you ship — if your app handles file uploads or large JSON payloads, the 4 MB default will catch you off guard in production.
- Watch Aborted_clients — a steady climb is an early warning that something is holding connections open or leaking them.
- Managed databases have aggressive timeouts — RDS, PlanetScale, and Cloud SQL often default to 60–600 seconds. Set
pool_recycleto half that value to stay safe.

