cronuru
Guide

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

CrateWhat it doesBest for
cronCron expression parsing, next-run computationCustom schedulers, validation, displaying next-runs
tokio-cron-schedulerAsync scheduler for Tokio runtimeMost async Rust services
apalisRedis-backed durable job queueRestart-resilient distributed scheduling
clokwerkSimple sync scheduler with English DSLSync apps, simple needs
job_schedulerSync in-process cron schedulerOlder crate; new code should use tokio-cron-scheduler
cron_clockCalendar-based scheduling with timezone supportSpecialized 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 cron crate’s 7-field
  • Async by default — jobs return futures, runs cooperatively in the Tokio runtime
  • Timezone-aware via Job::new_async_tz with a chrono_tz timezone
  • 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?cron crate
  • 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?
For parsing cron expressions: the `cron` crate. For running jobs in an async Tokio runtime: `tokio-cron-scheduler`. For Redis-backed durable jobs: `apalis`. For simple in-process scheduling without async overhead: `clokwerk`. Most modern Rust services pick `tokio-cron-scheduler` since they're already on Tokio.
What cron syntax does the cron crate accept?
The Rust cron crate uses a 7-field syntax: seconds, minutes, hours, day-of-month, month, day-of-week, year. This is closer to Quartz than to Unix. Standard 5-field Unix expressions need a leading `0` for seconds and a trailing `*` for year: `*/5 * * * *` becomes `0 */5 * * * * *`. Always double-check parsed expressions against the parser's expectations.
Does tokio-cron-scheduler survive restarts?
Not by default — schedules are in-memory. With the `postgres-storage` or `nats-storage` feature flag, you can persist schedules to PostgreSQL or NATS, which restores them after restarts. For simpler durability needs, apalis offers Redis-backed jobs out of the box.
How do I shut down a Rust scheduler gracefully?
tokio-cron-scheduler returns a `JobScheduler` that has a `shutdown()` method. Call it from your shutdown signal handler before exiting. Combine with a `tokio::signal::ctrl_c()` handler for SIGINT/SIGTERM. For apalis, the worker's `Monitor` has built-in graceful shutdown support.
Can Rust schedulers run sub-second?
Most crates support sub-second only via `OnInterval`-style triggers, not cron expressions (cron's minimum is 1 second in 7-field crates, 1 minute in 5-field). For Tokio-based services, just use `tokio::time::interval(Duration::from_millis(100))` directly for high-frequency loops — it's more idiomatic than reaching for a scheduler.