Monitor Rust scheduled tasks: tokio-cron-scheduler and reqwest ping on Ok/Err
In a tokio-cron-scheduler async job closure, call your job
function and match the Result: on
Ok GET the ping URL; on Err POST the error
string to /fail. Spawn with tokio::task::spawn
to catch panics via the JoinHandle. Absence detection
handles OOM kills and crashed processes. Free, no monitor cap.
Your assistant can do this for you
Monitor my Rust programs with LastPing
On this page
The snippets
Create a monitor in
the console, set the
schedule plus a grace window, and add these crates to
Cargo.toml: tokio-cron-scheduler,
reqwest, tokio.
tokio-cron-scheduler: match on Ok/Err
// Cargo.toml:
// tokio = { version = "1", features = ["full"] }
// tokio-cron-scheduler = "0.13"
// reqwest = { version = "0.12", features = ["rustls-tls"] }
use reqwest::Client;
use tokio_cron_scheduler::{Job, JobScheduler};
const PING: &str = "https://ping.lastping.dev/<your-monitor-id>";
async fn ping(client: &Client, suffix: &str, body: Option<String>) {
let url = format!("{}{}", PING, suffix);
let result = if let Some(b) = body {
client.post(&url).body(b).send().await
} else {
client.get(&url).send().await
};
if let Err(e) = result {
eprintln!("lastping error: {e}");
}
}
async fn run_job() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// your job logic
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let sched = JobScheduler::new().await?;
let client = Client::new();
sched.add(Job::new_async("0 0 2 * * *", move |_uuid, _lock| {
let c = client.clone();
Box::pin(async move {
match run_job().await {
Ok(_) => ping(&c, "", None).await,
Err(e) => {
let msg = e.to_string();
let body = msg[..msg.len().min(10_000)].to_string();
ping(&c, "/fail", Some(body)).await;
}
}
})
})?).await?;
sched.start().await?;
tokio::signal::ctrl_c().await?;
Ok(())
}
Catch panics via JoinHandle
Rust panics in async tasks unwind the task but the scheduler
keeps running. Spawning the job body as a separate task and
awaiting the JoinHandle lets you detect a panic
and send a /fail ping:
sched.add(Job::new_async("0 0 2 * * *", move |_uuid, _lock| {
let c = client.clone();
Box::pin(async move {
let handle = tokio::task::spawn(async {
run_job().await
});
match handle.await {
Ok(Ok(_)) => ping(&c, "", None).await,
Ok(Err(e)) => {
let body = e.to_string()[..e.to_string().len().min(10_000)].to_string();
ping(&c, "/fail", Some(body)).await;
}
Err(join_err) => {
// task panicked
let body = format!("panic: {:?}", join_err);
ping(&c, "/fail", Some(body[..body.len().min(10_000)].to_string())).await;
}
}
})
})?).await?;
Plain tokio interval (no cron syntax)
If you don't need cron syntax, a tokio::time::interval
loop is simpler and avoids an extra crate. The same
match pattern applies:
use std::time::Duration;
use tokio::time;
#[tokio::main]
async fn main() {
let client = Client::new();
let mut interval = time::interval(Duration::from_secs(24 * 3600));
interval.tick().await; // skip the immediate first tick
loop {
interval.tick().await;
match run_job().await {
Ok(_) => ping(&client, "", None).await,
Err(e) => {
let msg = e.to_string();
ping(&client, "/fail",
Some(msg[..msg.len().min(10_000)].to_string())).await;
}
}
}
}
Why OOM kills still need absence detection
Even Rust can be OOM-killed. The spawned task and the scheduler both vanish with the process; no async code runs. Absence detection is the correct safety net: no ping within schedule+grace → incident. See also the Go monitoring guide for a similar defer-based pattern, and the LastPing MCP server for AI-agent provisioning.
What this catches
The match reports what it can; the silence reports the rest.
- A returned Err: the
/failping opens an incident immediately with the error string stored. - A panic: caught via
JoinHandle::awaitreturningErr(JoinError); the payload is POST'd to/fail. - OOM kill or SIGKILL: no async code runs; the missing ping opens the incident after the grace window.
- The binary never started: missing cron entry, deployment failure. Absence → incident.
- Recovery: the next successful run closes the incident and sends a recovery notice automatically.
Questions people ask
Front-loaded answers: the most important fact first.
-
How do I monitor a Rust cron job?
Use
tokio-cron-scheduler. In the async job closure, call your job function and match onOk/Err: onOksend a GET tohttps://ping.lastping.dev/<your-monitor-id>withreqwest; onErrPOST the error string to the/failendpoint. -
What crates do I need?
tokio-cron-schedulerfor scheduling,reqwestfor HTTP, andtokiowith the full feature set. These are standard in the Tokio ecosystem. A plaintokio::time::intervalloop removes the scheduler dependency if cron syntax isn't needed. -
How do I catch panics in async Rust?
Tokio tasks can catch panics: spawn the job body with
tokio::task::spawnand await the returnedJoinHandle. If the handle returnsErr(JoinError)the task panicked; format the error and POST it to/fail. -
Can I use a different HTTP client?
Yes.
hyper,ureq(blocking), or the standard library'sstd::net::TcpStreamwith a manual HTTP request all work. The ping is a plain GET or POST: no special client required. -
What if the process is OOM-killed?
An OOM-killed process sends no ping. Absence-detection monitoring handles this: the missing ping opens an incident after the grace window without any working code on the failure side.
-
Is Rust job monitoring free?
Yes. LastPing is free for individuals with no monitor cap. See how it compares to Healthchecks.io and Cronitor.
Even Rust can be OOM-killed. Absence detection is the safety net.
One match arm now beats debugging a silent batch window later. First alert wired up in about a minute, and LastPing monitors itself the same way.