Library / SDK
agronholm/apscheduler avatar
agronholm/apscheduler

APScheduler: a task scheduler that lives inside your Python process

Task scheduling library for Python

7,636 stars789 forksPythonMIT

At a glance

What is it?
It schedules jobs in the process that needs them, with cron, interval and calendar triggers, optional persistent stores and event brokers. The 4.0 line is still flagged pre-release.
Who is it for?
APScheduler is the right choice when the job belongs to the same process as the code that schedules it, because the library needs no broker, no separate worker and no extra container. It is the wrong choice when the work must survive a restart without a database behind it, or when you need work distribution across machines, because that is a queue and a worker fleet rather than a scheduler.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 16 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 21, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Scheduling inside the process that needs the work

APScheduler describes itself twice in the README, and both descriptions matter. It is a task scheduler, and it is a task queue system. That pairing is unusual. Most Python scheduling libraries are only the first thing, and the queue comes from somewhere else, usually Celery plus a broker. APScheduler can be used purely as a job queue if you have no scheduling needs, which is the case where it substitutes for a queue rather than adding to one.

The operational difference is that the default deployment has no moving parts beyond your own application. The scheduler object lives in your process, triggers fire in that process, and your job functions are called in that process. Nothing to deploy, nothing to monitor, no network hop between deciding to run something and running it. The cost is equally direct: if the process dies mid job, the work is gone unless you gave the jobs a persistent store and a way to recover.

The scaling story is layered on top of that base case. The README says multiple schedulers and workers can share a data store for high availability and horizontal scaling, which means the library will grow into a distributed arrangement if you want it to, but nothing forces you there. A single process with a SQLite store is a supported configuration.

Two major versions, and the README tells you which to use

The README opens with a warning that is easy to skim past and impossible to ignore later:

> The v4.0 series is provided as a pre-release and may change in a backwards incompatible fashion without any migration pathway, so do NOT use this release in production!

That single paragraph is the most important thing on the page. The release list backs it up. The three most recent releases are 3.11.3 on 2026-06-28, 3.11.2 on 2025-12-22 and 3.11.1 on 2025-10-31, all on the 3.x line. There is no 4.0 release in the list at all, which means a fresh install has to be an explicit choice between the stable line and a pre-release that the project says has no migration path.

Nothing is archived and the last push was on 2026-09-20, so this is an active project with a stable line and an in progress rewrite running side by side. The packaging confirms the supported floor: `requires-python = >= 3.10`, classifiers through Python 3.14, and the AnyIO framework classifier, which is the interesting one. AnyIO is a hard dependency, and AnyIO is the layer that lets the same library serve synchronous code, asyncio and Trio.

The dependency list is short and tells you what the library actually needs: `anyio ~= 4.0`, `attrs >= 22.1`, `tenacity >= 8.0` for retries, and `tzlocal >= 3.0`. Everything else is optional.

Data stores and event brokers define the deployment you end up with

The README splits the optional pieces into two lists, and keeping them apart explains the architecture. Data stores hold schedules and jobs so they can be shared between instances and survive restarts: PostgreSQL, MySQL and derivatives, SQLite, and MongoDB. Event brokers are needed when you run more than one scheduler or worker, and there are three: PostgreSQL, Redis and MQTT.

Those two lists appear in the optional dependency extras in `pyproject.toml`, one extra per backend, which is a tidy way to see the whole support surface:

toml
[project.optional-dependencies]
asyncpg = ["asyncpg >= 0.20"]
mongodb = ["pymongo >= 4.13.0"]
mqtt = ["paho-mqtt >= 2.0"]

Note that PostgreSQL appears twice in the project's world, as a data store and as a broker, and that it gets two drivers, `asyncpg` and `psycopg`. Anyone already running Postgres for the application does not add a new service.

The test infrastructure shows how seriously both paths are taken. The `docker-compose.yml` at the repository root brings up five services for the test suite: Postgres on 5432, MySQL on 3306, MongoDB on 27017, an EMQX broker for MQTT on 1883, and Redis on 6379. That is not a casual list. It is a project that expects its backends to behave differently, which is a reasonable expectation given that scheduler bugs tend to show up as jobs that quietly did not run.

Installing the scheduler and starting the container you need

There is no install snippet in the README, which is typical for a library whose packaging is unremarkable. The project name on PyPI is APScheduler and the module is imported from `apscheduler`.

bash
pip install APScheduler

Add the extra for whichever backend you want rather than installing everything, for example the Redis and SQLAlchemy extras, which cover both a Redis broker and a relational job store.

If you are running the test suite or experimenting with the broker and store combinations locally, the compose file in the repository root starts all five backends at once:

yaml
services:
  redis:
    image: redis
    ports:
      - 127.0.0.1:6379:6379

The `examples/` directory is the fastest way to see a working arrangement rather than a fragment. It contains `standalone/`, `web/`, `separate_worker/` and `gui/`. The separate worker example is the one to read if you want jobs to run outside the process that schedules them, because that is the shape that requires a shared store and a broker, and it is the shape where the design decisions in the previous section become necessary rather than optional.

The release notes are a timezone bug tracker

Reading the three most recent release bodies tells you what actually goes wrong in this library, and it is not what you might guess. All three releases fix daylight saving time behaviour in `CronTrigger` and interval jobs.

3.11.1 fixed a `CronTrigger` sticking on a folded datetime during the fall back transition, and fixed `scheduler.shutdown()` raising the wrong exception on asynchronous schedulers. 3.11.2 fixed an infinite loop where a `CronTrigger` job scheduled in a repeated interval during a DST transition could hang the scheduler. 3.11.3 fixed sub-minute interval jobs stalling for the length of a spring forward gap when the scheduler used a `ZoneInfo` time zone, because the wakeup delay was computed from the naive wall clock difference instead of the actual UTC difference. The same release fixed imported jobs losing their scheduler and job store links.

That is three consecutive releases with a timezone defect, and the details are worth reading as a warning about your own deployment. A scheduler that computes the next wakeup from wall clock time will behave differently twice a year unless the time zone is handled carefully. If your jobs must run at a fixed local time across a DST boundary, this is the library where you test that behaviour explicitly.

The other notable features from the README are about not overloading yourself: a cap on the maximum number of simultaneous instances of a given function, a limit on how late a job is allowed to start, and jitter, which adds a random delay to each run so a fleet of processes does not all wake at the same instant.

Where a scheduler stops and a queue begins

The most common comparison with APScheduler is Celery, and the difference is architectural rather than a matter of feature coverage. Celery is a distributed task queue: you publish messages to a broker, workers pull them off, and the queue is the source of truth for outstanding work. APScheduler is a scheduler that keeps its own record of what is due and when, and by default runs the work in the same process that decided to run it.

The practical consequences follow from that. With Celery you get work distribution across machines and retry policies as part of the model, and you pay for a broker plus worker processes plus result backend infrastructure. With APScheduler you get cron style scheduling, misfire control and jitter without any of that, and if the process holding the scheduler dies, in memory jobs do not come back.

The honest gap is durability. A persistent job store means the schedule survives a restart, which is different from saying the running job survives a crash. Any work that must not be lost needs its own recovery story, whether that is an idempotent job, a database transaction that can be repeated, or a real queue downstream.

The other comparison worth naming is plain cron, which is still the right answer more often than people expect. If the job is a script that exits, cron plus a log file and an alerting wrapper is less machinery. APScheduler earns its place when the job is a function inside an application that needs the schedule to live in the same codebase and the same deployment as the code that registered it.

Editorial conclusion

APScheduler is the right choice when the job belongs to the same process as the code that schedules it, because the library needs no broker, no separate worker and no extra container. It is the wrong choice when the work must survive a restart without a database behind it, or when you need work distribution across machines, because that is a queue and a worker fleet rather than a scheduler. Version 3.11.3 is the safe line today and version 4.0 is explicitly marked as not for production. The first thing to verify is which line your dependencies resolve to, since both can be installed into the same environment.

Frequently asked questions

What are the key differences between Celery and APScheduler?

Celery is a distributed task queue that publishes messages to a broker for workers to pull, so work spreads across machines and retries are part of the model. APScheduler is a scheduler that records what is due and when, and by default runs the job in the same process, with optional persistent stores and event brokers when you outgrow that.

How do I install APScheduler?

Install it with `pip install APScheduler`, then add the extra for the backend you need, such as the Redis or SQLAlchemy extras. Python 3.10 or newer is required. The README does not carry an install snippet, so the packaging metadata in `pyproject.toml` is the reference.

Can I use APScheduler 4.0 in production?

Not according to the README, which states that the 4.0 series is a pre-release that may change in a backwards incompatible fashion without any migration pathway, and says not to use it in production. The latest releases in the repository are on the 3.11 line, with 3.11.3 published on 2026-06-28.

What triggers and backends does APScheduler support?

Triggers cover cron style scheduling, even intervals, calendar based intervals such as X months at the same time of day, and one off dates, and they can be combined. Persistent data stores are PostgreSQL, MySQL, SQLite and MongoDB, and event brokers for multi instance setups are PostgreSQL, Redis and MQTT. Custom trigger classes are treated the same as the built in ones.

Official sources

  1. agronholm/apscheduler on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/agronholm-apscheduler.svg)](https://hysenlabs.com/projects/agronholm-apscheduler)