How to monitor Kubernetes CronJobs
Monitor a Kubernetes CronJob with a heartbeat: the job's command pings a URL when it succeeds, and you are alerted when a ping is late for its schedule. That catches every failure mode at once: suspended, never scheduled, crashed, stuck or silently skipped. Add kubectl get cronjob checks by hand, and Vigil's DaemonSet for failed pods.
What can go wrong with Kubernetes CronJobs
- It never starts. The CronJob is suspended, the schedule is wrong, or the controller missed more than 100 schedules (for example while the controller was down with no
startingDeadlineSeconds) and stopped scheduling it. - Skipped by concurrencyPolicy. With
concurrencyPolicy: Forbida run that is still going makes the next one be skipped, with only an event to show for it. - Fails after retries. The pod fails
backoffLimittimes (6 by default) and the Job is marked failed. Only one failed Job is kept by default (failedJobsHistoryLimit: 1), so the evidence disappears quickly. - Runs too long. A hung run occupies the slot for hours;
activeDeadlineSecondskills it only if you set it. - Wrong timezone. Without
spec.timeZone(stable since Kubernetes 1.27) schedules are in the controller's timezone, usually UTC.
The signals that matter, with alert thresholds
| Signal | Alert when | Why it matters |
|---|---|---|
| Heartbeat ping on success | late by more than the grace period | Covers every way a run can fail to happen. |
| Run duration (start → finish ping) | > 2× the usual, or over a max run time | Hung or degraded job. |
| Exit code reported | non-zero | The job ran and failed. |
| Failed pods from Job runs | any | Context for the failure. |
| lastSuccessfulTime | older than one schedule interval plus grace | The same fact, checked by hand. |
Thresholds are starting points. Tune them after a week of data: an alert nobody acts on should be raised or removed.
How to check Kubernetes CronJobs by hand
kubectl get cronjob -A
kubectl get cronjob nightly-backup -n ops -o jsonpath='{.status.lastSuccessfulTime}'
kubectl get jobs -n ops --sort-by=.status.startTime kubectl describe cronjob nightly-backup -n ops | tail -20
kubectl create job --from=cronjob/nightly-backup manual-test -n ops kubectl logs job/manual-test -n ops -f
How to watch Kubernetes CronJobs with Vigil
- In New monitor create a Heartbeat monitor, choose cron schedule and paste the CronJob's exact
schedule, with the same timezone asspec.timeZone(UTC if unset). Set a grace period a bit longer than a normal run. - Append the ping to the container command:
/app/backup.sh && curl -fsS -m 10 --retry 3 https://puzaricloud.in/ping/<token>. Images without curl can usewget -qO- …. Only successful runs ping, so a failure is a missing ping. - For duration and failure details, ping
https://puzaricloud.in/ping/<token>/startfirst and finish withhttps://puzaricloud.in/ping/<token>/$?(the exit code), and set a maximum run time so a hung run alerts before the next one is due. - Apply Vigil's DaemonSet from Kubernetes so failed and OOM-killed Job pods also show up as incidents with their reason.
- Every schedule page in the cron library includes a ready-made CronJob manifest with the ping wired in.
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-backup
spec:
schedule: "0 2 * * *"
timeZone: "Asia/Kolkata"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 600
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 3600
template:
spec:
restartPolicy: Never
containers:
- name: backup
image: registry.example.com/backup:1.4
command: ["/bin/sh", "-c"]
args:
- |
curl -fsS -m 10 https://puzaricloud.in/ping/$TOKEN/start
/app/backup.sh; rc=$?
curl -fsS -m 10 https://puzaricloud.in/ping/$TOKEN/$rc
exit $rc
env:
- name: TOKEN
valueFrom: {secretKeyRef: {name: vigil-ping, key: token}}
Every alert goes to the channels you pick: email, Slack, Telegram, Discord, Microsoft Teams, Pushover, webhooks or on-call paging. Failures are confirmed by a second check before anyone is woken up.
Kubernetes CronJobs monitoring FAQ
Why not just alert on failed Jobs?
Because the worst failures produce no Job at all: a suspended CronJob, a bad schedule, too many missed start times, or a run skipped by concurrencyPolicy. A heartbeat notices the absence of success, whatever the cause.
What grace period should I use?
Longer than the job's normal duration plus scheduling delay (image pull, pod start). For a 5-minute nightly job, 15-20 minutes is reasonable; the cron explainer suggests one per schedule.
Should the ping token be a Secret?
Yes. Anyone with the token can mark the job as successful, so keep it in a Secret, as in the example, rather than in the manifest.