apalis: Durable Scheduled Jobs in Rust
`apalis` is a background job framework for Rust — and `apalis-cron` lets you drive those jobs on a cron schedule. Unlike an in-process scheduler, apalis is built around storage backends (Redis, PostgreSQL, SQLite), retry policies, and Tower middleware, which makes it the right tool when scheduled work must survive restarts and failures. This guide is accurate to apalis 0.7.
Updated
What apalis is
apalis isn’t a cron scheduler — it’s a background job framework, in the same family as Sidekiq (Ruby) or Celery (Python). Jobs are produced by a backend and consumed by workers, with Tower middleware layered in for retries, timeouts, concurrency control, and tracing.
apalis-cron is one backend among several: instead of pulling jobs from a queue, it emits a job every time a cron schedule fires. That means you get cron triggering plus apalis’s durability and retry machinery — which is the whole reason to choose it over a plain in-process scheduler like tokio-cron-scheduler.
Add it to your project
[dependencies]
apalis = "0.7"
apalis-cron = "0.7"
tokio = { version = "1", features = ["full"] }
chrono = "0.4"
The latest stable line is 0.7 (a 1.0 release is in the release-candidate stage, so expect some API churn when it lands — pin your version).
A cron-driven worker
The message type is just a plain struct (it needs Default, Debug, and Clone). You parse a schedule, wrap it in a CronStream, and build a worker whose build_fn is your async handler:
use apalis::prelude::*;
use apalis_cron::{CronStream, Schedule};
use chrono::Local;
use std::str::FromStr;
#[derive(Default, Debug, Clone)]
struct Reminder;
async fn handle_tick(_job: Reminder, _ctx: CronContext<Local>, data: Data<usize>) {
// This runs each time the schedule fires.
println!("tick — shared data = {}", *data);
}
#[tokio::main]
async fn main() {
let schedule = Schedule::from_str("@daily").unwrap();
let worker = WorkerBuilder::new("morning-reminder")
.data(42usize) // shared state injected into the handler
.backend(CronStream::new(schedule))
.build_fn(handle_tick);
worker.run().await;
}
The handler receives three things: the job message, a CronContext<Local> (the firing time and timezone), and any Data<T> you registered with .data(...).
The cron format apalis uses
apalis-cron parses schedules with the Rust cron crate, which uses a 7-field, seconds-first layout — second minute hour day-of-month month day-of-week year — and also accepts shorthand macros.
This is a real difference from tokio-cron-scheduler (which is 6-field): apalis adds an optional trailing year field.
| Schedule | apalis (cron crate) | Macro |
|---|---|---|
| Every 5 minutes | 0 */5 * * * * * | — |
| Every hour | 0 0 * * * * * | @hourly |
| Midnight daily | 0 0 0 * * * * | @daily |
| 9 AM weekdays | 0 0 9 * * Mon-Fri * | — |
| Weekly (Sun 00:00) | 0 0 0 * * Sun * | @weekly |
If the year field is omitted, the cron crate accepts a 6-field form too, but writing it out keeps the intent explicit. Unsure what an expression does? Run it through the cron parser first.
Durability with storage
On its own, CronStream fires in memory — if the process dies mid-job, that run is gone. apalis’s answer is to route jobs through a storage backend so they’re persisted, acknowledged, and retried independently of any single process:
- apalis-redis — Redis-backed queue. Fast, simple, the common choice.
- apalis-sql — PostgreSQL, MySQL, or SQLite storage, if you’d rather not run Redis.
This is the architectural reason to pick apalis: a scheduled job becomes a durable unit of work with an at-least-once guarantee, instead of a fire-and-forget tick. For jobs you genuinely cannot drop — invoicing, payouts, report generation — that durability is the point. See the apalis documentation for wiring a storage backend to a worker, as the exact API is evolving toward 1.0.
Retries and middleware
Because apalis is built on Tower, resilience is a layer you add rather than code you write. A retry policy, for instance:
use apalis::{prelude::*, layers::retry::RetryPolicy};
let worker = WorkerBuilder::new("morning-reminder")
.retry(RetryPolicy::retries(5)) // retry a failing run up to 5 times
.data(42usize)
.backend(CronStream::new(schedule))
.build_fn(handle_tick);
The same mechanism layers in timeouts, concurrency limits, rate limiting, and tracing spans. This is what you give up with a bare scheduler — and what makes apalis worth its extra weight for important work.
Running and shutting down
A single worker runs with worker.run().await. To run several workers together — and to get coordinated, graceful shutdown on SIGINT/SIGTERM — apalis provides a Monitor that you register workers on and run as a unit. Reach for Monitor once you have more than one worker or need to manage shutdown explicitly; check the current apalis docs for the exact builder API, which is changing ahead of 1.0.
In a web service, run the apalis worker (or monitor) and your HTTP server concurrently with tokio::join!, and make sure both stop on the same shutdown signal so in-flight jobs drain cleanly.
apalis vs tokio-cron-scheduler
They solve overlapping problems at different weights:
| tokio-cron-scheduler | apalis (+ apalis-cron) | |
|---|---|---|
| Model | In-process async scheduler | Background job framework |
| Cron format | 6-field (seconds-first) | 7-field (cron crate) + macros |
| Durability | In-memory; optional Postgres/NATS for schedules | Redis / SQL storage; durable per-run |
| Retries | Manual | Built-in middleware |
| Best for | Lightweight periodic tasks | Important jobs that can’t be dropped |
| Weight | Light | Heavier (storage, infra) |
Rule of thumb: if losing an occasional run on restart is fine, tokio-cron-scheduler is simpler. If a missed or failed run is a real problem, apalis’s durability and retries earn their keep. For the wider picture of Rust scheduling options, see the Rust cron scheduling guide.
Frequently asked questions
What is apalis in Rust?
How do I run a cron job with apalis?
What cron format does apalis use?
Does apalis survive restarts?
apalis or tokio-cron-scheduler — which should I use?
Related
Every 15 Minutes
A common interval for durable background jobs.
ToolCron Parser
Check what a cron expression does before scheduling it.
Guidetokio-cron-scheduler
The lighter in-process async scheduler, for comparison.
GuideRust Cron Scheduling
The full comparison of Rust scheduling crates.
GuideKubernetes CronJob
Running scheduled Rust workloads on Kubernetes instead.