Steda is a durable task queue for Rust applications that already depend on PostgreSQL. Tasks are ordinary async Rust handlers; PostgreSQL stores queues, attempts, retries, checkpoints, durable sleeps, cancellation state, and results so work can survive process restarts and worker failure.
The model is deliberately small:
- a
Taskdefines a stable name and typed input/output pair; - queues persist logical tasks and their attempts;
- workers declare which task definitions they can execute;
- checkpointed steps preserve successful work across retries;
- durable sleeps suspend work without occupying a worker;
- idempotency keys make repeated external delivery safe to submit.
Installation
Steda requires PostgreSQL 18+. See MSRV for supported Rust versions.
Download steda.sql from the matching Steda release and apply it to your PostgreSQL database before starting producers or workers.
When upgrading Steda, apply the steda.sql file from the new release again before starting binaries built against that release. Database upgrades remain compatible within a major release line. A future major release may introduce breaking storage changes and require an explicit migration procedure, such as draining workers or running a one-time upgrade script; any such requirements will be documented in the release notes. Mixed-version deployments are not guaranteed unless a release explicitly states otherwise.
Features
Steda has no default features. TLS support for Steda::connect is opt-in:
tls-rustlsenables the SQLx rustls backend;tls-native-tlsenables the SQLx native TLS backend.
# or
Quick start
The example below also uses Serde derives and the Tokio runtime:
Define a task:
use ;
use ;
const RESIZE_IMAGE: = new;
Create a queue and register a handler:
let queue = steda.queue?;
queue.create.await?;
let worker = queue
.worker
.concurrency
.task
.build?;
let _worker = spawn;
Producers use the same task definition:
let task = queue
.spawn
.await?;
let output = task.result.await?;
println!;
The returned task keeps the output type attached, so results and snapshots remain typed. task.task_ref() produces a serializable reference that keeps the queue, task name, task ID, and Rust input/output types together across restarts. Producer and worker processes share the same Task constant and PostgreSQL state; no runtime task registry is required.
Durable workflows
Transactional task mutations
Awaiting queue.spawn(...) submits through the queue's shared pool. When application state and
durable work must commit atomically, submit the same builder through a caller-owned SQLx
transaction:
let mut tx = pool.begin.await?;
// Write application state through `tx`.
let task = queue
.spawn
.submit
.await?;
tx.commit.await?;
The task becomes visible when the transaction commits. Rolling the transaction back also rolls
back the task spawn. Explicit cancellation and manual retry can participate in the same application
transaction with task.cancel_in(&mut tx) and task.retry_in(&mut tx).
Idempotent submission
Queue-scoped idempotency keys make repeated external delivery safe to submit:
let task = payments
.spawn
.idempotency_key
.await?;
Replaying the same request returns the existing logical task. Reusing the key for a different
request returns Error::IdempotencyConflict. The key remains reserved only while that logical
task is retained; retention cleanup removes the task and releases its idempotency key.
Retries
Tasks can use bounded retry policies with configurable backoff:
use Duration;
use RetryStrategy;
let task = queue
.spawn
.max_attempts
.retry_strategy
.await?;
max_attempts includes the first attempt. Without explicit configuration, Steda defaults to five attempts with exponential backoff.
Checkpointed steps
TaskContext::step persists a successful result under a typed stable identity:
use Step;
const RESERVE_INVENTORY: = new;
let reservation = ctx
.step
.await?;
If the task runs again, Steda replays the stored result rather than executing the step body again.
See multistep_workflow for a complete task composed from several typed steps.
A checkpoint makes the Steda step replayable; it cannot make an external side effect exactly once. Use the external system's idempotency or fencing mechanism when that property is required.
Durable sleeps
A durable sleep persists its wake time and releases the worker claim:
use Duration;
use Sleep;
const SETTLEMENT_WINDOW: Sleep = new;
ctx.sleep_for.await?;
When the wake time arrives, execution starts again from the handler entry point. Earlier checkpoints and sleeps replay until execution reaches new work.
Durable compatibility
Task names, step names, sleep names, and their serialized input, output, and checkpoint values are persisted workflow contracts. Keep their serialization compatible while old work may still exist, or use a new stable task, step, or sleep name for an incompatible version.
Provisioned execution
TaskExecutor allows a worker to
execute a claimed attempt in a separate process, container, sandbox, Kubernetes Job, or remote
environment without introducing another task model.
The Steda worker still owns claiming, leases, cancellation, retries, checkpoints, and terminal state. Custom executors are responsible for terminating or fencing external work when their execution future is cancelled.
See provisioned_executor for a complete example.
Runnable examples
The repository contains standalone programs that use only Steda's public API. Point
DATABASE_URL at PostgreSQL and run any of them with Cargo:
| Example | Demonstrates |
|---|---|
basic_task |
Typed producer/worker flow and typed results |
idempotent_webhook |
Deduplicating repeated webhook delivery |
retrying_delivery |
Bounded retries after transient failures |
cancellation |
Explicit and deadline-driven cancellation |
multistep_workflow |
A task composed from several typed durable steps |
checkpointed_order |
Multi-step work replayed across a retry |
durable_delay |
Suspending without holding a worker claim |
cross_queue_workflow |
Parent/child work across separate queues |
provisioned_executor |
Fresh execution environments under the normal Steda worker/retry path |
metrics |
Exporter-agnostic queue and worker execution counters |
tower_layer |
Custom Tower middleware around registered executor invocation |
See examples/README.md for the full walkthrough.
Operations
Queues have persisted cleanup policies, workers use finite PostgreSQL leases, and Steda exposes exporter-agnostic queue and worker metrics. See the crate documentation for lifecycle, graceful shutdown, cleanup, metrics, middleware, and database-pool behavior.
Database roles
Steda uses PostgreSQL invoker privileges and creates queue storage dynamically. The simplest deployment uses one database role to run migrations, provision queues, and execute Steda operations. Deployments that separate migration, provisioning, and runtime roles must grant the required schema, function, and queue-table privileges explicitly; Steda does not install a least-privilege role split automatically.
Tower middleware
Task handler invocation can be wrapped at the root with ordinary Tower layers through
Steda::builder(pool). Middleware receives task metadata and TaskContext; PostgreSQL remains
authoritative for claims, leases, retries, cancellation, and terminal state.
See tower_layer for a complete example.
MSRV
The current MSRV (minimum supported Rust version) is 1.95.
Steda will keep a rolling MSRV policy of at least two versions behind the latest stable release (so if the latest stable release is 1.97, we would support 1.95).
Note that the MSRV is not increased automatically.
Contributing
Contributions to Steda are welcome. See the Contributing Guide for information on reporting bugs, proposing features, submitting pull requests, and the licensing terms that apply to contributions.
Security Policy
If you believe you have found a security vulnerability, please do not report it through GitHub Issues. See our Security Policy for reporting instructions.
Credit
Steda was inspired in part by Absurd, whose work helped shape parts of its durable execution model.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
This software includes third-party components subject to separate license terms. See THIRD_PARTY_NOTICES.md.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Steda by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.