Clokwerk: Interval Scheduling in Rust
`clokwerk` schedules recurring Rust tasks with a readable, fluent DSL — `every(10.minutes()).at("3:00 pm")` — instead of cron expressions. Inspired by Python's `schedule`, it's the crate to reach for when you'd rather write intervals in plain English than parse a five-field string. This guide covers the sync and async schedulers, the interval DSL, timezones, and when clokwerk is the right call over a cron-based scheduler.
Updated
What clokwerk is
clokwerk is an in-process task scheduler built around a fluent interval DSL rather than cron strings. It’s a direct port of the idea behind Python’s schedule and Ruby’s clockwork: you describe when in readable method calls, and clokwerk runs your closure when the time comes.
scheduler.every(10.minutes()).run(|| println!("tick"));
scheduler.every(1.day()).at("3:00 pm").run(|| println!("daily report"));
That readability is the reason to choose it. If you’re already thinking in cron expressions — or you need seconds-level precision, a year field, or per-job timezones encoded in the schedule — a cron scheduler like tokio-cron-scheduler is the better fit. clokwerk deliberately trades cron’s expressiveness for a schedule that reads like a sentence.
One practical note: clokwerk’s last release predates 2023, so it isn’t actively developed. It’s small, stable, and still heavily downloaded — fine for simple recurring work — but if you want a crate under active maintenance, weigh that against tokio-cron-scheduler.
Add it to your project
[dependencies]
clokwerk = "0.4"
The async scheduler is included in the default features, so you don’t need to enable anything extra to use AsyncScheduler. clokwerk pulls in chrono for its time handling.
Scheduling tasks
Create a Scheduler, register tasks with every(...).run(...), then drive it — either by calling run_pending() yourself in a loop, or by handing it to a background thread:
use clokwerk::{Scheduler, TimeUnits};
use clokwerk::Interval::*; // Monday, Tuesday, … Weekday, Weekend
use std::thread;
use std::time::Duration;
fn main() {
let mut scheduler = Scheduler::new();
scheduler.every(10.minutes()).run(|| println!("every 10 minutes"));
scheduler.every(1.day()).at("3:00 pm").run(|| println!("daily at 3 pm"));
scheduler.every(Wednesday).at("14:20:17").run(|| println!("weekly on Wednesday"));
// Option A — drive it yourself:
loop {
scheduler.run_pending();
thread::sleep(Duration::from_millis(100));
}
}
run_pending() executes any tasks whose time has come and returns immediately. The sleep sets your resolution — 100 ms means a task can fire up to 100 ms late, which is fine for minute- and hour-scale schedules.
If you’d rather not own the loop, watch_thread spawns the polling loop for you and returns a handle:
let handle = scheduler.watch_thread(Duration::from_millis(100));
// … your program runs …
handle.stop(); // also stops when the handle is dropped
The interval DSL
The DSL comes from the TimeUnits trait (.seconds(), .minutes(), .hours(), .days(), .weeks()) and the Interval enum (weekday variants). You compose them into a schedule:
| Schedule | clokwerk | cron equivalent |
|---|---|---|
| Every 10 minutes | every(10.minutes()) | */10 * * * * |
| Every 10½ minutes | every(10.minutes()).plus(30.seconds()) | (not expressible) |
| Daily at 3:00 pm | every(1.day()).at("3:00 pm") | 0 15 * * * |
| Weekdays only | every(Weekday).at("9:00 am") | 0 9 * * 1-5 |
| Multiple days | every(Tuesday).at("14:20").and_every(Thursday).at("15:00") | two expressions |
A few things cron can’t do as cleanly show up here: .plus(...) for compound intervals like “every 10 minutes and 30 seconds”, and and_every(...) to chain different days onto one task. Conversely, anything needing the full cron grammar — “the last weekday of the month”, L/W/# — is out of scope; that’s cron-scheduler territory. See the Rust cron guide for the whole spectrum.
Async tasks
For async work, swap Scheduler for AsyncScheduler. The .run() closure returns a future, and you drive the scheduler with run_pending().await — usually from a spawned task so it runs alongside the rest of your service:
use clokwerk::{AsyncScheduler, TimeUnits};
use std::time::Duration;
#[tokio::main]
async fn main() {
let mut scheduler = AsyncScheduler::new();
scheduler.every(10.minutes()).run(|| async {
// .await your async work here
println!("async tick");
});
tokio::spawn(async move {
loop {
scheduler.run_pending().await;
tokio::time::sleep(Duration::from_millis(100)).await;
}
});
// … run your HTTP server or main loop here …
}
AsyncScheduler works with both Tokio and async-std, so it drops into whichever runtime your service already uses. Because the closures are futures, tasks that overlap in time run concurrently rather than blocking each other — a real advantage over the sync scheduler for I/O-bound jobs.
Timezones
By default, clokwerk interprets .at("3:00 pm") in the local timezone of the machine. To pin a schedule to a specific zone regardless of where the process runs, construct the scheduler with with_tz:
use clokwerk::Scheduler;
use chrono::Utc;
// All .at(...) times are now interpreted as UTC:
let mut scheduler = Scheduler::with_tz(Utc);
// Or a named zone, with chrono-tz:
// let mut scheduler = Scheduler::with_tz(chrono_tz::America::New_York);
Pinning a zone is the safe default for anything user-facing — it keeps “3 pm” meaning 3 pm through daylight-saving changes, instead of drifting with the server’s local clock. The same with_tz constructor exists on AsyncScheduler.
clokwerk vs cron schedulers
clokwerk and cron-based schedulers overlap, but they optimize for different things:
| clokwerk | tokio-cron-scheduler | |
|---|---|---|
| Schedule syntax | Fluent interval DSL | Cron expressions (6-field) |
| Sub-minute precision | Down to seconds via .plus() | Yes, seconds field |
| Async | AsyncScheduler (Tokio / async-std) | Async-native (Tokio) |
| Readability | Reads like a sentence | Compact but cryptic |
Complex rules (L, #, ranges) | No | Yes |
| Maintenance | Stable, not actively developed | Actively maintained |
| Persistence | In-memory only | In-memory; optional Postgres/NATS |
Rule of thumb: if your schedules are plain intervals and you value readability, clokwerk is a pleasant, tiny dependency. The moment you need cron’s full grammar, seconds-level cron precision, or an actively maintained crate, move to tokio-cron-scheduler — or apalis if the jobs also need durability and retries. For the full comparison of Rust scheduling options, see the Rust cron scheduling guide.
Pitfalls
- It’s not cron.
every(5.minutes())is not*/5 * * * *aligned to the wall clock — clokwerk schedules relative to when the task was registered (and last ran), so “every 5 minutes” means five minutes after the last run, not at :00, :05, :10. If you need clock-aligned firing, use a cron scheduler. - Nothing runs until you drive it. Registering a task does nothing on its own; you must call
run_pending()in a loop (or usewatch_thread/ a spawned async loop). If tasks never fire, that’s almost always why. - Resolution is your sleep interval. A task can fire as late as your
sleepduration. Keep it small (100 ms) for punctual jobs, larger to save CPU when timing is loose. - Local timezone by default.
.at(...)uses the machine’s local zone unless you build withwith_tz. Pin the zone explicitly for anything that must fire at a specific wall-clock time. - Not maintained, not persistent. clokwerk hasn’t shipped a release since before 2023 and holds schedules in memory — a restart forgets everything. For important, must-not-drop jobs, use a durable framework like apalis.
For the full comparison of Rust scheduling crates and when to reach for each, see the Rust cron scheduling guide.
Frequently asked questions
Does clokwerk use cron expressions?
Is clokwerk still maintained?
How do I run async tasks with clokwerk?
clokwerk or tokio-cron-scheduler — which should I use?
Related
Every 10 Minutes
`every(10.minutes())` in clokwerk, `*/10 * * * *` in cron.
PatternDaily at 9 AM
`every(1.day()).at("9:00 am")` — the kind of schedule clokwerk reads best.
ToolCron Builder
Build a cron expression visually — for when you switch to a cron scheduler.
GuideRust Cron Scheduling
The full landscape — cron, tokio-cron-scheduler, apalis, clokwerk, and when to pick each.
Guidetokio-cron-scheduler
The async scheduler to use when you do want cron expressions, not intervals.
GuideThe Rust cron Crate
Parse and compute cron times directly, if intervals aren't enough.