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:
| Schedule | Unix (5-field) | cron crate (7-field) |
|---|---|---|
| Every 5 minutes | */5 * * * * | 0 */5 * * * * * |
| Every hour | 0 * * * * | 0 0 * * * * * |
| 9 AM daily | 0 9 * * * | 0 0 9 * * * * |
| Weekdays at 9 AM | 0 9 * * 1-5 | 0 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 (JAN–DEC) and weekdays (SUN–SAT). 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+upcomingonly 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 anOption. A schedule can legitimately have no future run (e.g. a year in the past), so handleNonerather than unwrapping blindly.- Recomputing in a hot loop.
upcomingrecomputes 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?
Does the cron crate run scheduled jobs?
How do I get the next run time from a cron expression in Rust?
cron crate vs tokio-cron-scheduler — what's the difference?
Related
Every 5 Minutes
`0 */5 * * * * *` in the cron crate's 7-field form.
PatternEvery 5 Seconds
`*/5 * * * * * *` — sub-minute, which the crate's seconds field makes possible.
ToolCron Parser
Check what an expression fires — before you wire it into Rust.
GuideRust Cron Scheduling
The full landscape — cron, tokio-cron-scheduler, apalis, clokwerk, and when to pick each.
Guidetokio-cron-scheduler
The async scheduler that runs jobs — pair it with the cron crate's parsing, or use its own.
Guideapalis: Durable Jobs in Rust
When you need restart-resilient, storage-backed scheduled jobs.