Cómo programar tareas en Python sin Celery y alternativas
Todo desarrollador backend probablemente se ha enfrentado a esto. Estás construyendo un servicio web con FastAPI o Flask, el proyecto aún es pequeño, ejecutándose en un solo servidor. Entonces el product manager pregunta: "¿Oye, enviemos a los clientes un resumen cada mañana a las 9 AM y reiniciemos los carritos expirados cada hora?"
El primer instinto es agregar Celery con Celery Beat y Redis. Pero levantar un broker, configurar workers y monitorear daemons solo para dos tareas en segundo plano se siente como exagerado. La segunda opción es cron del sistema, pero eso dispersa la lógica entre el código de la aplicación y la configuración del servidor. Quieres el scheduler embebido en tu código, capaz de manejar asyncio, y escalar a través de múltiples nodos cuando sea necesario.
Aquí es donde la librería APScheduler (Advanced Python Scheduler) resulta útil.
Qué puede hacer la librería
APScheduler existe desde hace un tiempo, y el proyecto está actualmente en transición a la versión 4.0. La cuarta rama aún está en estado pre-release, pero ha reestructurado significativamente la arquitectura de la librería.
Anteriormente era principalmente un scheduler local para un solo proceso, pero ahora la herramienta ha evolucionado a un sistema completo de cola distribuida y programación. El caso de uso más simple aún se ejecuta en solo tres líneas de código.
El concepto central: declaras funciones regulares de Python o corrutinas y adjuntas un trigger con las condiciones de ejecución necesarias.
from apscheduler.schedulers.asyncio import AsyncIOScheduler
scheduler = AsyncIOScheduler()
async def send_digest():
print("Отправляем утренний дайджест...")
# Запуск каждый будний день в 9 утра
scheduler.add_job(send_digest, 'cron', day_of_week='mon-fri', hour=9, minute=0)
scheduler.start()
La librería maneja el seguimiento del tiempo, calcula los offsets e invoca las funciones en el momento correcto.
Opciones de programación
Hay cuatro tipos de triggers integrados disponibles:
- Trigger cron. Sintaxis familiar estilo Linux. Puedes configurar flexiblemente días de la semana, meses, horas específicas y minutos.
- Intervalos. Ejecutar cada N segundos, minutos u horas. Útil para polling regular de APIs externas.
- Trigger calendario. Necesario cuando el intervalo depende de la duración de un mes o año. Por ejemplo, ejecutar una tarea estrictamente el primer día de cada mes al mediodía.
- Ejecución única. Se dispara exactamente una vez en una fecha y hora futura especificada.
Si las condiciones integradas no son suficientes, los triggers pueden combinarse usando reglas compuestas o puedes escribir tu propia clase con lógica personalizada.
Protección contra fallos comunes
En la práctica, las tareas en segundo plano regularmente encuentran sobrecargas y retrasos. APScheduler incluye varios mecanismos útiles que a menudo se pasan por alto en soluciones personalizadas construidas sobre while True y sleep.
Primero, limitar ejecuciones concurrentes. Si configuraste una exportación de reporte pesado cada cinco minutos, pero la ejecución anterior se atascó por siete minutos, el scheduler no lançará una segunda instancia en paralelo y no sobrecargará la base de datos.
Segundo, el parámetro jitter. Agrega un delay aleatorio al tiempo de inicio de la tarea. Imagina que tienes cien workers y todos necesitan refrescar el caché exactamente a las 00:00. Sin jitter, la base de datos recibirá un pico de carga instantáneo. Con un offset aleatorio de un par de segundos, la carga se distribuye uniformemente.
Tercero, manejo de grace time para missed triggers. Si el servidor se congeló bajo carga o estaba reiniciando, el scheduler verifica qué tan tarde está la tarea. Si el delay cabe dentro del límite aceptable, la tarea se ejecutará; si no, se omitirá sin acumular en la cola.
Almacenamiento y modo distribuido
Para scripts simples, las tareas pueden mantenerse en memoria. Pero si el servicio se reinicia, el schedule se reiniciará. Para evitar esto, la librería soporta almacenamiento persistente:
- PostgreSQL
- MySQL
- SQLite
- MongoDB
En la versión cuatro, el scheduler aprendió a trabajar en un cluster distribuido. Múltiples instancias de la aplicación se conectan a una base de datos compartida y broker de eventos (se soportan Redis, PostgreSQL LISTEN/NOTIFY y MQTT).
Esto logra escalamiento horizontal: un nodo cae, los demás recogen las tareas de la cola.
# Пример концепции работы с постоянным хранилищем
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
jobstores = {
'default': SQLAlchemyJobStore(url='postgresql+asyncpg://user:pass@localhost/mydb')
}
scheduler = AsyncIOScheduler(jobstores=jobstores)
Código síncrono y asíncrono
La librería se integra bien con el stack moderno de Python. Están disponibles diferentes implementaciones de scheduler:
AsyncIOSchedulere integración con Trio para backends async modernos como FastAPI, Litestar o Aiohttp.BackgroundScheduleryBlockingSchedulerpara scripts síncronos, Django y Flask.
No necesitas envolver funciones async en workarounds síncronos con asyncio.run() — el scheduler nativamente hace await de corrutinas dentro del event loop principal.
Dónde resulta útil
Yo generalmente recurro a APScheduler en tres situaciones típicas:
- Microservicios pequeños y bots. Cuando desplegar Celery o RQ sería excesivo y se necesitan tareas periódicas directamente dentro del proceso.
- Acciones de usuario diferidas. Por ejemplo, enviar un email pidiendo calificar un pedido exactamente 24 horas después de la entrega.
- Limpieza de datos temporales y sincronización periódica de datos de referencia de sistemas externos.
¿Vale la pena usarlo en un proyecto?
Si estás escribiendo en Python y necesitas ejecución predecible de tareas basadas en tiempo, APScheduler es una de las opciones más maduras.
La única nuance actualmente: el período de transición entre las versiones 3.x y 4.0. La tercera rama ha sido probada en producción durante años, es максимально estable, pero tiene limitaciones en operación distribuida. La versión 4.0 trae arquitectura moderna y escalamiento, pero el autor honestamente advierte sobre posibles cambios de API que rompan compatibilidad antes del lanzamiento final.
Para producción actual, es más seguro quedarse con la rama estable 3.x, y la versión 4.0 vale la pena probarla en pet projects o nuevos microservicios mientras vigilas los próximos cambios.
Proyectos relacionados