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.
Common errors
-
ModuleNotFoundError: No module named 'apscheduler.schedulers'; 'apscheduler' is not a package
cause This error occurs when a script named 'apscheduler.py' in the project directory shadows the APScheduler package, causing import conflicts.fixRename or remove the 'apscheduler.py' file in your project directory to avoid naming conflicts with the APScheduler package. -
AttributeError: 'BackgroundScheduler' object has no attribute 'add_cron_job'
cause The 'add_cron_job' method was removed in APScheduler 3.x; the correct method to add cron jobs is 'add_job' with 'trigger='cron''.fixReplace 'add_cron_job' with 'add_job' and specify 'trigger='cron'' in your code. -
ImportError: No module named 'apscheduler'
cause This error indicates that the APScheduler package is not installed in the Python environment.fixInstall APScheduler using 'pip install apscheduler'. -
AttributeError: module 'apscheduler' has no attribute 'Scheduler'
cause Users are attempting to import the `Scheduler` class directly from the `apscheduler` module, which was a pattern in APScheduler 2.x but is incorrect for the 3.x series. In APScheduler 3.x, scheduler classes are located in specific submodules.fixImport the specific scheduler class from its correct submodule, such as `from apscheduler.schedulers.background import BackgroundScheduler` or `from apscheduler.schedulers.blocking import BlockingScheduler`. -
TypeError: func must be callable
cause The function intended to be scheduled is being called immediately when passed to `add_job` (e.g., `my_function()`) instead of passing a reference to the function itself (`my_function`). This results in the return value of `my_function()` being passed, which is likely not a callable object.fixPass the function reference without parentheses: `scheduler.add_job(my_function, 'interval', seconds=5)`.
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
# 4.x API (pre-release, NOT installed by pip install APScheduler): from apscheduler import Scheduler # ImportError on 3.x install with Scheduler() as scheduler: scheduler.add_schedule(my_function, IntervalTrigger(seconds=30)) scheduler.run_until_stopped()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()