Kron is an embedded scheduler.
It runs inside a Python process, stores timer state in a local .kron/
directory, and can execute Python callbacks on schedules such as cron,
every, after, and at.
Embedded mode does not require Redis, RabbitMQ, Celery, a separate scheduler daemon, Kubernetes, or a cloud scheduler.
pip install kron-schedulerimport kronimport time
import kron
def send_digest():
print("send digest")
def cleanup():
print("cleanup")
kron.schedule("email_digest", cron="0 8 * * *", fn=send_digest)
kron.schedule("cleanup", every="30m", fn=cleanup)
kron.start(data_dir=".kron")
try:
while True:
time.sleep(60)
finally:
kron.shutdown()kron.start() is non-blocking. The scheduler runs in a background Rust runtime
thread.
kron.schedule("daily_digest", cron="0 8 * * *", fn=send_digest)
kron.schedule("cleanup", every="30m", fn=cleanup)
kron.schedule("retry_later", after="10s", fn=retry)
kron.schedule("new_year", at="2027-01-01T00:00:00Z", fn=celebrate)Exactly one schedule selector is used per timer.
Callbacks can take no arguments:
def cleanup():
delete_temp_files()Or one context dictionary:
def cleanup(context):
print(context["timer_id"])
print(context["run_id"])Python callback objects are not serialized. After process restart, the app must
call kron.schedule(...) again to reconnect the persisted timer metadata to the
Python function.
kron.start(data_dir=".kron")
kron.status("cleanup")
kron.list()
kron.shutdown(timeout=5.0)Async wrappers:
await kron.astart(data_dir=".kron")
timers = await kron.alist()
status = await kron.astatus("cleanup")
await kron.ashutdown()The async wrappers call the same runtime without blocking the event loop directly.
kron.schedule(
"sync_customer_data",
every="15m",
fn=sync_customer_data,
max_attempts=5,
)Failed callbacks are recorded as events and retried until the configured attempt limit is reached.
External side effects should be idempotent.
Kron can prevent multiple copies of the same timer from running at once.
kron.schedule(
"sync_reports",
every="10m",
fn=sync_reports,
overlap="skip",
)Overlap policies:
| Policy | Behavior |
|---|---|
delay |
Default. Wait for the current run to finish, then schedule the next run from that finish time. |
skip |
Keep the wall-clock schedule, but skip a due run if the previous run is still active. The skip is written to history. |
allow |
Allow concurrent runs of the same timer. |
Use overlap="skip" for jobs such as imports, report generation, billing sync,
or cache refreshes where a second copy should not start while the first one is
still running.
kron job list
kron job status <timer>
kron job history <timer>
kron log compact
kron doctor
kron runtime status
kron runtime shutdownUse a custom data directory:
kron --data-dir /var/lib/myapp/kron job listThe CLI uses local IPC when a runtime is active and read-only storage fallback when it is not.
Embedded mode stores state under data_dir:
.kron/
kron.aof
kron.snapshot
kron.lock
kron.token
kron.sock
kron.port
Storage behavior:
- timer changes and run transitions are written to an append-only log;
- snapshots compact derived state;
- startup restores state from snapshot plus AOF tail;
kron.lockprevents two writers on the same data directory;- truncated final tails are handled as crash tails;
- middle corruption fails loudly.
Persisted state includes:
- timer definitions;
- schedule metadata;
- next run time;
- retry state;
- last run status;
- run history;
- orphaned timer metadata after restart.
Kron records timer transitions as events:
TIMER_CREATED
RUN_DUE
RUN_STARTED
RUN_SUCCEEDED
RUN_FAILED
RUN_RETRYING
RUN_DEAD
Server mode also uses worker/claim events:
RUN_CLAIMED
RUN_LEASE_EXPIRED
WORKER_REGISTERED
WORKER_HEARTBEAT
WORKER_LOST
Embedded mode is useful when adding a scheduler service would be too much:
- local agents;
- edge devices;
- RISC-V boards;
- small services;
- offline-first tools;
- appliances;
- internal maintenance tasks.
Deployment shape:
Python process
-> Kron runtime
-> .kron/ local state
Kron also includes a standalone server mode for serializable worker tasks.
Start a node:
kron --data-dir .kron-n1 server start \
--node-id n1 \
--http 127.0.0.1:7379 \
--raft 127.0.0.1:7380 \
--cluster-token dev-secretCreate a timer that targets a worker task:
import kron
client = kron.Client("http://127.0.0.1:7379", token="dev-secret")
client.schedule(
"email_digest",
cron="0 8 * * *",
task="send_digest",
payload={"list": "daily"},
max_attempts=3,
)Worker:
worker = kron.Worker("http://127.0.0.1:7379", token="dev-secret")
@worker.task("send_digest")
def send_digest(payload):
send_email_digest(payload["list"])
worker.run()Server mode uses OpenRaft-backed state, worker leases, fencing tokens, token auth, role-scoped tokens, tenant-scoped workers, and an append-only audit log.
For public or enterprise exposure, put Kron behind a reverse proxy, service mesh, or private network boundary for TLS/mTLS and network policy.
Server mode writes security decisions to:
.kron/kron.audit.jsonl
Audit records are hash-chained.
Verify:
kron audit verifyInspect:
kron audit tail
kron audit query --actor tenant-a-worker
kron audit query --action worker.pollFor embedded scheduling, Kron can replace:
- system cron for app-owned jobs;
while True: sleep(...)loops;- Celery beat for small local schedules;
- RQ scheduler for simple jobs;
- custom database polling tables;
- cloud scheduler webhooks for local jobs.
For workflow DAGs, long-running business processes, or complex orchestration, use a workflow engine such as Temporal, Airflow, or Prefect.
cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspacepython3 -m venv .venv
.venv/bin/pip install -U pip maturin pytest
.venv/bin/maturin develop
.venv/bin/python -m pytest -q tests/python- Python Usage
- CLI Usage
- Security Guide
- Storage Format
- Snapshot and Compaction
- Multiprocess IPC
- Distributed Readiness
- Release Checklist
BSD 3-Clause. See LICENSE.