APScheduler

raw JSON →
3.11.2 verified Tue May 12 auth: no python install: verified quickstart: stale

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.

pip install APScheduler
error 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.
fix
Rename or remove the 'apscheduler.py' file in your project directory to avoid naming conflicts with the APScheduler package.
error 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''.
fix
Replace 'add_cron_job' with 'add_job' and specify 'trigger='cron'' in your code.
error ImportError: No module named 'apscheduler'
cause This error indicates that the APScheduler package is not installed in the Python environment.
fix
Install APScheduler using 'pip install apscheduler'.
error 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.
fix
Import the specific scheduler class from its correct submodule, such as from apscheduler.schedulers.background import BackgroundScheduler or from apscheduler.schedulers.blocking import BlockingScheduler.
error 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.
fix
Pass the function reference without parentheses: scheduler.add_job(my_function, 'interval', seconds=5).
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.
fix For production: pip install APScheduler installs stable 3.x. For 4.x pre-release: pip install 'APScheduler>=4.0a1'. The 3.x and 4.x APIs are completely incompatible.
gotcha scheduler.start() must be called explicitly. Creating a scheduler and adding jobs without calling start() does nothing — jobs never run.
fix Always call scheduler.start() after adding jobs. For Flask/Django: call it in app startup, not module level.
gotcha Jobs are lost on restart unless a persistent job store (SQLAlchemy, Redis, MongoDB) is configured. The default MemoryJobStore holds jobs only in RAM.
fix Configure a persistent job store: scheduler = BackgroundScheduler(jobstores={'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')})
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.
fix Run the scheduler in a single dedicated process. Use a persistent job store with coalesce=True and max_instances=1. Or use a task queue (Celery, RQ) for multi-process environments.
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.
fix Web apps: use BackgroundScheduler. Standalone scheduler scripts: use BlockingScheduler. Async apps: use AsyncIOScheduler.
gotcha All times are naive (timezone-unaware) unless you configure a timezone. Scheduled jobs may run at wrong times during DST transitions.
fix Always set timezone: BackgroundScheduler(timezone='UTC') or use pytz: BackgroundScheduler(timezone=pytz.timezone('America/New_York'))
pip install APScheduler[sqlalchemy]
pip install APScheduler[redis]
python os / libc variant status wheel install import disk
3.10 alpine (musl) APScheduler - - 0.28s 18.5M
3.10 alpine (musl) redis - - 0.29s 22.3M
3.10 alpine (musl) sqlalchemy - - 0.29s 43.2M
3.10 slim (glibc) APScheduler - - 0.20s 19M
3.10 slim (glibc) redis - - 0.19s 23M
3.10 slim (glibc) sqlalchemy - - 0.19s 42M
3.11 alpine (musl) APScheduler - - 0.40s 20.4M
3.11 alpine (musl) redis - - 0.39s 25.0M
3.11 alpine (musl) sqlalchemy - - 0.39s 48.3M
3.11 slim (glibc) APScheduler - - 0.31s 21M
3.11 slim (glibc) redis - - 0.32s 25M
3.11 slim (glibc) sqlalchemy - - 0.34s 47M
3.12 alpine (musl) APScheduler - - 0.63s 12.3M
3.12 alpine (musl) redis - - 0.57s 16.7M
3.12 alpine (musl) sqlalchemy - - 0.57s 39.7M
3.12 slim (glibc) APScheduler - - 0.54s 13M
3.12 slim (glibc) redis - - 0.54s 17M
3.12 slim (glibc) sqlalchemy - - 0.54s 38M
3.13 alpine (musl) APScheduler - - 0.60s 11.9M
3.13 alpine (musl) redis - - 0.58s 16.3M
3.13 alpine (musl) sqlalchemy - - 0.58s 39.1M
3.13 slim (glibc) APScheduler - - 0.54s 12M
3.13 slim (glibc) redis - - 0.55s 17M
3.13 slim (glibc) sqlalchemy - - 0.55s 38M
3.9 alpine (musl) APScheduler - - 0.27s 18.0M
3.9 alpine (musl) redis - - 0.27s 21.2M
3.9 alpine (musl) sqlalchemy - - 0.28s 41.8M
3.9 slim (glibc) APScheduler - - 0.22s 18M
3.9 slim (glibc) redis - - 0.21s 22M
3.9 slim (glibc) sqlalchemy - - 0.23s 41M

3.x stable API. BackgroundScheduler runs in a thread. Always call start() and shutdown().

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()