Troubleshooting
PostgreSQL & databases

Fix "MySQL server has gone away" (2006) in Django

Fix Django OperationalError 2006 MySQL server has gone away and 2013 lost connection: align CONN_MAX_AGE with wait_timeout, enable CONN_HEALTH_CHECKS, close connections in long-running scripts, and raise max_allowed_packet.

Problem statement

Requests or background jobs fail now and then with:

django.db.utils.OperationalError: (2006, 'MySQL server has gone away')

or (2013, 'Lost connection to MySQL server during query'). It often hits the first request after a quiet night, Celery tasks that wake up after an idle period, or management commands that run for hours. Django tried to use a connection that MySQL had already closed. MySQL closes idle connections after wait_timeout seconds, and also drops connections that send packets larger than max_allowed_packet, or when the server restarts.

How a connection goes staleDjango opens connkept for reuseIdle periodpast wait_timeoutMySQL closes itclient not toldNext query2006 gone awayA persistent connection outlived the server's idle timeout and was reused without a check.
  1. Check the server timeout: SHOW VARIABLES LIKE 'wait_timeout';.
  2. Keep CONN_MAX_AGE well below wait_timeout, and set CONN_HEALTH_CHECKS = True (Django 4.1+).
  3. In Celery tasks and long scripts, call django.db.close_old_connections() before database work.
  4. For errors on large inserts or BLOBs, raise max_allowed_packet on the server.
  5. Check the MySQL error log for crashes or OOM restarts if errors cluster in time.

Step-by-step solution

1. Compare the timeouts

SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';
SHOW VARIABLES LIKE 'max_allowed_packet';

Defaults are 28,800 seconds (8 hours) for wait_timeout, but managed services and hosting providers often lower it to 60-600 seconds. Then check Django:

DATABASES["default"]["CONN_MAX_AGE"]   # seconds a connection may be reused

If CONN_MAX_AGE is None (unlimited) or larger than wait_timeout, a connection can outlive the server's patience.

2. Configure persistent connections safely

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": env("DB_NAME"),
        "USER": env("DB_USER"),
        "PASSWORD": env("DB_PASSWORD"),
        "HOST": env("DB_HOST"),
        "CONN_MAX_AGE": 60,
        "CONN_HEALTH_CHECKS": True,
        "OPTIONS": {"charset": "utf8mb4"},
    }
}

CONN_HEALTH_CHECKS makes Django test a reused connection at the start of each request and reconnect if it is dead. It removes most 2006 errors in web requests at the cost of a cheap ping per request.

connection lifetime vs server idle timeout (seconds)MySQL wait_timeout (managed)600CONN_MAX_AGE = Noneunlimited: goes staleCONN_MAX_AGE = 6060: always fresh0300600Keep Django's reuse window well inside the server's idle timeoutdjango-deployment.com
Illustrative values: check your provider's wait_timeout, which is often far below 8 hours.

3. Fix long-running processes

Django closes or checks connections at request boundaries. Celery workers, management commands, and custom daemons have no requests, so connections can sit idle between tasks:

from django.db import close_old_connections

@shared_task
def sync_inventory():
    close_old_connections()
    ...

Celery's Django integration calls close_old_connections around tasks when the app is set up through django.setup() via the standard celery.py, but long tasks that pause (waiting on an API for minutes) can still lose the connection mid-task. Call close_old_connections() after long waits, or restructure so database work happens in short bursts.

For a management command that loops forever:

while True:
    close_old_connections()
    process_batch()
    time.sleep(300)

4. Large packets

A 2006 or 2013 right after inserting a big JSON field, file content, or a huge bulk_create usually means the packet exceeded max_allowed_packet (64 MB default on MySQL 8, less on older versions and some hosts):

# my.cnf
[mysqld]
max_allowed_packet = 128M

Or reduce the payload: smaller bulk_create(batch_size=500) batches, and store files in object storage rather than BLOB columns.

5. Server restarts and crashes

If errors cluster at specific times across all processes, check whether MySQL restarted:

SHOW GLOBAL STATUS LIKE 'Uptime';

and the MySQL error log for crashes or OOM kills. Managed services also restart during maintenance windows; health checks plus retries in task code handle these.

when the error appears tells you whyWhen it happensCause and fixfirst request after idle timeCONN_MAX_AGE > wait_timeout;lower it, health checks onCelery task after a quiet periodstale worker connection;close_old_connections()on large inserts or uploadsmax_allowed_packetexceeded; raise it or batcheverything at once, then fineserver restart or crash;check Uptime and error logTiming points to the cause faster than the error text doesdjango-deployment.com
Group errors by time and process before changing settings.

Verification check: leave the app idle for longer than wait_timeout, then load a page and run a Celery task; neither should fail. Grep logs for 2006 and 2013 over the next week.

Explanation

MySQL does not notify clients when it closes an idle connection; the client only learns about it when it next writes to the socket. Django's persistent connections (CONN_MAX_AGE > 0) save the cost of connecting on each request, but create exactly this window. Django closes connections that exceed their age at the start and end of requests and, with health checks, pings reused connections first. Code that runs outside the request cycle bypasses both mechanisms unless it calls close_old_connections itself.

Edge cases / notes

  • ProxySQL or a load balancer between Django and MySQL may have its own idle timeout, shorter than MySQL's.
  • Transactions cannot be retried transparently after a lost connection; wrap critical work in retry logic at the task level.
  • CONN_MAX_AGE = 0 (the default) opens a new connection per request and avoids staleness entirely at a small latency cost.
  • MariaDB reports the same errors and uses the same variables.

Next steps

FAQ

What does MySQL server has gone away mean?

Django used a connection the server had already closed, or the server dropped it mid-query.

Should I raise wait_timeout?

You can, but lowering CONN_MAX_AGE and enabling health checks fixes the cause without holding idle connections longer.

Do Celery tasks need close_old_connections?

Long or idle-prone tasks benefit from calling it before database work, especially after long waits.

Is error 2013 the same problem?

Closely related: the connection was lost during a query, often due to timeouts, packet size, or a restart.

Keep going