APScheduler programa funciones para ejecución inmediata, futura o recurrente. Funciona en aplicaciones pequeñas y servicios dedicados, pero reinicios, concurrencia y múltiples instancias requieren diseño.

Instalar y elegir el scheduler

Instala la versión elegida por el proyecto y fíjala en el archivo de dependencias:

python -m pip install APScheduler

Los ejemplos siguen la API 3.x, todavía habitual. APScheduler 4 reorganiza conceptos e imports, así que comprueba la versión principal instalada. BlockingScheduler ocupa el proceso principal y encaja en un servicio dedicado. BackgroundScheduler usa un hilo dentro de una aplicación síncrona duradera. En aplicaciones asíncronas, elige la integración documentada para tu versión. Inícialo una vez y ciérralo de forma controlada.

from zoneinfo import ZoneInfo
from apscheduler.schedulers.blocking import BlockingScheduler


def generar_informe() -> None:
    print("informe iniciado")


scheduler = BlockingScheduler(timezone=ZoneInfo("Europe/Madrid"))
scheduler.add_job(
    generar_informe,
    "cron",
    hour=7,
    minute=30,
    id="informe-diario",
    replace_existing=True,
    max_instances=1,
)
scheduler.start()

Usa date para una ejecución, interval para una cadencia fija y cron cuando el requisito sigue el calendario. Un intervalo de 24 horas mide tiempo transcurrido; “cada día a las 7:30” sigue el calendario local y puede cruzar cambios horarios.

from datetime import datetime, timedelta

scheduler.add_job(
    enviar_recordatorio,
    "date",
    run_date=datetime.now(tz=ZoneInfo("Europe/Madrid")) + timedelta(minutes=10),
    args=["factura-1842"],
)
scheduler.add_job(
    actualizar_panel,
    "interval",
    minutes=15,
    id="actualizar-panel",
    replace_existing=True,
)

Con un almacén persistente, pasa argumentos serializables y prefiere funciones del nivel del módulo. Las lambdas y funciones internas son difíciles de reconstruir tras reiniciar. No incluyas secretos en los argumentos, pues pueden aparecer serializados o en diagnósticos.

Decidir qué ocurre con los retrasos

El equipo puede suspenderse, el proceso puede detenerse o todos los hilos pueden estar ocupados. misfire_grace_time determina cuánto retraso se acepta. coalesce=True puede reunir varias ocurrencias perdidas en una ejecución después de recuperarse.

scheduler.add_job(
    importar_cotizaciones,
    "cron",
    hour=2,
    id="cotizaciones-diarias",
    replace_existing=True,
    max_instances=1,
    misfire_grace_time=900,
    coalesce=True,
)

Consolidar ejecuciones sirve al reconstruir una instantánea porque el resultado nuevo sustituye a los anteriores. Puede ser incorrecto en facturación, donde cada periodo debe procesarse. Representa periodos pendientes como datos duraderos y haz que la tarea los reclame. Un disparo correcto no demuestra que la operación de negocio terminó.

Diseñar una tarea idempotente

No supongas ejecución exactamente una vez. El proceso puede completar una petición externa y fallar antes de registrar el éxito. Dos instancias también pueden coincidir durante un despliegue. Asigna una clave estable a cada operación y haz que el destino rechace duplicados o actualice el mismo resultado sin daño.

def cerrar_dia_contable(dia: str) -> None:
    if libro_mayor.ya_cerrado(dia):
        return

    asientos = libro_mayor.calcular(dia)
    libro_mayor.guardar_cierre(
        dia, asientos, clave_operacion=f"cierre:{dia}"
    )

Una restricción única en la base de datos es más fuerte que consultar e insertar en dos pasos, pues existe una condición de carrera. Mantén transacciones cortas, define tiempos de espera y registra un estado terminal claro. Los reintentos deben seguir una política explícita según la excepción.

Persistencia y despliegues

El almacén predeterminado en memoria pierde la programación cuando termina el proceso. Es válido si el código la recrea al iniciar con IDs estables y replace_existing=True. Un almacén persistente ayuda con horarios creados dinámicamente, pero añade conexión, copias de seguridad, esquema y compatibilidad.

No supongas que compartir un job store coordina schedulers independientes. La persistencia no elige un líder. En un servidor web con varios workers, ejecuta un servicio dedicado o usa el scheduler de la plataforma para enviar trabajo a una cola. Evita mantener activas juntas las versiones antigua y nueva.

La comprobación de salud debe distinguir “proceso vivo” de “scheduler accede al almacén y los ejecutores avanzan”. Al cerrar, deja de aceptar trabajo y concede un periodo acorde con la duración de las tareas.

Observar la ejecución real

Registra ID, hora prevista, inicio real, duración, intento y resultado. Evita argumentos completos si contienen datos personales. Los eventos de APScheduler pueden alimentar contadores de éxito, fallo y ocurrencias perdidas. Alerta por retrasos sostenidos o duración anormal, no por cualquier fallo transitorio.

Para llamadas a otro servicio, usa un ID de correlación de extremo a extremo. Mantén acotadas las etiquetas de métricas: el nombre estable de tarea sirve, el ID de cliente produce cardinalidad excesiva. Prueba excepciones, un ejecutor retrasado y un reinicio con trabajo pendiente.

Elige el scheduler según el ciclo de vida y revisa la versión instalada porque las APIs cambian. Define siempre la zona y trata las horas locales ambiguas durante transiciones.

max_instances=1 evita solapamiento en un scheduler, no entre procesos. Iniciar uno por worker puede duplicar trabajo. Usa un proceso dedicado o sistema distribuido.

Las tareas deben ser idempotentes, observables, limitadas por tiempo y repetibles de forma consciente. Para workers distribuidos, compara Celery y tareas en segundo plano.

APScheduler programa trabajo, pero no es una cola distribuida completa. Una cola suele ser mejor cuando muchas máquinas ejecutan tareas, hacen falta workers especializados, reintentos duraderos o manejo de mensajes fallidos, o hay que absorber picos fuera del proceso web. Una arquitectura común deja que el scheduler publique un mensaje pequeño y que los workers hagan el trabajo pesado.

Para mantenimiento periódico o automatización interna moderada, APScheduler puede ser más sencillo. Documenta responsable, zona horaria, política de retraso, concurrencia, clave de idempotencia y recuperación. Estas decisiones importan más que la expresión cron.

Lista de revisión

Valida la programación en un proceso de pruebas con la misma zona y configuración del almacén. Ensaya una ejecución normal, un fallo intencional, una tarea más larga que su intervalo y un reinicio durante el trabajo. Comprueba que los logs identifican una operación sin exponer datos y que el operador puede repetirla con seguridad. Cambiar el número de réplicas no debe multiplicar ejecuciones.

Decide quién puede modificar horarios y cómo se auditan los cambios. Las expresiones cron aportadas por usuarios necesitan validación y límites razonables de frecuencia, pues un error puede crear carga excesiva. Prefiere configuración revisada con el código cuando los horarios sean política operativa.

La documentación oficial de APScheduler, consultada el 22 de julio de 2026, cubre tareas, schedules, jobs, almacenamiento y ejecutores. Fija la versión y usa su API correspondiente.