Effortlessly schedule and manage your background tasks.
Built with the tools and technologies:
Welcome to the official guide for using fastapi_crons, a high-performance, developer-friendly cron scheduling extension for FastAPI. This library enables you to define, monitor, and control scheduled background jobs using simple decorators and provides CLI tools, web-based monitoring, and SQLite-based job tracking.
- Native integration with FastAPI using
from fastapi import FastAPI, Crons - Define cron jobs with decorators
- Async + sync job support
- SQLite job state persistence
- CLI for listing and managing jobs
- Automatic monitoring endpoint (
/crons) - Named jobs, tags, and metadata
- Easy to plug into any FastAPI project
pip install fastapi-cronsThat includes the web dashboard. Optional extras add the pluggable backends:
pip install fastapi-crons[sqlalchemy] # SQLAlchemy state/lock backends
pip install fastapi-crons[sqlmodel] # SQLModel state/lock backends
pip install fastapi-crons[otel] # OpenTelemetry tracingfrom fastapi import FastAPI
from fastapi_crons import Crons, get_cron_router
app = FastAPI()
crons = Crons(app)
# Mount the management endpoints under a prefix. Without one they are served
# from the application root, where `GET /{job_name}` shadows your own routes.
app.include_router(get_cron_router(), prefix="/crons")
@app.get("/")
def root():
return {"message": "Hello from FastAPI"}@crons.cron("*/5 * * * *", name="print_hello")
def print_hello():
print("Hello! I run every 5 minutes.")
@crons.cron("0 0 * * *", name="daily_task", tags=["rewards"])
async def run_daily_task():
# Distribute daily rewards or any async task
await some_async_function()ββββββββββββββ minute (0 - 59)
β ββββββββββββββ hour (0 - 23)
β β ββββββββββββββ day of the month (1 - 31)
β β β ββββββββββββββ month (1 - 12)
β β β β ββββββββββββββ day of the week (0 - 6) (Sunday to Saturday)
β β β β β
* * * * ** * * * *: Every minute*/15 * * * *: Every 15 minutes0 * * * *: Every hour0 0 * * *: Every day at midnight0 0 * * 0: Every Sunday at midnight
Once included, visit:
GET /crons
You'll get a full list of jobs with:
nameexpr(cron expression)tagslast_run(from SQLite)next_run
A prebuilt web UI for browsing jobs and their run history ships with the
package β no extra needed. Mount the cron router and visit /dashboard
underneath whatever prefix you chose:
GET /dashboard
See examples/dashboard/app.py for a runnable
setup.
Note: the dashboard exposes job names, schedules and run history, and ships with no authentication of its own. Put it behind your own auth or network controls before mounting it on a public deployment.
We use SQLite (via aiosqlite) to keep a persistent record of when each job last ran. This allows observability and resilience during restarts.
For projects already using SQLAlchemy or SQLModel with PostgreSQL or MySQL, you can reuse your existing database connection instead of SQLite.
pip install fastapi-crons[sqlalchemy]
# or
pip install fastapi-crons[sqlmodel]from sqlalchemy.ext.asyncio import create_async_engine
from fastapi_crons import Crons
from fastapi_crons.state.sqlalchemy import SQLAlchemyStateBackend
engine = create_async_engine("postgresql+asyncpg://user:pass@host/db")
backend = SQLAlchemyStateBackend(engine)
crons = Crons(app, state_backend=backend)Works with sync engines too:
from sqlalchemy import create_engine
engine = create_engine("postgresql://user:pass@host/db")
backend = SQLAlchemyStateBackend(engine)# env.py
from fastapi_crons.state.sqlalchemy import cron_metadata
target_metadata = [Base.metadata, cron_metadata]pip install fastapi-crons[sqlalchemy]from fastapi_crons.locking.sqlalchemy import SQLAlchemyLockBackend
from fastapi_crons.locking import DistributedLockManager
from fastapi_crons import CronConfig
engine = create_async_engine("postgresql+asyncpg://...")
backend = SQLAlchemyLockBackend(engine)
manager = DistributedLockManager(backend, CronConfig())
crons = Crons(app, lock_manager=manager)For PostgreSQL, advisory locks are available as a lighter alternative (no table required):
from fastapi_crons.locking.sqlalchemy import PostgreSQLAdvisoryLockBackend
backend = PostgreSQLAdvisoryLockBackend(engine)CREATE TABLE IF NOT EXISTS job_state (
name TEXT PRIMARY KEY,
last_run TEXT
);By default, job state is stored in a SQLite database named cron_state.db in the current directory. You can customize the database path:
from fastapi_crons import Crons, SQLiteStateBackend
# Custom database path
state_backend = SQLiteStateBackend(db_path="/path/to/my_crons.db")
crons = Crons(state_backend=state_backend)Every worker registers the same jobs, so when a job becomes due exactly one worker must execute it. Point all workers at a shared lock backend and that is guaranteed: each scheduled tick is claimed atomically, and the worker that wins the claim is the only one that runs it.
export CRON_ENABLE_DISTRIBUTED_LOCKING=true
export CRON_REDIS_URL=redis://localhost:6379/0# ...or wire a backend explicitly (a URL works, a client is not required)
from fastapi_crons import Crons, CronConfig, DistributedLockManager
from fastapi_crons.locking import RedisLockBackend
config = CronConfig()
manager = DistributedLockManager(RedisLockBackend(config.redis_url), config)
crons = Crons(app, lock_manager=manager)No Redis? The SQLAlchemy lock backend coordinates through your existing database instead:
from fastapi_crons.locking.sqlalchemy import SQLAlchemyLockBackend
manager = DistributedLockManager(SQLAlchemyLockBackend(engine), CronConfig())Two things to know:
- The default
SQLiteStateBackendis per-machine. Workers spread across hosts need Redis or the SQLAlchemy state backend. - Lock keys are namespaced
fastapi_crons:lock:so they cannot collide with anything else in a shared Redis. Override withCRON_LOCK_KEY_PREFIX.
A job that takes longer than its own interval does not queue up: ticks that elapse while it runs are coalesced, and it resumes at the next scheduled time.
The scheduler supports both async and sync job functions Jobs can be:
async defβ run in asyncio loopdefβ run safely in background thread usingawait asyncio.to_thread(...)
# List all registered jobs
fastapi-crons list
# Manually run a specific job
# -i imports the module that registers your jobs (repeatable)
fastapi-crons run-job <job_name> -i myapp.jobs
# Show overall system status
fastapi-crons status
# Inspect / change configuration
fastapi-crons config-show
fastapi-crons config-set <key> <value>
# Run the scheduler outside of a FastAPI app
fastapi-crons start-scheduler -i myapp.jobs
# See every command and its options
fastapi-crons --help- Distributed locking via Redis
- Retry policies
- Manual run triggers via HTTP
- Admin dashboard with metrics
You can add tags to jobs for better organization:
@cron_job("*/5 * * * *", tags=["maintenance", "cleanup"])
async def cleanup_job():
# This job has tags for categorization
passFastAPI App
β
βββ Crons()
β βββ Registers decorated jobs
β βββ Starts background scheduler (async)
β
βββ SQLite Backend
β βββ Tracks last run for each job
β
βββ /crons endpoint
β βββ Shows current job status (with timestamps)
β
βββ CLI Tool
βββ List jobs / Run manually
We welcome PRs and suggestions! If you'd like this added to FastAPI officially, fork the repo, polish it, and submit to FastAPI with a clear integration proposal.
- Each job has an isolated error handler
- Errors are printed and don't block scheduler
- Future: Add error logging / alert hooks
Made with β€οΈ by Mehar Umar.
Designed to give developers freedom, flexibility, and control when building production-grade FastAPI apps.