Interval and cron jobs for FastAPI, Django, Celery, and any
Python backend.
Run them on a background thread with per-job retries and live inspection — no database, no
broker.
pip install rust-py-scheduler
Rust handles timing, threads, and retries. Python stays simple.
Run a job "10s", "5m", or "1h" apart. One readable string — no
timestamps to manage.
Standard 5-field Unix cron. scheduler.cron("0 9 * * 1-5", fn) runs weekdays at 9am, in your
local timezone.
Register with a direct call (scheduler.every("5s", fn)) or a decorator
(@scheduler.every("5s")) — your function stays callable.
start_background() runs the loop on a dedicated OS thread and returns immediately. The GIL is
released while idle.
max_retries=N retries a failing job immediately, up to N extra times, before the tick counts
as an error.
An exception in one job is caught, printed, and tracked in error_count /
last_error.
Every other job keeps running.
scheduler_lifespan() for FastAPI, start_in_background() for Django — start and
stop with your app's lifecycle.
A task's .delay is just a callable, so scheduler.every("5m", task.delay) works
with no extra glue.
Scheduling, timing, retries, and thread management compile to a native .so extension. No Rust
needed to use it.
Call the core API, or wire it into your framework's lifecycle.
import time
from rust_py_scheduler import Scheduler
scheduler = Scheduler()
# Direct call: registers immediately, returns the job id.
job_id = scheduler.every("2s", lambda: print("tick"))
# Decorator form: same registration, function stays callable.
@scheduler.every("3s", max_retries=2)
def report():
print("tick (with retry budget)")
scheduler.start_background() # returns immediately
time.sleep(7)
for job in scheduler.list_jobs():
print(job) # run_count, error_count, next_run_at, ...
scheduler.remove_job(job_id)
scheduler.shutdown()
from rust_py_scheduler import Scheduler
scheduler = Scheduler()
# 5-field Unix cron: minute hour day-of-month month day-of-week
scheduler.cron("0 * * * *", hourly_task) # top of every hour
scheduler.cron("*/15 * * * *", poll) # every 15 minutes
@scheduler.cron("0 9 * * 1-5") # weekdays at 9am
def morning_report():
...
@scheduler.cron("30 2 * * *", max_retries=2) # daily at 02:30
def nightly_cleanup():
...
# Evaluated in local time. Invalid expressions raise ValueError now,
# not later — fields support *, a-b, */step, and 1,2,3 lists.
from fastapi import FastAPI
from rust_py_scheduler import Scheduler
from rust_py_scheduler.fastapi import scheduler_lifespan
scheduler = Scheduler()
@scheduler.every("30s")
def heartbeat():
...
# Starts on app startup, shuts down (and joins) on app shutdown.
app = FastAPI(lifespan=scheduler_lifespan(scheduler))
# apps.py
from django.apps import AppConfig
from rust_py_scheduler import Scheduler
from rust_py_scheduler.django import start_in_background
scheduler = Scheduler()
@scheduler.every("5m")
def refresh_cache():
...
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self):
# Idempotent per process; best-effort atexit shutdown.
start_in_background(scheduler)
from rust_py_scheduler import Scheduler
scheduler = Scheduler()
# A Celery task's .delay is just a callable — no extra glue needed.
scheduler.every("5m", lambda: send_report.delay("daily-metrics"))
scheduler.cron("0 8 * * 1-5", lambda: send_report.delay("digest"))
# Need countdown / eta / queue routing? Use apply_async:
scheduler.every(
"1h",
lambda: send_report.apply_async(args=["hourly"], countdown=10),
)
# The scheduler only enqueues; the Celery worker does the work.
Five fields, Unix semantics — the expressions you already know.
* every value5 single9-17 range*/15 step0,30 listlocal time timezone1 minute resolutionValueError eager validation# weekdays at 9am — registered, returns a job id
"7f3c9a12-4b8e-..."
{
"schedule": "cron 0 9 * * 1-5",
"run_count": 3,
"next_run_at": "1718960400",
"last_error": None
}
Rust schedules and runs. Python registers. You inspect.
The native .so extension is compiled once at publish time via maturin +
PyO3, against the stable ABI (abi3-py310) — one wheel covers Python
3.10–3.13+.
Your users just pip install — no Rust toolchain, no database, no message broker required.
Install from PyPI. No Rust, no compilers, no configuration.