Cron Monitoring vs Uptime Monitoring vs APM: What Each Catches
Updated
Contents
Uptime monitoring checks that a URL answers when a prober asks; cron monitoring checks that a scheduled job reported in when its schedule said it would, and alerts when it didn't. Error tracking, logs, and APM sit between the two: they record what happened inside code that ran, so they explain failures in detail but have nothing to say about a run that never started. A nightly job can stop running while every dashboard stays green. The table below maps seven scheduled-job failures to the tools that actually see them.
Four Tools, Four Kinds of Signal#
- Uptime checks. An external prober requests a URL on its own interval and alerts when the answer is wrong or missing. Google's SRE book calls this black-box monitoring: "testing externally visible behavior as a user would see it" (Monitoring Distributed Systems). A batch job usually serves no URL, so the probe tests the web process next to it.
- Error tracking. An SDK inside your process captures exceptions with their stack traces. The signal exists only when code ran and raised.
- Logs, metrics, and APM. Data that running processes emit: log lines, counters and gauges, traces. Rich detail about runs that happened, nothing about a run that didn't unless someone writes a rule for the absence.
- Heartbeat (cron) monitoring. The job sends a check-in on each run. The monitor holds the schedule and a grace period and alerts when a due check-in hasn't arrived.
Only the last one knows when the job was supposed to run.
What Each Tool Sees in Seven Failure Scenarios#
| Scenario | Uptime check | Error tracking | Logs | Metrics and APM | Heartbeat |
|---|---|---|---|---|---|
| Job never starts | No | No | Only with a no-results alert | Only with a last-success age rule | Yes: check-in missed |
| Job throws | No | Yes, with stack trace | If logged and alerted on | Error spans or a failure counter | Yes: fail, or check-in missed |
| Exits 0 but did nothing | No | No | Only if a rule checks counts | Only if a rule checks a count | Only if it checks reported values |
| Job hangs | No | No | Only with a no-results alert | Only with an age or run-time rule | Partly: late or stuck |
| Host down | Only if that host serves the URL | No | Only with a no-results alert | up == 0, if the host is scraped |
Yes: check-in missed |
| Schedule deleted | No | No | Only with a no-results alert | Only if the series outlives the job | Yes: check-in missed |
| Queue backlog | No | No | Only with a no-results alert | If the queue exports depth or age | Yes: check-in late |
The logs column repeats itself on purpose: a log platform reports something missing only through an alert on an expected line that didn't appear, with the job's schedule written into the query window.
The Job Never Starts#
The cron daemon stopped, a systemd timer was disabled, or the scheduler's container was scaled to zero. Nothing executes, so nothing is produced: no exception, no log line, no span, and the web app's health check keeps passing. How to know if a cron job ran covers what cron's own log can and can't prove here.
The Job Throws#
This is error tracking's case: the stack trace and how often it recurs. Scheduled jobs add two catches. A short-lived process must send its telemetry before it exits; OpenTelemetry's batching span processor waits up to 5 seconds between exports by default (OpenTelemetry trace SDK), so shut the SDK down before the process ends. And a job that catches its own exception and exits 0 reports success to everything that reads the exit code.
The Job Exits 0 but Did Nothing#
The export read an empty source, the API answered 200 with no rows, a date filter matched nothing. Every tool sees a healthy run. Catching it needs a check on the result, and the strongest place for it is the job: exit non-zero when no records were processed, which turns this into the previous scenario.
The Job Hangs#
A lock that never releases, a network call with no timeout. There is no exception and no exit code, and tracing doesn't help: OpenTelemetry's built-in span processors export only finished spans (specification), so the hung run's span hasn't been sent. A heartbeat monitor sees a completion that never arrived, or a start with no finish; whether and how quickly it alerts depends on its rules (CronRadar's are below). In any setup, a time limit inside the job, as in the wrapper further down, turns the hang into a prompt failure.
The Host Is Down#
An uptime check sees this only if the down host serves the probed URL; a dedicated cron server or queue worker usually serves nothing. Prometheus sets up to 0 when a scrape fails (Prometheus), but metrics pushed through a Pushgateway lose that per-instance signal (Prometheus). A heartbeat monitor running elsewhere sees the missing check-in, though not why: a dead host, a stopped daemon, and a broken network path look the same from outside.
The Schedule Is Deleted#
A deploy installed a crontab without the line, a refactor dropped a recurring-job registration, someone deleted the Kubernetes CronJob. Nothing errors, because nothing that remains is wrong, and a metric rule's series can vanish with the job (see the Prometheus section below). A heartbeat monitor configured separately from the scheduler keeps expecting the check-in, so the deletion surfaces as a missed run. Monitors registered by reading the scheduler's job list can behave differently by design, as CronRadar's do.
The Job Waits in a Queue#
Celery beat hands due tasks to worker nodes (Celery), and Hangfire enqueues due recurring jobs as fire-and-forget jobs (Hangfire). If every worker is busy, or none serves that queue, the job waits. Nothing failed; only queue depth or oldest-job age moves, if it's exported and alerted on. A check-in sent when the job actually runs isn't there on time, so the monitor flags the run as late, then missed once the grace period passes. Hangfire monitoring shows how to read Hangfire's queue and recurring-job state directly.
Detecting Absence Requires Knowing the Schedule#
An alert about something that didn't happen needs a component that knows when it should have happened and keeps its own clock. Error trackers, log pipelines, and APM agents evaluate data that arrives. An uptime checker keeps a clock, but it's the prober's interval: it asks "are you up?" every minute and never whether the 02:30 run happened.
The expectation has to follow the schedule, not a fixed interval. Take a job on 0 6 * * 1-5, weekdays at 06:00. A rule of "alert when there has been no success for 25 hours" fires every Saturday and Sunday morning, when nothing is wrong. Stretch it to 73 hours and the weekend noise stops, but a missed Tuesday run isn't reported until Thursday morning. A monitor that evaluates the cron expression knows the next run is Tuesday at 06:00 and can alert a grace period after that, on any day of the week.
The component also has to survive the failure it watches for: a checker on the job's host dies with the host. The dead man's switch guide makes the case for running it outside your infrastructure.
Alerting on a Last-Success Timestamp in Prometheus#
Prometheus's instrumentation guide says "the key metric of a batch job is the last time it succeeded" and recommends pushing it as a gauge to a Pushgateway (Prometheus). A nightly backup sets backup_last_success_timestamp_seconds when it succeeds, and a rule fires on time() - backup_last_success_timestamp_seconds > 26 * 3600, which is the schedule written by hand as a fixed age, with the weekday problem above. The rule also sees only series that exist: the Pushgateway doesn't persist pushed metrics unless started with --persistence.file, so a restart loses them (Pushgateway), and kube-state-metrics stops exposing a Kubernetes object's metrics once the object is deleted (kube-state-metrics). With the series gone, the expression returns nothing and nothing fires; catching that takes a separate absent() rule (Prometheus functions). And the Pushgateway, Prometheus, and Alertmanager all have to be running for the alert to reach you; the prometheus-operator runbooks cover that with an always-firing Watchdog alert sent to an external service that notices when it stops (Watchdog runbook), a heartbeat for the monitoring itself. Kubernetes CronJob monitoring has working kube-state-metrics rules.
Combining the Tools#
Give each tool the question it answers:
- Heartbeat monitoring: did each run happen on time and finish? Missed, late, failed, and stuck runs, alerted to whoever owns the job.
- Error tracking: why did it fail? The stack trace.
- Logs: what did the run do? Include a run ID so a check-in can be matched with its lines.
- Metrics: is something degrading? Duration, records processed, queue depth over time.
- Uptime checks: can users reach the service? They belong to the web app, not the jobs.
When a job throws, the error tracker and the heartbeat monitor both alert. Pick one to notify the owner and let the other supply context.
A wrapper can cover three roles at once. This one logs the job's output with the run ID, stops the run after two hours, and reports start, completion, or failure:
#!/bin/sh
# /opt/nightly-import-monitored.sh
url="https://cron.life/api/v1/ping/nightly-import"
run_id="nightly-import-$(date +%s)-$$"
log=/var/log/nightly-import.log
report() {
event=$1
shift
curl --fail --silent --show-error --max-time 5 --output /dev/null \
--request POST --get --user "$CRONRADAR_API_KEY:" \
--data-urlencode "schedule=15 3 * * *" \
--data-urlencode "gracePeriod=900" \
--data-urlencode "run_id=$run_id" \
"$@" "$url/$event" || true
}
report start
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) start run_id=$run_id" >>"$log"
# Send TERM after 2 hours, then KILL 60 seconds later if it is still running.
timeout -k 60 7200 /opt/nightly-import.sh >>"$log" 2>&1
rc=$?
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) exit=$rc run_id=$run_id" >>"$log"
case "$rc" in
0) report complete ;;
124) report fail --data-urlencode "message=nightly-import.sh hit its 2-hour limit" ;;
*) report fail --data-urlencode "message=nightly-import.sh exited with code $rc" ;;
esac
exit "$rc"Exit status 124 is how GNU coreutils timeout reports that it stopped the command (coreutils manual). BusyBox's timeout, common in Alpine images, replaces itself with the command and doesn't return 124, so a timed-out run lands in the generic branch (BusyBox source). Either way, a hang becomes a failure report at two hours. Each report gives up after five seconds and ignores errors, and the script exits with the job's own code. The schedule is read in UTC, so on a server not set to UTC, send the UTC equivalent of the crontab time.
Where CronRadar Fits#
CronRadar is the last column: a monitor for scheduled jobs. A job reports to https://cron.life with a heartbeat or with start, complete, and fail, and CronRadar compares those check-ins with the job's five-field cron schedule, evaluated in UTC, plus a grace period (90 seconds by default, 60 minimum). A late run is warning inside grace and critical after it, and a fail report alerts immediately. For the exits-0 case, a complete report sent over HTTP can carry up to five numeric or boolean outcomes, such as records_processed, checked against expectations you configure, and a missing or violated one opens an incident; the .NET SDK 1.0.13 can't send outcomes. Alerts go to email, Slack, webhook, Discord, or PagerDuty. How to monitor cron jobs walks through connecting one job.
It deliberately isn't the other columns: no uptime probes, on-call scheduling, status pages, logs, metrics, or traces. It keeps execution events and an optional failure message of up to 1,000 characters, so stack traces stay in your error tracker and output in your log platform. The CronRadar Scheduled Job Methodology lists what CronRadar covers for each job and what to pair it with, such as PagerDuty for escalation.
Its limits:
- Stuck detection works per job, not per run. It needs a
startreport. Every check-in, includingstart, moves the expected time to the job's next scheduled run; if that time plus grace passes while the last event is astart, the monitor goescriticalas stuck. The next run'sstartresets that, so a hang is caught only when the job then goes silent. There is no per-job maximum-runtime setting, which is why the wrapper above sets a time limit. - A heartbeat-only report detects missed runs but not duration or stuck runs, and it can't carry outcomes.
- Discovered jobs that disappear are paused. The Hangfire extension reads the recurring-job registry, so new jobs are monitored without per-job code, and when a reconciliation no longer contains a previously discovered job, CronRadar pauses that monitor and marks it as no longer discovered, keeping its history. CronRadar.Quartz 0.1.0 does the same only when every trigger in the scheduler is a repeating simple trigger, because one rejected cron expression stops the reconciliation from pausing anything. A paused monitor raises no missed-run alerts, so a deleted registration isn't reported the way a deleted crontab line with HTTP check-ins is.
- Package status, checked September 20, 2026: the published CronRadar.Hangfire 0.1.0 can report a failure before Hangfire finishes retrying, including cancellations; reporting only after retries are exhausted requires the pending 0.2.1 release.
- Outside .NET, report over HTTP. No other CronRadar SDK is installable today, so those jobs use HTTP check-ins like the wrapper above, with no auto-discovery.
Choosing between hosted heartbeat services? Cron monitoring tools compared covers how they connect jobs and handle missed runs.
Frequently Asked Questions#
What is the difference between cron monitoring and uptime monitoring?#
Uptime monitoring requests a URL on the prober's own interval, so it tells you whether a service is reachable now. Cron monitoring holds each job's schedule and waits for its check-in, so it tells you whether a particular run happened and finished on time. A web app can pass every uptime check while its nightly job hasn't run in a week.
Can uptime monitoring detect a failed cron job?#
Not directly, because the job serves nothing a prober can request. You can approximate it with an endpoint that returns an error when the job's last success is older than its schedule allows. That puts the schedule logic in your code and makes the check depend on the web app being up, so a check-in from the job itself is usually simpler. Some uptime services sell that check-in as a separate monitor type; UptimeRobot heartbeat monitoring covers one that expects a request every fixed interval rather than on a cron schedule.
Does error tracking catch cron jobs that don't run?#
No. An error tracker records exceptions raised by code that executed, so a job that never starts, was removed from the schedule, or hangs without throwing sends it nothing. Keep it for the stack trace when a run fails, alongside a monitor that notices runs that didn't happen.
Do I need cron monitoring if I have APM?#
If a missed run would matter before someone noticed it by hand, yes. APM instruments runs that happen; it flags a missing run only through a rule that encodes the job's schedule, evaluated by a stack that must itself be up. A heartbeat monitor outside your infrastructure closes that gap. Datadog cron job monitoring shows what one such platform can detect and where its windows miss.
What is heartbeat monitoring?#
Heartbeat monitoring, also called cron monitoring or a dead man's switch, alerts when an expected signal stops arriving. Each run of the job sends a check-in, either one request on success or separate start, complete, and fail requests. The monitor knows when the next check-in is due and alerts once it is overdue by more than the grace period.
Put this guide to work
Know when the next run is missing
Connect one scheduled job, confirm a real completion, then verify the alert destination. Keep your existing scheduler in charge of running the work.