APScheduler queues Python functions for immediate, future, or recurring execution. It fits small applications and dedicated services, but restarts, concurrency, and multiple instances require deliberate design.
Choose a scheduler and trigger
Install the release selected by the project and pin it in the dependency file:
python -m pip install APScheduler
These examples use the widely deployed 3.x API. APScheduler 4 reorganizes concepts and imports, so check the installed major version before copying code. BlockingScheduler owns the foreground process and suits a dedicated daemon. BackgroundScheduler uses a thread inside a long-lived synchronous application. Async applications should use the integration documented for their version. Start the scheduler once during application startup and shut it down gracefully.
from zoneinfo import ZoneInfo
from apscheduler.schedulers.blocking import BlockingScheduler
def generate_report() -> None:
print("report started")
scheduler = BlockingScheduler(timezone=ZoneInfo("America/New_York"))
scheduler.add_job(
generate_report,
"cron",
hour=7,
minute=30,
id="daily-report",
replace_existing=True,
max_instances=1,
)
scheduler.start()
Choose a scheduler that matches the application lifecycle and check the installed major version because APIs evolve. Always state the timezone and handle ambiguous local times during transitions.
Use a date trigger for one execution, an interval for a fixed cadence, and cron when the requirement is expressed as calendar fields. An interval of 24 hours counts elapsed time; “every day at 07:30” follows a local calendar and can cross daylight-saving changes.
from datetime import datetime, timedelta
scheduler.add_job(
send_reminder,
"date",
run_date=datetime.now(tz=ZoneInfo("America/New_York")) + timedelta(minutes=10),
args=["invoice-1842"],
)
scheduler.add_job(
refresh_dashboard,
"interval",
minutes=15,
id="refresh-dashboard",
replace_existing=True,
)
With a persistent store, pass serializable arguments and prefer a top-level function. Lambdas and nested functions are difficult to reconstruct after restart. Keep secrets out of arguments because serialized values may appear in storage or diagnostics.
Decide how delays behave
A machine may be suspended, the process may stop, or all executor threads may be busy. misfire_grace_time says how late a run may start. coalesce=True can combine several missed occurrences into one execution after recovery.
scheduler.add_job(
import_daily_rates,
"cron",
hour=2,
id="daily-rates",
replace_existing=True,
max_instances=1,
misfire_grace_time=900,
coalesce=True,
)
Coalescing works for rebuilding a current snapshot because the newest result replaces older ones. It may be wrong for billing, where every period must be processed. Model those periods as durable domain records and let the job claim pending work. A successful scheduler event is not proof that the business operation completed.
Make jobs idempotent
Do not assume exactly-once execution. A process can finish an external request and crash before recording success, or two scheduler instances can overlap during deployment. Give each logical operation a stable key and make the destination reject or harmlessly update duplicates.
def close_accounting_day(day: str) -> None:
if ledger.already_closed(day):
return
rows = ledger.calculate(day)
ledger.save_close(day, rows, operation_key=f"close:{day}")
A database unique constraint on operation_key is stronger than a check followed by an insert, because the latter has a race. Keep transactions short, set network timeouts, and record a clear terminal status. Retry according to an explicit exception policy, never through an unbounded loop hidden inside the function.
Persistence and deployments
The default memory store loses schedules when the process exits. That is acceptable if code recreates every schedule at startup with stable IDs and replace_existing=True. A persistent store helps with schedules created dynamically by users, but adds connection, schema, backup, and version-compatibility responsibilities.
Do not assume that sharing one store turns independent schedulers into a coordinated cluster. Persistence does not elect a leader. In a multi-worker web deployment, run one dedicated scheduler service or use a platform scheduler that submits work to a queue. Avoid keeping the old and new scheduler active together longer than intended.
Health checks should distinguish “process is alive” from “scheduler can access its store and executors are progressing.” On shutdown, stop accepting work and allow a grace period suited to job duration.
Observe actual execution
Log the job ID, scheduled time, actual start, duration, attempt, and outcome. Avoid full arguments when they contain personal data. APScheduler events can feed counters for completed, failed, and missed jobs. Alert on sustained misses or unusual duration rather than every transient failure.
For calls to another service, carry an end-to-end correlation ID. Keep metric labels bounded: a stable job name is useful, while customer IDs create excessive cardinality. Test failure behavior by raising an exception, delaying an executor, and restarting with a pending run.
max_instances=1 prevents overlap in one scheduler, not across independent processes. Starting a scheduler in every web worker can duplicate work. Prefer a dedicated process or distributed system.
Jobs should be idempotent, observable, time-bounded, and safe to retry intentionally. Decide how late and missed runs behave. For distributed workers, compare Celery background tasks.
APScheduler is a scheduler, not a complete distributed queue. A queue is usually better when jobs run on many machines, need specialized workers, durable retries, or dead-letter handling, or must absorb bursts independently of the web process. A common architecture uses one scheduler to enqueue a small message and lets workers perform the expensive operation.
For periodic cleanup or modest internal automation, APScheduler can remain simpler. Document ownership, timezone, missed-run policy, concurrency limit, idempotency key, and recovery procedure for every important job. Those decisions matter more than the trigger expression.
A practical review checklist
Before releasing a schedule, verify it in a staging process with the same timezone and store configuration. Exercise a normal run, an intentional failure, a run longer than its interval, and a restart during execution. Confirm that logs identify one logical operation without leaking payloads and that an operator can rerun it safely. Check that changing the deployment replica count does not change the number of scheduled executions.
Also decide who can modify schedules and how changes are audited. User-provided cron expressions need validation and reasonable frequency limits, otherwise a typo can create excessive load. Prefer configuration reviewed with the application when schedules are operational policy; reserve dynamic storage for genuine user-managed scheduling.
The official APScheduler user guide, accessed July 22, 2026, covers tasks, schedules, jobs, stores, and executors. Pin your version and verify its matching API before reusing examples.