Skip to content
LastPing
GUIDE · RUST · FREE

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
  1. The snippets
  2. What this catches
  3. Questions people ask

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 /fail ping opens an incident immediately with the error string stored.
  • A panic: caught via JoinHandle::await returning Err(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 on Ok/Err: on Ok send a GET to https://ping.lastping.dev/<your-monitor-id> with reqwest; on Err POST the error string to the /fail endpoint.

  • What crates do I need?

    tokio-cron-scheduler for scheduling, reqwest for HTTP, and tokio with the full feature set. These are standard in the Tokio ecosystem. A plain tokio::time::interval loop 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::spawn and await the returned JoinHandle. If the handle returns Err(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's std::net::TcpStream with 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.

FREE FOR INDIVIDUALS · FULLY HOSTED

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.