Cómo solucionar Flask MySQL OperationalError: (2006, 'MySQL server has gone away')
Tu aplicación Flask funciona perfecto toda la mañana. Después de un rato sin tráfico —el almuerzo, la noche, un fin de semana— la primera request que llega revienta con esto:
sqlalchemy.exc.OperationalError: (mysql.connector.errors.OperationalError) 2006 (HY000):
MySQL server has gone away
O, si usás mysqlclient directamente:
_mysql_exceptions.OperationalError: (2006, 'MySQL server has gone away')
Es uno de los errores más reportados en proyectos Flask + SQLAlchemy, y también uno de los que peor se resuelven en foros, porque mucha gente prueba "cambiar el pool_recycle a un número random" sin entender qué está pasando realmente. Vamos a diagnosticarlo bien.
Qué significa el error 2006 (y por qué el nombre confunde)
El mensaje "MySQL server has gone away" suena a que el servidor se cayó. Casi nunca es así. Lo que en realidad describe es que tu aplicación intentó usar una conexión que MySQL ya había cerrado por su lado, sin que tu aplicación se enterara.
Esto pasa porque las conexiones TCP entre tu app y MySQL no son gratuitas de mantener: el servidor las cierra automáticamente después de un período de inactividad, definido por la variable wait_timeout (28800 segundos = 8 horas por defecto en muchas instalaciones). <cite index="10-1">El error ocurre típicamente si el servidor estuvo inactivo por unas 8 horas</cite>, que es justo el patrón clásico: funciona bien todo el día con tráfico constante, y la primera request después de la madrugada o el fin de semana falla, porque la conexión que estaba "guardada" en el pool ya fue cerrada por el servidor mientras nadie la usaba.
Tu aplicación Python, sin embargo, no tiene forma de saber esto de antemano: SQLAlchemy (o el driver que uses) toma una conexión del pool, asume que sigue viva, intenta usarla, y recién ahí MySQL le informa que la conexión ya no existe.
Causa 1 (la más común): conexiones inactivas que superan el wait_timeout
Este es el escenario del cron job, del panel de administración que nadie usa a la madrugada, o de la API interna con tráfico intermitente. La solución no es "reintentar" —eso solo pospone el problema— sino configurar el pool de conexiones para que nunca entregue una conexión potencialmente vencida.
Solución con pool_recycle (la más usada):
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://usuario:contraseña@localhost/mi_basedatos'
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
'pool_recycle': 3600, # recicla conexiones cada hora, antes de que MySQL las cierre
'pool_pre_ping': True, # verifica la conexión antes de usarla
}
db = SQLAlchemy(app)
pool_recycle le dice a SQLAlchemy que descarte cualquier conexión más vieja que el valor indicado (en segundos), sin esperar a que MySQL la rechace. El valor debe ser menor al wait_timeout configurado en tu servidor MySQL. Si tu wait_timeout es el default de 8 horas (28800 segundos), un pool_recycle de 3600 (una hora) es un margen razonable y conservador.
Solución con pool_pre_ping (la más robusta, y la recomendada como primera línea de defensa):
<cite index="8-1">pool_pre_ping prueba la conexión antes de usarla</cite>, agregando un chequeo ligero (SELECT 1) antes de entregarte la conexión del pool. Si esa conexión ya fue cerrada por el servidor, SQLAlchemy la descarta automáticamente y abre una nueva, de forma transparente para tu código. La única desventaja es una latencia mínima extra por request, generalmente insignificante frente al costo de que tu aplicación falle en producción.
Recomendación práctica: usá ambas configuraciones juntas. pool_pre_ping te protege contra el error en tiempo real; pool_recycle reduce la frecuencia con la que ese chequeo extra tiene que descartar conexiones muertas.
Causa 2: verificá primero cuál es tu wait_timeout real
Antes de elegir un valor para pool_recycle, confirmá el timeout real de tu servidor en vez de adivinar:
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';
Si tu proveedor de hosting (muchos planes compartidos o servicios gestionados) configura un wait_timeout bajo —algunos lo bajan a 60 o 120 segundos para ahorrar recursos—, un pool_recycle de 3600 no te va a servir de nada: seguirás recibiendo el error porque el servidor cierra la conexión mucho antes de que SQLAlchemy la recicle. En ese caso, ajustá pool_recycle a un valor bien por debajo del wait_timeout real de tu instancia, no del valor por defecto que asumís que tiene.
Causa 3: sin usar SQLAlchemy — reconexión manual
Si estás usando mysql.connector o mysqlclient directamente, sin el pool de SQLAlchemy, el patrón de solución es distinto: capturar el error específico y reconectar.
import mysql.connector
from mysql.connector import OperationalError
def obtener_conexion():
return mysql.connector.connect(
host='localhost',
user='usuario',
password='contraseña',
database='mi_basedatos',
connection_timeout=60,
)
def ejecutar_query(query, params=None, intentos=3):
conn = obtener_conexion()
try:
cursor = conn.cursor()
cursor.execute(query, params)
return cursor.fetchall()
except OperationalError as e:
if e.errno == 2006 and intentos > 0:
conn.reconnect(attempts=3, delay=2)
cursor = conn.cursor()
cursor.execute(query, params)
return cursor.fetchall()
raise
finally:
conn.close()
<cite index="8-1">El método reconnect con reintentos y un delay entre cada uno es la forma recomendada de manejar este caso sin un pool de conexiones administrado por un ORM</cite>. Ojo con un detalle importante: si estabas en medio de una transacción cuando la conexión murió, esa transacción se perdió por completo. Reconectar no recupera el estado anterior; tu lógica de negocio debe estar preparada para reintentar la operación completa desde cero, no solo la query individual.
Causa 4: paquetes demasiado grandes (max_allowed_packet)
Menos común, pero confunde mucho porque el mensaje de error es idéntico al del timeout. Si el error 2006 aparece de forma consistente al insertar filas grandes, subir archivos como BLOB, o restaurar un dump de base de datos —y no después de un período de inactividad— la causa probable es que tu query supera el límite de max_allowed_packet configurado en el servidor.
Verificá el límite actual:
SHOW VARIABLES LIKE 'max_allowed_packet';
Si estás restaurando un backup y falla a mitad de camino:
# Confirmá que el dump no esté cortado
wc -l backup.sql
# Restaurá especificando un límite mayor
mysql --max_allowed_packet=64M -u usuario -p mi_basedatos < backup.sql
Para aumentar el límite de forma permanente en el servidor (requiere acceso de administrador), agregá en el archivo de configuración my.cnf:
[mysqld]
max_allowed_packet=64M
Y reiniciá el servicio de MySQL para que el cambio tome efecto.
Causa 5: conexiones agotadas por no cerrarlas correctamente
Si además del error 2006 ves ocasionalmente errores de tipo max_user_connections exceeded o tu aplicación se vuelve lenta antes de fallar, es una señal de que tu código no está devolviendo las conexiones al pool correctamente —por ejemplo, abriendo una nueva conexión por cada request sin cerrarla, o sin usar el patrón de contexto (with) del ORM. Con Flask-SQLAlchemy, esto casi siempre se evita solo con dejar que la extensión gestione el ciclo de vida de las sesiones (db.session), en vez de crear conexiones manuales dentro de las vistas.
Tabla de diagnóstico rápido
| Patrón del error | Causa probable | Solución |
|---|---|---|
| Falla solo tras horas de inactividad (madrugada, cron diario) | Conexión cerrada por wait_timeout |
pool_recycle + pool_pre_ping |
| Falla constantemente con inserts grandes o restaurando dumps | max_allowed_packet insuficiente |
Aumentar el límite en servidor y cliente |
| Falla intermitente sin patrón de horario, junto con errores de conexiones agotadas | Conexiones no liberadas correctamente | Revisar manejo de sesiones/pool en el código |
| Falla en cron jobs o workers de larga duración (Celery, scripts standalone) | Conexión abierta al inicio del proceso y reutilizada horas después | Reconexión explícita o pool_pre_ping |
El contraargumento: ¿pool_pre_ping tiene costo real?
Vale la pena decirlo con honestidad: agregar un SELECT 1 antes de cada query que usa el pool sí agrega latencia, aunque mínima (típicamente menos de un milisegundo en conexiones locales o de baja latencia). En aplicaciones con muchísimo tráfico y sensibilidad extrema a la latencia, algunos equipos prefieren depender solo de pool_recycle bien calibrado y aceptar un margen pequeño de errores 2006 ocasionales, manejados con reintentos a nivel de aplicación. Para la gran mayoría de proyectos —especialmente APIs internas, dashboards administrativos y aplicaciones con tráfico intermitente— el costo de pool_pre_ping es insignificante comparado con el costo de un 500 inesperado en producción, así que la recomendación por defecto sigue siendo usarlo.
Resumen práctico según tu perfil
- Si sos vibe coder o estás prototipando: agregá
pool_pre_ping: Truea tu configuración de SQLAlchemy y olvidate del problema; es la solución de menor esfuerzo y mayor cobertura. - Si mantenés una app Flask en producción con tráfico intermitente (cron jobs, APIs internas, paneles poco usados): combiná
pool_recycle(calibrado según elwait_timeoutreal de tu servidor) conpool_pre_ping, y agregá manejo de reconexión en tus workers de larga duración (Celery, scripts standalone) que abren la conexión una sola vez al iniciar. - Si administrás el servidor MySQL además de la app: verificá
wait_timeoutymax_allowed_packetconSHOW VARIABLESantes de tocar el código de la aplicación; muchas veces la solución más simple es alinear la configuración del servidor con la del pool de conexiones, no al revés.
Conclusión
"MySQL server has gone away" no significa que tu servidor se cayó: significa que tu aplicación intentó reutilizar una conexión que el servidor ya había cerrado por inactividad, o que el paquete enviado excedía el límite configurado. Con pool_pre_ping y un pool_recycle bien calibrado, este error prácticamente desaparece de cualquier aplicación Flask en producción.
¿Te apareció con un patrón distinto a los de esta guía? Contame en los comentarios el traceback completo y lo sumamos.


Comentarios