El problema que nadie ve#
Hace dos meses desplegué una API FastAPI en producción y el servidor se comportaba extraño. CPU estable en 40-50% sin motivo aparente. Pensé que era un leak de memoria, que eran los logs, que era la base de datos. Era --reload.
Resulta que copiar-pegar el comando de desarrollo directamente al contenedor Docker es más común de lo que debería ser. Y sí, uvicorn con --reload funciona. El servidor responde. Los requests van rápido. Pero hay un coste que no ves hasta que tienes 10K requests diarios.
Qué es exactamente el file-watcher#
El flag --reload de uvicorn inicia un proceso adicional que monitoriza todos los ficheros Python de tu proyecto. No solo tu código. Todos.
Cuando activas --reload, uvicorn:
- Inicia un process manager (watchfiles por defecto)
- Cada X segundos (por defecto 0.4s) escanea todos los .py del directorio
- Calcula checksums o hashes de cada fichero
- Si detecta cambios, reinicia el worker completo
- Mientras tanto, sigue escaneando en cada ciclo, incluso sin cambios
Este escaneo no es gratis. En un proyecto mediano con 200 ficheros Python distribuidos en vendor, librerías y módulos propios, cada ciclo de watchfiles toca disco y CPU.
Cómo consume CPU en cada request#
La parte criminal es que el file-watcher no se pausa durante los requests. Mientras tu API está procesando una solicitud:
- El monitor sigue escaneando ficheros en background
- Compite por I/O de disco con tu aplicación
- En contenedores, si no tienes límites de recursos, puede llegar a consumir más CPU que la propia lógica del request
Medí esto en mi servidor. Con un request simple a una endpoint que tarda 50ms:
Sin --reload:
CPU: 2-3% por request
I/O wait: <1%Con --reload:
CPU: 8-12% por request
I/O wait: 3-5%No parece mucho en un request aislado. Pero con 100 requests concurrentes, ese overhead se multiplica.
Cómo detectarlo en tu servidor#
1. Mira los procesos en ejecución#
ps aux | grep uvicornSi ves dos procesos uvicorn (o un uvicorn + watchfiles), tienes --reload activo.
2. Revisa tu comando de inicio#
# MAL - esto es lo que probablemente tienes
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
# BIEN - así debe estar en producción
uvicorn main:app --host 0.0.0.0 --port 80003. Monitoriza CPU durante un test de carga#
# Terminal 1: corre tu servidor
docker run -it tu-contenedor
# Terminal 2: genera carga
ab -n 1000 -c 10 http://localhost:8000/endpointObserva top o docker stats. Si ves picos inexplicables, sospecha de --reload.
4. Revisa los logs de uvicorn#
Con --reload activo, verás mensajes como:
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Will watch for changes in these directories: ...Si ves “Will watch for changes”, tienes un problema.
La solución (es obvia, pero importante)#
En Docker, asegúrate de que tu Dockerfile NO usa --reload:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
# Aquí no va --reload
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]Para desarrollo local, usa --reload sin culpa:
uvicorn main:app --reloadLo que aprendí#
El file-watcher de uvicorn es excelente para desarrollo. Es transparente, funciona bien y acelera el ciclo. Pero en producción es como dejar el ordenador escaneando antivirus continuamente.
Revisé mis otros contenedores después de esto. Encontré tres más con --reload activo. Después de quitarlo, el consumo de CPU bajó entre 30-45%.
Es uno de esos bugs que no es un bug. Tu aplicación funciona. Los requests se procesan. Pero tu servidor está haciendo trabajo invisible que no necesita hacer.
Próxima vez antes de desplegar a producción, grep por --reload. Te ahorrará una sesión de troubleshooting.