Rust Cron Scheduling — A Complete Guide
Rust's scheduling crates are split by execution model: `cron` parses expressions, `tokio-cron-scheduler` runs jobs in an async runtime, `apalis` adds Redis-backed durability, and `clokwerk` covers simpler in-process needs. This guide compares all four with code samples and the patterns that matter in production Rust services.
Updated
The crate landscape
| Crate | What it does | Best for |
|---|---|---|
| cron | Cron expression parsing, next-run computation | Custom schedulers, validation, displaying next-runs |
| tokio-cron-scheduler | Async scheduler for Tokio runtime | Most async Rust services |
| apalis | Redis-backed durable job queue | Restart-resilient distributed scheduling |
| clokwerk | Simple sync scheduler with English DSL | Sync apps, simple needs |
| job_scheduler | Sync in-process cron scheduler | Older crate; new code should use tokio-cron-scheduler |
| cron_clock | Calendar-based scheduling with timezone support | Specialized timezone scheduling |
For most modern Rust web services (axum, actix-web, rocket, all async): tokio-cron-scheduler. For background batch processing with Redis/PostgreSQL: apalis. Everything else is niche.
cron
Pure cron expression parsing and next-run computation. Doesn’t run anything.
[dependencies]
cron = "0.12"
chrono = "0.4"
use cron::Schedule;
use std::str::FromStr;
use chrono::Utc;
// 7-field syntax: sec min hour day month weekday year
let expr = "0 */5 * * * * *"; // every 5 minutes
let schedule = Schedule::from_str(expr).unwrap();
// Next 10 fire times
for next in schedule.upcoming(Utc).take(10) {
println!("{}", next);
}
// Validate a user-submitted expression
if Schedule::from_str(user_input).is_ok() {
// safe
}
The Rust cron crate uses 7-field syntax (seconds at the front, year at the end), not standard 5-field Unix. This is the most common gotcha — porting a Unix expression like */5 * * * * requires adding 0 for seconds and * for year: 0 */5 * * * * *.
Use the cron crate when you need to:
- Validate cron expressions in a config file or API
- Display next-run times in a UI
- Build your own scheduler on top of the cron parser
- Compute “how often” a schedule fires (count
.upcoming()over a date range)
tokio-cron-scheduler
The async scheduler most Rust web services reach for.
[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-cron-scheduler = "0.13"
use tokio_cron_scheduler::{Job, JobScheduler};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let sched = JobScheduler::new().await?;
// Cron-style — 6-field with seconds
sched.add(Job::new_async("0 */5 * * * *", |_uuid, _lock| {
Box::pin(async move {
refresh_cache().await;
})
})?).await?;
// Or with timezone
sched.add(Job::new_async_tz(
"0 0 9 * * MON-FRI",
chrono_tz::America::New_York,
|_uuid, _lock| Box::pin(async move {
send_morning_digest().await;
}),
)?).await?;
// Or interval-based (no cron syntax)
sched.add(Job::new_repeated_async(
std::time::Duration::from_secs(60),
|_uuid, _lock| Box::pin(async move {
heartbeat().await;
}),
)?).await?;
sched.start().await?;
// Wait forever (or until shutdown signal)
tokio::signal::ctrl_c().await?;
sched.shutdown().await?;
Ok(())
}
Notes:
- 6-field syntax (with seconds at the front), unlike the
croncrate’s 7-field - Async by default — jobs return futures, runs cooperatively in the Tokio runtime
- Timezone-aware via
Job::new_async_tzwith achrono_tztimezone - Persistent storage behind feature flags:
postgres-storage,nats-storage. Default is in-memory. - Graceful shutdown via
JobScheduler::shutdown()
For axum/actix-web/rocket apps that need scheduled background work, drop this into your main and you’re done.
apalis
Redis-backed (or PostgreSQL-backed) job queue with cron-style scheduling.
[dependencies]
apalis = { version = "0.5", features = ["redis", "cron"] }
tokio = { version = "1", features = ["full"] }
use apalis::prelude::*;
use apalis::redis::RedisStorage;
use apalis::cron::{CronStream, Schedule};
use std::str::FromStr;
#[derive(Default, Debug, Clone)]
struct Reminder;
async fn send_reminder(_job: Reminder, _ctx: JobContext) -> Result<JobResult, JobError> {
println!("Sending reminders...");
Ok(JobResult::Success)
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let schedule = Schedule::from_str("0 */5 * * * *").unwrap();
Monitor::new()
.register_with_count(2, move |c| {
WorkerBuilder::new(format!("reminder-worker-{c}"))
.stream(CronStream::new(schedule.clone()).to_stream())
.build_fn(send_reminder)
})
.run()
.await
}
What apalis adds beyond tokio-cron-scheduler:
- Persistent storage (Redis, PostgreSQL, SQLite) — survives restarts
- Distributed by default — multiple workers across processes coordinate via the storage backend
- Retry policies, dead-letter handling
- Job middleware (logging, tracing, metrics)
Trade-off: more infrastructure (need Redis or Postgres) and more setup code. For services that already use Redis and need durable scheduled jobs, the trade is usually worth it.
clokwerk
A simpler sync scheduler with an English-like DSL. No async.
[dependencies]
clokwerk = "0.4"
use clokwerk::{Scheduler, TimeUnits, Job};
use std::time::Duration;
let mut scheduler = Scheduler::new();
scheduler.every(5.minutes()).run(|| {
println!("Every 5 minutes");
});
scheduler.every(1.day()).at("09:00").run(|| {
println!("Daily at 9 AM");
});
scheduler.every(1.day()).at("17:00")
.and_every(1.day()).at("17:30")
.run(|| println!("5 PM and 5:30 PM"));
// Run pending tasks in a loop
loop {
scheduler.run_pending();
std::thread::sleep(Duration::from_secs(10));
}
Use clokwerk only for:
- CLI tools or daemons that don’t already have an async runtime
- Quick scripts where pulling in Tokio feels excessive
- Sync codebases that would have to async-ify everything to use tokio-cron-scheduler
For anything serious in a modern Rust service, tokio-cron-scheduler is the right call — most Rust services are already async, and the async pattern composes better with other I/O.
Choosing the right one
- Need to parse cron expressions, not run jobs? →
croncrate - Async Rust service (axum/actix/rocket/etc.)? →
tokio-cron-scheduler - Need persistence + distributed workers + retry? →
apalis(Redis or PostgreSQL) - Sync CLI tool or daemon? →
clokwerk - Need SLA-grade reliability? → Move scheduling out of Rust. Use Kubernetes CronJob or AWS EventBridge Scheduler to invoke a Rust HTTP endpoint or run a Rust binary on a schedule.
For most production Rust services in 2026: tokio-cron-scheduler with the postgres-storage feature when you need restart resilience. Move to apalis if you need full distributed job-queue semantics. Decouple to Kubernetes CronJob if you need true scheduler-grade SLA.
Frequently asked questions
Which Rust scheduling crate should I use?
What cron syntax does the cron crate accept?
Does tokio-cron-scheduler survive restarts?
How do I shut down a Rust scheduler gracefully?
Can Rust schedulers run sub-second?
Related
Every 5 Minutes
`*/5 * * * *` — common Rust scheduled-job interval.
ToolCron Parser
Verify cron expressions before adding them to a scheduler.
GuideThe cron Crate
Rust's most-downloaded cron parser — the 7-field format and computing run times.
Guidetokio-cron-scheduler
Deep dive on the async scheduler most Rust services reach for.
Guideapalis: Durable Jobs in Rust
Storage-backed, retry-resilient scheduled jobs in Rust.
GuideGo Cron Scheduling
The Go equivalents — robfig/cron, go-co-op/gocron.
GuideNode.js Cron Jobs
The Node equivalents for comparison.
GuideKubernetes CronJob
Run scheduled Rust binaries as K8s CronJobs for production reliability.