cronuru
Guide

The Rust cron Crate

The `cron` crate is Rust's most-downloaded cron *parser* — it turns an expression into a `Schedule` and computes upcoming run times over a `chrono` timezone, but it deliberately does **not** run jobs for you. Its format is unusual too: 7 fields, with both a seconds field at the front and a year field at the end. This guide covers parsing, the field format, timezones, and how to pair it with an actual scheduler, accurate to version 0.17.

Updated

Add it to your project

The cron crate computes times over chrono datetimes, so you’ll almost always add both:

[dependencies]
cron = "0.17"
chrono = "0.4"

cron is the most-downloaded cron parser in the Rust ecosystem — but note that word. It reads expressions and tells you when they fire; it doesn’t run anything. See Actually running jobs below for how to close that gap.

Parse and compute run times

Parse an expression into a Schedule with from_str, then iterate upcoming times over a timezone:

use std::str::FromStr;
use cron::Schedule;
use chrono::Utc;

fn main() {
    // sec min hour day-of-month month day-of-week year
    let expression = "0 30 9 * * MON-FRI *"; // 9:30 AM on weekdays
    let schedule = Schedule::from_str(expression).unwrap();

    // The next 5 fire times, in UTC:
    for datetime in schedule.upcoming(Utc).take(5) {
        println!("-> {}", datetime);
    }

    // Just the next one:
    if let Some(next) = schedule.upcoming(Utc).next() {
        println!("next run: {next}");
    }
}

upcoming returns a lazy iterator, so .take(n) or .next() only computes as many times as you ask for. There’s also after(&datetime) if you want runs following a specific instant rather than “now.”

The 7-field format

This is the crate’s biggest surprise. Unlike Unix (5 fields) or even Spring and Quartz (6 fields), the cron crate uses 7 fields — seconds at the front and a year at the end:

┌───────────── second (0–59)
│ ┌─────────── minute (0–59)
│ │ ┌───────── hour (0–23)
│ │ │ ┌─────── day of month (1–31)
│ │ │ │ ┌───── month (1–12 or JAN–DEC)
│ │ │ │ │ ┌─── day of week (SUN–SAT or number)
│ │ │ │ │ │ ┌─ year (e.g. 2026, or *)
│ │ │ │ │ │ │
0 30 9 * * MON-FRI *

Porting from a standard 5-field crontab means adding a seconds field at the front and a year field at the end:

ScheduleUnix (5-field)cron crate (7-field)
Every 5 minutes*/5 * * * *0 */5 * * * * *
Every hour0 * * * *0 0 * * * * *
9 AM daily0 9 * * *0 0 9 * * * *
Weekdays at 9 AM0 9 * * 1-50 0 9 * * MON-FRI *
Every 5 seconds(not expressible)*/5 * * * * * *

Because the seconds field exists, the crate can express sub-minute schedules that Unix cron can’t — see every 5 seconds. Names work for both months (JANDEC) and weekdays (SUNSAT). When you don’t care about the year, use *. If you’re unsure what an expression fires, run it through the cron parser first.

Timezones with chrono

upcoming takes any chrono::TimeZone, so the timezone is chosen at call time, not baked into the expression. Pass Utc for UTC, or a chrono-tz zone to pin wall-clock time:

chrono-tz = "0.10"
use std::str::FromStr;
use cron::Schedule;
use chrono_tz::America::New_York;

let schedule = Schedule::from_str("0 0 9 * * MON-FRI *").unwrap();

// 9 AM New York time, regardless of where the process runs:
for dt in schedule.upcoming(New_York).take(3) {
    println!("-> {dt}");
}

Pinning a zone is the safe default for anything user-facing — it keeps “9 AM” meaning 9 AM through daylight-saving transitions, instead of drifting with the server’s clock.

Actually running jobs

The crate computes times; you decide how to act on them. The simplest loop sleeps until the next fire time and runs your work:

use std::str::FromStr;
use cron::Schedule;
use chrono::Utc;

let schedule = Schedule::from_str("0 */5 * * * * *").unwrap(); // every 5 minutes

loop {
    let Some(next) = schedule.upcoming(Utc).next() else { break };
    let wait = (next - Utc::now()).to_std().unwrap_or_default();
    std::thread::sleep(wait);      // or tokio::time::sleep(wait).await
    run_task();
}

This is fine for a single job. Once you have many jobs, overlap concerns, timezones per job, persistence, or graceful shutdown, stop hand-rolling the loop and use a real scheduler — tokio-cron-scheduler for async in-process work, or apalis for durable, retry-backed jobs. Both accept cron expressions directly, so you rarely need the cron crate and a scheduler together.

Pitfalls

  • 7 fields, not 5. Pasting a Unix expression parses wrong or errors. Add the leading seconds field and the trailing year — see the table above.
  • Day-of-week. Numeric day-of-week values differ between cron dialects, so prefer the unambiguous name forms (MON, TUE, …) — which is what the crate’s own docs use — to avoid off-by-one surprises.
  • It doesn’t run anything. Schedule + upcoming only compute times. If jobs aren’t firing, it’s because nothing is calling them — you need a loop or a scheduler.
  • upcoming(Utc).next() returns an Option. A schedule can legitimately have no future run (e.g. a year in the past), so handle None rather than unwrapping blindly.
  • Recomputing in a hot loop. upcoming recomputes from “now” each call; compute the next time once, sleep to it, then advance — don’t poll it in a tight spin loop.

For the full comparison of Rust scheduling crates and when to reach for each, see the Rust cron scheduling guide.

Frequently asked questions

What cron format does the Rust cron crate use?
The `cron` crate expects 7 fields: second, minute, hour, day-of-month, month, day-of-week, and year. That's two more than a standard Unix crontab (which has 5 and starts at minutes). A Unix expression like `*/5 * * * *` becomes `0 */5 * * * * *` — a leading `0` for seconds and a trailing `*` for year. Day names (MON-FRI) and month names (JAN-DEC) are supported.
Does the cron crate run scheduled jobs?
No. The `cron` crate is a parser and a time calculator only. `Schedule::from_str` parses an expression, and `.upcoming(timezone)` yields future `DateTime`s — but nothing executes your code. To actually run jobs you sleep until the next time and call your function yourself, or hand the schedule to a runtime like tokio-cron-scheduler or apalis.
How do I get the next run time from a cron expression in Rust?
Parse the expression into a `Schedule`, then take the first item from `.upcoming()`: `let next = Schedule::from_str(expr)?.upcoming(Utc).next();`. `.upcoming(Utc)` returns an iterator of upcoming `DateTime` values, so `.next()` gives the next fire time (as an `Option`) and `.take(n)` gives the next n.
cron crate vs tokio-cron-scheduler — what's the difference?
The `cron` crate only parses expressions and computes times; you write the loop that waits and runs work. tokio-cron-scheduler is a full async scheduler — it owns the timing loop, runs your closures on a Tokio runtime, and supports intervals, one-shot jobs, and optional persistence. Use the cron crate when you just need to reason about an expression; use tokio-cron-scheduler (or apalis) when you need something to actually fire jobs.