cronoq for Python
An elastic, priority-aware task scheduler for asyncio, powered by a Rust engine. cronoq decides when your coroutines run, in what order and with how much concurrency: priorities with aging, deadlines, fixed start times and cron, dependencies, retries with backoff, shared rate limits, and autoscaling, all in-process, without extra infrastructure.
pip install cronoq
Why
- Important work first, nothing starves. Four priority classes with aging; deadlines raise urgency as they approach.
- Respect rate limits. Token buckets and semaphores shared across pools gate dispatch, so a
burst never turns into a 429 storm;
Retryable(retry_after=...)honoursRetry-After. - Scheduled jobs that start on time. Fixed-start tasks reserve capacity and get a worker held for them; cron schedules with time zones.
- Pipelines without glue code. Dependencies by key with success/failure/completion edges, atomic batches, idempotent keys.
- Elastic concurrency. Pools scale between
minandmaxfrom queue pressure and wait-time objectives. - Fast. The engine makes about a million scheduling decisions per second; payloads stay Python objects in memory, and are serialized only when a durable store is configured.
Quickstart
import asyncio
import cronoq
async def call_llm(ctx: cronoq.TaskContext, prompt: str) -> str:
# raise cronoq.Retryable("429", retry_after=2.0) to back off,
# raise cronoq.Permanent("bad input") to fail without retrying
await asyncio.sleep(0.1)
return f"answer to {prompt!r}"
async def main() -> None:
pools = [cronoq.Pool("llm", call_llm, min=1, max=16)]
limits = [cronoq.Resource.token_bucket("openai", rate=50.0, burst=100)]
async with cronoq.Scheduler(pools, resources=limits) as sched:
chat = await sched.submit("hi", pool="llm", priority="high", deadline_in=2.0,
resources={"openai": 1})
batch = [await sched.submit(f"doc {i}", pool="llm", priority="low",
resources={"openai": 1}) for i in range(10)]
print(await chat)
print(len([await h for h in batch]), "batch items done")
print((await sched.snapshot()).pool("llm"))
asyncio.run(main())
- A scheduler is bound to the event loop that started it: use
async with cronoq.Scheduler(...)(orawait sched.start()/await sched.shutdown()) inside that loop. If the loop closes without a shutdown, the scheduler stops and further calls raisecronoq.SchedulerError. Leavingasync withwaits up toshutdown_graceseconds (default 5) for running attempts. - Executors are
async def run(ctx, payload)coroutines and run on that loop.ctxhasid,attempt,kind,key,pool,tags,timeout,deadline(Unix seconds),is_cancelled()andheartbeat(). - Plain (sync) functions are accepted too and run in the loop's default thread pool. A thread
cannot be interrupted: on timeout or cancellation the attempt is aborted, but the function
keeps running until it returns, and the next attempt may overlap with it. Check
ctx.is_cancelled()in long-running sync executors. - Payloads and results are arbitrary Python objects. They are serialized only when the scheduler has a durable store (see Durable state).
- Tasks without
pool=go to the scheduler'sdefault_pool: the pool named"default", or the first pool. cronoq.Retryable/cronoq.Permanentcontrol retries; any other exception is retryable. Awaiting a task that did not succeed raisescronoq.TaskFailed(.status,.message). All cronoq exceptions can be pickled.- Cancelling a task, a timeout, or a lost lease cancels the executor's asyncio task. A
CancelledErrorthe executor raises on its own (not caused by cronoq) is a retryable failure. An executor that swallowsCancelledErrorkeeps running and may overlap with the retry. - Invalid durations or configuration (negative, NaN, out of range) raise
ValueError. cronoq.compat.ElasticPriorityScheduleris a synchronous, thread-safe API for code written againstElasticPriorityScheduler.- Wheels use the stable ABI (
abi3, CPython 3.14+). Free-threaded (no-GIL) CPython builds are not supported.
Status events
sched.events() is an async iterator of cronoq.StatusEvent (id, pool, key, status,
attempt, at in Unix seconds, message). It delivers terminal events (succeeded,
failed, dead_lettered, cancelled, ...) by default, and every transition (ready,
running, retrying, ...) when the scheduler is created with emit_transitions=True.
async with sched.events() as events: # or: events = sched.events(); ...; events.close()
async for e in events:
print(e.id, e.status, e.message)
- Each stream buffers up to
event_capacityevents (aSchedulerargument, default 1024). When the consumer falls behind, the oldest events are dropped and counted inevents.lagged. Withsched.events(on_lag="raise")the next iteration raisescronoq.EventsLagged(.skipped); iterating again continues with the buffered events. - A stream ends (
StopAsyncIteration) when the scheduler shuts down or the stream is closed.
Durable state
Scheduler(pools, store=cronoq.SqliteStore("state.db")) keeps queued work, schedules and
results in a SQLite file across restarts. Payloads and results are stored as JSON unless you pass
codec=; several processes can share the file, with one leading and the others standing by.
More: Python quickstart · use cases · API reference.
Client and remote workers for cronoq-server
pip install "cronoq[client]"
The extra installs grpcio, which has no prebuilt wheel for Windows on arm64.
import os
from cronoq.client import CancelTask, Client, Reject, Worker, task
# Submit and inspect (client token; the server requires tokens of at least 16 characters).
async with Client("localhost:7070", token=os.environ["CRONOQ_CLIENT_TOKEN"]) as c:
sub = await c.submit({"prompt": "hi"}, pool="llm", priority="high",
deadline_in=30, timeout=60, key="job-1")
result = await c.wait(sub.id, timeout=120) # Result(id, status, output, message, ...)
print(result.status, result.json())
done = await c.submit("quick", wait=True, wait_timeout=10) # submit and wait in one call
info = await c.get("job-1") # TaskInfo: status, pool, key, kind, attempt, timestamps, ...
await c.submit_batch([task("a", key="a"), task("b", key="b", after=["a"])])
async for event in c.watch(pools=["llm"], keys=["job-1"], all_transitions=True):
... # every transition of job-1 (default: terminal statuses only)
# Serve a pool (worker token).
async def handle(lease): # lease.payload / .json() / .text(), .attempt, .pool, .tags, .cancelled
if not can_handle(lease):
raise Reject("missing model") # hand the lease back unrun
if obsolete(lease):
raise CancelTask("superseded") # end the task as cancelled
return {"answer": 42} # bytes, str, JSON-serialisable value or None
worker = Worker("localhost:7070", "llm", handle, slots=8,
token=os.environ["CRONOQ_WORKER_TOKEN"])
await worker.run() # reconnects with backoff; `await worker.stop()` drains
- Payloads:
bytesare sent as-is,stras UTF-8 text, other values as JSON (application/json). Outputs are encoded the same way and carry their content type. - Handlers raise
cronoq.Retryable(msg, retry_after=...)orcronoq.Permanent(msg); any other exception is a retryable failure. An output larger than the server'smax_message_bytesis a permanent failure. Reject(reason)gives the lease back: the server offers it, within the same attempt, to another worker of the pool with a free slot; if there is none, the attempt fails as retryable and counts against the retry budget.CancelTask(reason)ends the task ascancelled.- The worker speaks protocol version 3. The server may grant fewer slots than requested
(
worker.granted_slots, capped bylimits.max_slots_per_worker); leases beyond the grant are handed back. When the server announces it is shutting down (worker.last_shutdown) the worker reconnects with backoff, or immediately when leadership moved to another node. - Each running lease is heartbeated every
lease.lease_ttl / 3; when the server aborts an attempt (timeout, cancellation, lease expiry) the handler task is cancelled andlease.cancelledis set. - Failed calls raise
cronoq.client.ClientErrorwith the gRPC status name in.code(e.g.unauthenticated,not_foundfromget/waiton an unknown task,resource_exhaustedwhen a pool is full,unavailablewhen the server is not the leader or is shutting down);.retryableis true for the last two.Client.waitandClient.submit(..., wait=True)raiseTimeoutErrorwhen the wait times out (the server caps waits atlimits.max_wait_s), as doesClient.submit(..., call_timeout=...). Client.snapshot()(metrics or admin token) returns per-pool figures,totalsandworkers_connected.ClientandWorkercan be created outside a running loop; they connect on first use.Worker.stop()can be called more than once (callers wait for the same drain), and a stopped worker can be run again. A handler that swallowsCancelledErrorkeeps running after an abort and may overlap with the retry of the same task.
Migrating from ElasticPriorityScheduler
from cronoq.compat import ElasticPriorityScheduler, Priority, Task
A synchronous, thread-safe drop-in for the original API. See the migration guide.
Development
python3 -m venv .venv
.venv/bin/pip install maturin pytest pytest-asyncio grpcio protobuf mypy
.venv/bin/maturin develop -m crates/cronoq-py/Cargo.toml
cd crates/cronoq-py && ../../.venv/bin/python -m pytest
../../.venv/bin/python -m mypy.stubtest cronoq._cronoq # stubs match the extension
python/tests/test_client.py starts a real cronoq-server (building it with cargo if needed,
or using CRONOQ_SERVER_BIN). The stubs in cronoq/_proto are generated by
scripts/gen_proto.sh. See CONTRIBUTING.md.
License
MIT OR Apache-2.0.
Metadata
Release files for cronoq 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| cronoq-0.2.0.tar.gz | 284.9 kB | With provenance | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| cronoq-0.2.0-cp314-abi3-win_arm64.whl | CPython 3.14 | abi3 | Windows ARM64 | Details |
| cronoq-0.2.0-cp314-abi3-win_amd64.whl | CPython 3.14 | abi3 | Windows x86-64 | Details |
| cronoq-0.2.0-cp314-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.14 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| cronoq-0.2.0-cp314-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.14 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| cronoq-0.2.0-cp314-abi3-macosx_11_0_arm64.whl | CPython 3.14 | abi3 | macOS 11.0+ ARM64 | Details |
| cronoq-0.2.0-cp314-abi3-macosx_10_12_x86_64.whl | CPython 3.14 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 16.6 MB
Release files / cronoq-0.2.0.tar.gz
| Download URL | cronoq-0.2.0.tar.gz |
|---|---|
| Size | 284.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c75d28912200b3ed928551cbb0c7e53f644ffab4427fb180df85463750d10767
|
|
BLAKE2b-256 checksum How to use checksums |
3c5f049b1024c5cdceaa49314b993fcfb2b3afe7fbf448d5e94027d6c93dea0e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / cronoq-0.2.0-cp314-abi3-win_arm64.whl
| Download URL | cronoq-0.2.0-cp314-abi3-win_arm64.whl |
|---|---|
| Size | 2.5 MB |
| Tags | CPython 3.14 Windows ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
593af66754590952c5d8ac133599c9c880b98bf840ce6b1eade619ffc9215f5f
|
|
BLAKE2b-256 checksum How to use checksums |
84af57c6315d887c0d3766b48cd6ee683715b24cb961ffaaa48afaea67a423af
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / cronoq-0.2.0-cp314-abi3-win_amd64.whl
| Download URL | cronoq-0.2.0-cp314-abi3-win_amd64.whl |
|---|---|
| Size | 2.7 MB |
| Tags | CPython 3.14 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
907bbe6d6f183fb03dcc2b4d05a956a750f25ad08054f10dee57adcbbc16f61c
|
|
BLAKE2b-256 checksum How to use checksums |
180cd7be5c83291111e9d684da37b5f74c92688486183bf70e3cb6867044a12f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / cronoq-0.2.0-cp314-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | cronoq-0.2.0-cp314-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 2.9 MB |
| Tags | CPython 3.14 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
35c70993fadfabdf01bfd1d21be1875b60abe8c676ad87cbfc06305a38e88d5f
|
|
BLAKE2b-256 checksum How to use checksums |
9a2028fe2d2ccf3711ab4bdfce693739a9f710b2d05d45c077b092a87f4431c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / cronoq-0.2.0-cp314-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | cronoq-0.2.0-cp314-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 2.8 MB |
| Tags | CPython 3.14 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
954b4ee273fd70d3bf23730e27ddb0919104a481d10390f327ab014cb35b3763
|
|
BLAKE2b-256 checksum How to use checksums |
348e6cbe53c8d09073e7e8c6fad96e4467c40c244f18c83880b9731164abac88
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / cronoq-0.2.0-cp314-abi3-macosx_11_0_arm64.whl
| Download URL | cronoq-0.2.0-cp314-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 2.6 MB |
| Tags | CPython 3.14 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
99777b2acf9da8d002c842f02bb095590239a7c7a60d4a60a1ec11c98533723c
|
|
BLAKE2b-256 checksum How to use checksums |
ea278c3e70ca8b6949c401f92489fbcb62ccc387375787701aae63d89410fe84
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / cronoq-0.2.0-cp314-abi3-macosx_10_12_x86_64.whl
| Download URL | cronoq-0.2.0-cp314-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 2.7 MB |
| Tags | CPython 3.14 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
c94f58fc8cea694bfc478245a6589811bbe30077fbac43c4e7d746fff67dd455
|
|
BLAKE2b-256 checksum How to use checksums |
5ad15fb1e69c78297fbd8327937175684ce24c10ffc2989dd879398824073f48
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logSecurity for cronoq 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 7 files in this release have been uploaded with provenance metadata.
Attestation for cronoq-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.