Embedded-first runtime for reliable application scheduling
Project description
Kron
Reliable application time, embedded first.
Cron runs commands.
Kron makes time observable, persistent, and coordinated.
What Is Kron?
Kron is a small runtime for application scheduling.
It is built around one idea:
Time should be state, not a blind background command.
Classic cron can run something at 08:00, but it cannot tell your application much about what happened. Kron gives timers persistent state, history, retries, crash recovery, observability, and a path from embedded mode to a standalone server.
Kron starts as an embedded time engine.
The server is a deployment mode, not the product.
What It Does Today
Kron currently provides an embedded Python runtime backed by a Rust engine.
It can:
- register timers from Python callbacks;
- run timers from
cron,every,after, oratschedules; - start in the background without blocking the Python process;
- persist timer metadata in a local
.kron/data directory; - recover timer metadata after process restart;
- mark persisted timers as
orphaneduntil their Python function is re-registered; - record every run as events in an append-only log;
- retry failed callbacks with configurable max attempts;
- expose timer status and history through Python and the CLI;
- compact the append-only log into a JSON snapshot;
- prevent two runtimes from writing the same data directory at the same time;
- let the CLI inspect an active runtime through local IPC.
It also includes an experimental server mode that can:
- run as a standalone process;
- accept JSON task schedules over HTTP;
- register Python workers by task name;
- assign runs to workers;
- attach fencing tokens to claimed runs;
- persist server state through an OpenRaft-backed file store.
Server mode is not the recommended production path yet.
Common Use Cases
Kron is useful when an application needs scheduled work but a full job stack is too heavy.
Good v0.1 use cases:
- send periodic email digests from a Python app;
- clean local or application-owned resources on a schedule;
- run small maintenance callbacks;
- retry transient application tasks;
- keep observable history for scheduled work;
- replace ad-hoc
while True: sleep(...)loops; - prototype scheduling without Redis, RabbitMQ, Celery, or cloud schedulers.
Not recommended yet:
- critical payment processing;
- high-volume distributed task queues;
- multi-tenant hosted scheduling;
- replacing Temporal, Airflow, Prefect, or a production message broker;
- depending on stable storage compatibility across alpha releases.
Feature Matrix
| Capability | Status | Notes |
|---|---|---|
| Embedded Python scheduling | Works | Primary v0.1 path |
cron, every, after, at schedules |
Works | Timezone support exists; DST needs continued hardening |
Non-blocking kron.start() |
Works | Runs in a background thread |
| Persistent timer metadata | Works | Stored under .kron/ |
| Append-only event log | Works | NDJSON, fsync on append |
| Snapshot and compaction | Works | Format is not stable before v1.0 |
| Retry on callback failure | Works | Max attempts supported |
| CLI status/list/history | Works | Uses IPC when runtime is active, read-only fallback otherwise |
| Data directory locking | Works | Prevents two local writers |
Python Client/Worker server API |
Experimental | For serializable task payloads |
| OpenRaft server mode | Experimental | See distributed readiness |
| Async Python wrapper | Works | await kron.astart(), sync callbacks only |
| PyPI release | Ready to publish | Manual/Trusted Publishing workflow prepared |
Why Kron?
Most applications eventually need scheduled work:
- send an email digest every morning;
- clean temporary files every 30 minutes;
- retry a failed payment later;
- run a maintenance task once at a specific time;
- know whether yesterday's scheduled job actually succeeded.
Today this usually means choosing between system cron, Celery, Sidekiq, Airflow, Temporal, cloud schedulers, custom Redis locks, or a homemade loop.
Kron aims for the missing middle:
- embedded like SQLite;
- observable like a real runtime;
- persistent without an external database;
- simple enough to use in a small app;
- structured enough to grow into server mode.
Install
pip install kron-scheduler
The package name is
kron-schedulerbecausekronis already occupied on PyPI. The Python import remainsimport kron.
python -m venv .venv
.venv/bin/pip install maturin pytest
.venv/bin/maturin develop
Run an example:
.venv/bin/python examples/email_digest.py
Quickstart: Embedded Python
import kron
def send_digest():
print("sending digest")
def cleanup_temp_files():
print("cleaning temp files")
kron.schedule("email_digest", cron="0 8 * * *", fn=send_digest)
kron.schedule("cleanup", every="30m", fn=cleanup_temp_files)
kron.start(data_dir=".kron") # non-blocking, runs in a background thread
No broker.
No external database.
No worker stack.
No cloud scheduler.
Timers persist in .kron/, and every run writes events to an append-only log.
Embedded public API:
kron.schedule(name, fn=callable, cron="0 8 * * *")
kron.schedule(name, fn=callable, every="30m")
kron.schedule(name, fn=callable, after="10s")
kron.schedule(name, fn=callable, at=datetime_obj)
kron.start(data_dir=".kron")
kron.status(name)
kron.list()
kron.shutdown(timeout=5.0)
await kron.astart(data_dir=".kron")
await kron.astatus(name)
await kron.alist()
await kron.ashutdown(timeout=5.0)
Callbacks can accept no arguments, or one context dictionary:
def task(context):
print(context["timer_id"], context["run_id"])
Observe Timers
kron job list
kron job status email_digest
kron job history email_digest
Example status:
status: scheduled
fn: send_digest (registered)
next_run: 2026-06-11 08:00:00 Europe/Rome
last_run: 2026-06-10 08:00:01
duration: 340ms
retries: 0
Example history:
2026-06-10 08:00:01 RUN_STARTED
2026-06-10 08:00:01 RUN_SUCCEEDED 340ms
2026-06-09 08:00:01 RUN_FAILED timeout
2026-06-09 08:00:33 RUN_RETRYING attempt 2
2026-06-09 08:00:34 RUN_SUCCEEDED 280ms
Core Ideas
Persistent Timers
A timer is not just a callback. It has state:
- schedule;
- next run time;
- last run result;
- retry count;
- history;
- registration status.
Event Log First
Kron records state transitions as events:
TIMER_CREATED
RUN_DUE
RUN_STARTED
RUN_SUCCEEDED
RUN_FAILED
RUN_RETRYING
RUN_DEAD
State is derived from the log and can be inspected by the CLI.
Honest Execution Semantics
Kron can make scheduling reliable.
Kron does not promise exactly-once side effects.
External effects such as emails, payments, webhooks, and database writes must still be idempotent.
Safety
Scheduled functions can perform real side effects: send emails, write databases, call APIs, create cloud resources, or charge users. Treat scheduled callbacks like production code.
- Make callbacks idempotent.
- Keep external effects guarded by application-level idempotency keys.
- Test timers with short local schedules before using real schedules.
- Use embedded mode for v0.1 production experiments.
- Keep experimental server mode away from critical workloads until the distributed test matrix is stronger.
Experimental Server Mode
The stable path for v0.1 is embedded Python. Kron also includes an experimental standalone server mode for serializable worker tasks.
Do not use server mode for critical production workloads yet. It still needs more 3-node failure testing, leader redirect hardening, and a production-grade Raft storage backend.
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-secret
Server mode is for serializable worker tasks, not embedded Python callbacks.
import kron
client = kron.Client("http://127.0.0.1:7379", token="dev-secret")
client.schedule(
"email_digest",
every="30m",
task="send_digest",
payload={"list": "daily"},
)
worker = kron.Worker("http://127.0.0.1:7379", token="dev-secret")
@worker.task("send_digest")
def send_digest(payload):
print(payload)
worker.run()
Distributed mode uses OpenRaft for committed state, leader election, log replication, and membership. The current file-backed Raft store is intended for alpha testing, not long-term production storage.
Every claimed run has:
X-Kron-Timer: email_digest
X-Kron-Run: run_01J...
X-Kron-Idempotency-Key: email_digest:2026-06-10T08:00:00Z
X-Kron-Fencing-Token: 42
Project Status
Kron is alpha software moving toward a credible v0.1.
The primary product today is embedded Python scheduling backed by the Rust core. That path is the most mature part of the project and is suitable for experimentation, local tools, small services, and early adopters who can accept alpha storage compatibility.
Working today:
- Rust core engine;
- Python embedded API with PyO3;
- non-blocking
kron.start(); - controlled shutdown;
- data directory locking;
- append-only event log;
- snapshot and compaction;
- CLI observe/admin commands;
- local IPC with token auth;
- Python callback execution and retry;
- Python callback context with
timer_idandrun_id; - Python asyncio wrapper API for non-blocking use in async apps;
- local crash recovery for persisted timer metadata;
- wheel build and clean wheel import checks;
- experimental OpenRaft-backed server mode;
- Python
ClientandWorkerAPIs for server mode; - single-node distributed client/worker roundtrip test;
- 3-node distributed smoke test covering join, timer replication, and follower write rejection;
- 3-node leader failover test covering leader kill, new leader election, and writes after failover;
- worker recovery test covering abandoned run reclaim after leader kill and lease expiry;
- majority-continuation test after follower loss;
- stale completion rejection tests after lease expiry and after a replacement worker succeeds;
- segmented OpenRaft storage tests for reopen, purge, truncate, legacy-store rejection, corrupted records, and truncated final records;
- BSD 3-Clause license.
Distributed mode status:
- uses OpenRaft for committed state, leader election, log replication, and membership;
- has token-authenticated public and internal Raft HTTP endpoints;
- supports serializable worker tasks and JSON payloads;
- has fencing tokens for claimed runs;
- has a real 3-node subprocess smoke test;
- has a real leader-kill failover test;
- has a worker recovery test after leader kill and lease expiry;
- has a majority-continuation test after follower loss;
- rejects stale worker completions with committed fencing validation;
- uses a segmented file-backed OpenRaft store with manifest, checksummed log records, and deterministic tail-truncation handling;
- remains experimental, not production-ready.
Still not mature enough for enterprise production:
- full network partition tests are still missing;
- client/CLI/worker leader redirect is still basic;
- Raft storage is segmented and crash-tested at unit level, but not yet a long-running enterprise log store;
- no stable storage compatibility promise yet;
- no native TLS/mTLS yet; use the documented reverse-proxy or service-mesh deployment model;
- async Python callbacks are not supported yet;
- PyPI publication still requires running the release workflow with a real PyPI project;
Current honest claim:
Kron embedded mode is the primary alpha product. Distributed mode is an OpenRaft-backed experimental server with early 3-node validation, not yet an enterprise production scheduler.
Development
See CONTRIBUTING.md for contribution guidelines.
Run Rust tests:
cargo test --workspace
Run linting:
cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings
Run Python tests:
.venv/bin/python -m pytest -q tests/python
Build a Python wheel:
.venv/bin/maturin build --release
Check a release artifact:
.venv/bin/pip install twine
.venv/bin/twine check target/wheels/*
Publish manually:
.venv/bin/maturin publish
Repository Layout
crates/kron-core Rust engine, log, scheduler, IPC, OpenRaft adapter
crates/kron-cli CLI for observe/admin/server mode
crates/kron-py Python bindings via PyO3
docs/ design notes, ADRs, usage docs
examples/ small runnable Python examples
tests/python Python integration tests
What Kron Is Not
Kron is not a workflow engine.
Kron does not have DAGs.
Kron does not replace Temporal, Airflow, or Prefect.
Kron does not require Redis, RabbitMQ, Postgres, or Kubernetes.
Kron does one thing:
It makes application time reliable, observable, and coordinated.
Design Docs
- ADR 0001: Embedded Time Engine First
- Kron v0 Engine Design
- Multiprocess IPC
- Snapshot and Compaction
- Storage Format
- Python Usage
- CLI Usage
- Security Guide
- Release Checklist
Community Files
License
Kron is released under the BSD 3-Clause License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kron_scheduler-0.1.0.tar.gz.
File metadata
- Download URL: kron_scheduler-0.1.0.tar.gz
- Upload date:
- Size: 83.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4df3b11bfeb195115c41adfbf5b457466b30eca8667e632c26a719cb12ec89c2
|
|
| MD5 |
b29feb57d07b1e19f7acb0f26dc629eb
|
|
| BLAKE2b-256 |
0877afc626fe728f054d5f6be2eaea99b4cc2fdb21f0437a935449fdc3b5e347
|
File details
Details for the file kron_scheduler-0.1.0-cp312-cp312-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: kron_scheduler-0.1.0-cp312-cp312-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 1.3 MB
- Tags: CPython 3.12, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0f8340d2b487a334c72505c471e6ad1f56cc3c6666afdd86039bf06d851d4296
|
|
| MD5 |
5513ed7a7db151c62cfd307c5369cebd
|
|
| BLAKE2b-256 |
cebe53fb11529b861ff94ce2a5854313ebf8de876c090378f3af1549d1cc2ea0
|
File details
Details for the file kron_scheduler-0.1.0-cp312-cp312-macosx_11_0_arm64.whl.
File metadata
- Download URL: kron_scheduler-0.1.0-cp312-cp312-macosx_11_0_arm64.whl
- Upload date:
- Size: 985.5 kB
- Tags: CPython 3.12, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dfdcaa5cf59ff8e725ded0d6c934fe33372edfa606ed6d516d5fc210cbd06baf
|
|
| MD5 |
fe4c6a84bdb19ac3c314e21a8526a988
|
|
| BLAKE2b-256 |
bce67d9907fc0743a16a9eb5f4804b2067c443522c3276f241b68eede17fb8df
|