cronuru
Guide

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:

Scheduleclokwerkcron equivalent
Every 10 minutesevery(10.minutes())*/10 * * * *
Every 10½ minutesevery(10.minutes()).plus(30.seconds())(not expressible)
Daily at 3:00 pmevery(1.day()).at("3:00 pm")0 15 * * *
Weekdays onlyevery(Weekday).at("9:00 am")0 9 * * 1-5
Multiple daysevery(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:

clokwerktokio-cron-scheduler
Schedule syntaxFluent interval DSLCron expressions (6-field)
Sub-minute precisionDown to seconds via .plus()Yes, seconds field
AsyncAsyncScheduler (Tokio / async-std)Async-native (Tokio)
ReadabilityReads like a sentenceCompact but cryptic
Complex rules (L, #, ranges)NoYes
MaintenanceStable, not actively developedActively maintained
PersistenceIn-memory onlyIn-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 use watch_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 sleep duration. 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 with with_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?
No — that's the whole point of clokwerk. Instead of parsing a five- or seven-field cron string, you build schedules with a fluent DSL: `scheduler.every(10.minutes()).run(...)` or `scheduler.every(1.day()).at("3:00 pm").run(...)`. It's modelled on Python's `schedule` library. If you specifically need cron-expression parsing, use tokio-cron-scheduler or the cron crate instead.
Is clokwerk still maintained?
clokwerk's last release (0.4) predates 2023, so it isn't actively developed, but it's small, stable, and still widely downloaded — it does one thing and the API hasn't needed to change. For a simple interval scheduler that's fine; if you need ongoing updates or cron syntax, tokio-cron-scheduler is the more actively maintained choice.
How do I run async tasks with clokwerk?
Use `AsyncScheduler` instead of `Scheduler`. It's included by default and its `.run()` takes an async closure returning a future: `scheduler.every(10.minutes()).run(|| async { do_work().await; });`. You then drive it from a task that calls `scheduler.run_pending().await` in a loop, typically spawned with `tokio::spawn`. It works with both Tokio and async-std.
clokwerk or tokio-cron-scheduler — which should I use?
Use clokwerk when your schedules are simple intervals ("every 10 minutes", "every day at 3 pm") and you'd rather read a DSL than a cron string. Use tokio-cron-scheduler when you need real cron expressions, seconds-level precision, per-job timezones baked into the expression, or an actively maintained crate. Both run in-process; neither persists across restarts on its own.