CronCanary DocsPricingOpen app
← Integration guides

Monitor a Python cron job

Ping from any Python scheduler — a plain cron script, APScheduler, or Celery beat — so a crash or a job that never ran gets caught instead of going unnoticed.

Why Python cron jobs fail silently

A Python script invoked from cron runs in a stripped-down environment: no login shell, no .bashrc, often no virtualenv activated unless the crontab line does it explicitly. The most common failure isn't a bug in the job logic — it's an ImportError because cron's PATH doesn't include the venv's bin/, or a ModuleNotFoundError because the wrong system Python ran instead of the one with your packages installed. Cron's own MAILTO was designed to catch exactly this by emailing stdout/stderr, but on modern servers there's frequently no local MTA configured, so that mail silently goes nowhere. The script errors out on line one, cron considers its own job "done" (it ran, even if the Python process inside it died instantly), and nobody is told. A ping-based check doesn't care why the job failed — it only cares whether the success ping arrived, so a wrong interpreter, a missing dependency, an unhandled exception, or a box that never woke up all surface the same way: silence.

Plain script

Wrap the job body in a try/except and ping on both paths. Five-second timeouts keep a monitoring outage from ever blocking the real job:

import requests URL = "$URL" # e.g. https://croncanary-ping.sleeezydesigns.workers.dev/<your-uuid> try: requests.get(URL + "/start", timeout=5) do_the_work() requests.get(URL, timeout=5) # success except Exception: requests.get(URL + "/fail", timeout=5) # failure -> immediate alert raise

A reusable decorator

If you have more than one scheduled function, wrap once instead of repeating the try/except every time:

import functools, requests def monitored(url): def deco(fn): @functools.wraps(fn) def wrap(*a, **k): requests.get(url + "/start", timeout=5) try: r = fn(*a, **k); requests.get(url, timeout=5); return r except Exception: requests.get(url + "/fail", timeout=5); raise return wrap return deco @monitored("$URL") def nightly_report(): ...

APScheduler

Wrap the job function you pass to add_job(...) with the same decorator, or ping inside the function body directly. Give the check a Cron or Interval schedule that matches your CronTrigger/IntervalTrigger args, including the same timezone — APScheduler jobs default to the scheduler's configured timezone, which is easy to get out of sync with the check.

Celery beat

For a task fired by beat, ping at the end of the task body (or with the decorator above). Match the check's Cron schedule to the corresponding crontab(...) entry in beat_schedule. Because beat is a single, separate process from your workers, it has its own failure mode — see the dedicated Celery beat monitoring guide for how to catch beat itself going down, not just an individual task failing.


Related guides


Add a live status badge to your README

Every check has a public SVG badge that shows its live status (updates within ~1 minute). Paste this into any README — it doubles as a heartbeat anyone on the team can see:

[![CronCanary](https://croncanary.fluxath.app/badge/<your-check-id>.svg)](https://croncanary.fluxath.app)

Copy the exact markdown from your check's detail page. Add ?label=your-text to customize the left label.


Ready to wire this up? Create a free check — 20 checks, all alert channels, no card.