APScheduler
Advanced Python Scheduler — schedule Python functions to run at specified times or intervals. Current stable version is 3.11.2. A completely rewritten 4.x is in pre-release with a different API (apscheduler.Scheduler instead of BackgroundScheduler, etc.) — 4.x is NOT installed by pip install APScheduler. Two concurrent stable lines: use 3.x for production. The 3.x vs 4.x API confusion is the #1 footgun.
Warnings
- breaking APScheduler 4.x (a complete rewrite) is in pre-release and has a completely different API. pip install APScheduler installs 3.x — NOT 4.x. 4.x docs and tutorials showing from apscheduler import Scheduler will raise ImportError on 3.x installs.
- gotcha scheduler.start() must be called explicitly. Creating a scheduler and adding jobs without calling start() does nothing — jobs never run.
- gotcha Jobs are lost on restart unless a persistent job store (SQLAlchemy, Redis, MongoDB) is configured. The default MemoryJobStore holds jobs only in RAM.
- gotcha Running multiple processes each with their own scheduler (e.g. gunicorn with multiple workers) causes duplicate job execution. APScheduler 3.x has no inter-process coordination.
- gotcha BlockingScheduler blocks the main thread — use it only when the scheduler IS the application. BackgroundScheduler runs in a daemon thread and is suitable for running alongside a web app.
- gotcha All times are naive (timezone-unaware) unless you configure a timezone. Scheduled jobs may run at wrong times during DST transitions.
Install
-
pip install APScheduler -
pip install APScheduler[sqlalchemy] -
pip install APScheduler[redis]
Imports
- BackgroundScheduler
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.interval import IntervalTrigger # 3.x API — the current stable API scheduler = BackgroundScheduler() # Add jobs scheduler.add_job(my_function, 'interval', seconds=30) scheduler.add_job(my_function, CronTrigger(hour=9, minute=0)) # 9am daily scheduler.add_job(my_function, 'cron', hour=9, minute=0) # same # Must call start() explicitly scheduler.start() # Shutdown cleanly import atexit atexit.register(lambda: scheduler.shutdown())
Quickstart
from apscheduler.schedulers.background import BackgroundScheduler
from datetime import datetime
import time
def job_function():
print(f'Job ran at {datetime.now()}')
# BackgroundScheduler runs in a daemon thread
scheduler = BackgroundScheduler()
# Interval trigger
scheduler.add_job(job_function, 'interval', seconds=10, id='my_job')
# Cron trigger (every day at 9:30am)
scheduler.add_job(job_function, 'cron', hour=9, minute=30)
# Date trigger (one-off)
from datetime import timedelta
scheduler.add_job(
job_function,
'date',
run_date=datetime.now() + timedelta(minutes=1)
)
# Must start explicitly
scheduler.start()
print('Scheduler started. Press Ctrl+C to exit.')
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
scheduler.shutdown()