How to Get Cron Job Failure Alerts in 2026
A cron job that fails at 2 AM doesn't page anyone by default. Cron itself has no alerting mechanism beyond email — and that only works when the job actually runs and produces output. If the job doesn't run at all, you get silence.
This guide covers five methods to get alerted when cron jobs fail silently, from the simplest (built-in email) to the most reliable (heartbeat monitoring). Each method includes working code examples in Bash, Python, Node.js, Laravel, and GitHub Actions.
Alert methods compared
| Method | Catches failures | Catches non-runs | Setup time |
|---|---|---|---|
| MAILTO | Yes | No | 2 min |
| Slack webhook | Yes | No | 10 min |
| Discord webhook | Yes | No | 10 min |
| Telegram bot | Yes | No | 10 min |
| Heartbeat monitoring | Yes | Yes | 30 sec |
“Catches non-runs” means detecting jobs that never started — crontab cleared, crond stopped, server rebooted. Only heartbeat monitoring detects this.
1. Built-in MAILTO (and why it's unreliable)
Cron can email you when a job produces output. Add MAILTO to your crontab:
# crontab -e MAILTO=ops@example.com MAILFROM=server@example.com # This will email you if backup.sh writes to stdout or stderr 0 2 * * * /usr/local/bin/backup.sh
Requirements: A working MTA on the server (sendmail, postfix, or msmtp). Most cloud VMs don't have one configured out of the box.
The catch: MAILTO only fires when there IS output. If your job silently doesn't run (crontab cleared, crond stopped, path broken), there's no output, and no email. This is the most common failure mode.
The other catch: Server-sent emails land in spam. Gmail and Outlook aggressively filter mail from VPS IP addresses without proper SPF/DKIM/DMARC.
2. Custom Slack webhook script
Create a Slack incoming webhook at api.slack.com/messaging/webhooks, then wrap your cron job to post on failure.
Bash:
#!/bin/bash
# /usr/local/bin/cron-slack-wrapper.sh "Job Name" /path/to/script.sh
JOB_NAME="$1"
shift
SLACK_WEBHOOK="https://hooks.slack.com/services/T00000000/B00000000/xxxxxxxxxxxx"
"$@" 2>&1
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
curl -s -X POST "$SLACK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"text\": \"*Cron job failed*\\nJob: `$JOB_NAME`\\nHost: `$(hostname)`\\nExit: `$EXIT_CODE`\"}"
fi
exit $EXIT_CODEPython:
import subprocess, requests, socket
SLACK_WEBHOOK = "https://hooks.slack.com/services/T00000000/B00000000/xxxxxxxxxxxx"
def run_with_alert(job_name, command):
result = subprocess.run(command, shell=True, capture_output=True, text=True)
if result.returncode != 0:
requests.post(SLACK_WEBHOOK, json={
"text": f"*Cron job failed*\nJob: `{job_name}`\nHost: `{socket.gethostname()}`\nExit: `{result.returncode}`"
})
return result.returncode
if __name__ == "__main__":
exit(run_with_alert("Nightly backup", "/usr/local/bin/backup.sh"))Node.js:
const { execSync } = require("child_process");
const os = require("os");
const SLACK_WEBHOOK = "https://hooks.slack.com/services/T00000000/B00000000/xxxxxxxxxxxx";
async function runWithAlert(jobName, command) {
try {
execSync(command, { stdio: "pipe" });
} catch (err) {
await fetch(SLACK_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `*Cron job failed*\nJob: \`${jobName}\`\nHost: \`${os.hostname()}\`\nExit: \`${err.status}\``,
}),
});
process.exit(err.status);
}
}
runWithAlert("Nightly backup", "/usr/local/bin/backup.sh");Limitation: This catches failures (non-zero exit codes) but not silent non-runs. If the job doesn't execute at all, no Slack message is sent.
3. Discord webhook
Create a webhook in Server Settings → Integrations → Webhooks. Same pattern as Slack, different URL format.
#!/bin/bash
DISCORD_WEBHOOK="https://discord.com/api/webhooks/000000000/xxxxxxxxxxxx"
/usr/local/bin/backup.sh 2>&1
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
curl -s -X POST "$DISCORD_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"content\": \"**Cron job failed**\\nJob: `Nightly backup`\\nHost: `$(hostname)`\\nExit: `$EXIT_CODE`\"}"
fi
exit $EXIT_CODESame limitation as Slack: Only fires when the job runs and fails. Can't detect a job that never started.
4. Telegram bot alerts
Message @BotFather on Telegram, create a bot, get your chat ID, then:
#!/bin/bash
BOT_TOKEN="110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw"
CHAT_ID="123456789"
/usr/local/bin/backup.sh 2>&1
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
curl -s "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \
-d "chat_id=$CHAT_ID" \
-d "text=*Cron job failed*%0AJob: `Nightly backup`%0AHost: `$(hostname)`%0AExit: `$EXIT_CODE`" \
-d "parse_mode=Markdown"
fi
exit $EXIT_CODENote: Replace the bot token and chat ID with your own. Same limitation — can't detect silent non-runs.
5. Heartbeat monitoring (catches every failure mode)
Methods 1-4 share the same blind spot: they can't detect a job that never ran. Heartbeat monitoring flips the model — instead of reporting failures, your job reports success. If the success signal is missing, you get alerted.
This catches every failure mode:
- Job crashes with non-zero exit code
- Job hangs and never finishes
- Crontab is cleared or crond is stopped
- Server reboots and cron doesn't start
- PATH is broken and the binary isn't found
Here's how to add heartbeat pings in every language:
Bash (one-liner):
# The && ensures the ping only fires if the job exits with code 0 0 2 * * * /usr/local/bin/backup.sh && curl -s https://api.getcronsafe.com/ping/abc123
Python:
import requests
def run_backup():
# ... your backup logic ...
pass
if __name__ == "__main__":
run_backup()
# Ping CronSafe on success — if this never runs, you get alerted
requests.get("https://api.getcronsafe.com/ping/abc123", timeout=5)Node.js:
async function runBackup() {
// ... your backup logic ...
}
runBackup().then(() => {
fetch("https://api.getcronsafe.com/ping/abc123");
});Laravel (Artisan command):
<?php
// app/Console/Commands/NightlyBackup.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
class NightlyBackup extends Command
{
protected $signature = 'backup:run';
protected $description = 'Run nightly backup and ping CronSafe';
public function handle()
{
// ... your backup logic ...
// Ping CronSafe on success
Http::timeout(5)->get('https://api.getcronsafe.com/ping/abc123');
return Command::SUCCESS;
}
}// app/Console/Kernel.php — schedule the command
protected function schedule(Schedule $schedule)
{
$schedule->command('backup:run')->dailyAt('02:00');
}GitHub Actions:
# .github/workflows/nightly-backup.yml
name: Nightly backup
on:
schedule:
- cron: '0 2 * * *' # Every day at 2 AM UTC
jobs:
backup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run backup
run: ./scripts/backup.sh
- name: Ping CronSafe on success
if: success()
run: curl -s https://api.getcronsafe.com/ping/abc123The if: success() condition ensures the ping only fires when all previous steps pass. If any step fails, the ping is skipped and CronSafe alerts you.
FAQ
Why doesn't my cron job send an email when it fails?
Cron's MAILTO feature only sends email when a job produces output. If the job doesn't run at all (crontab cleared, crond stopped, server rebooted), there is no output, so no email. Verify your server has a working MTA — most cloud VMs don't.
What is the best way to get alerted when a cron job fails?
Heartbeat monitoring catches all failure modes, including silent ones. Your job pings a URL on success; if the ping is missing, you get alerted. It's the only method that detects jobs that never started.
Can I monitor cron jobs in GitHub Actions?
Yes. Add a curl step at the end of your workflow with if: success(). If the workflow fails or GitHub's scheduler skips it (which happens under load), the ping is missing and CronSafe alerts you.
Can I get cron failure alerts on my phone?
Yes. Use Telegram or Discord alerts — both have mobile apps with push notifications. With CronSafe, you configure your alert channel once and get notified on any device.
Get instant alerts with CronSafe
20 free monitors. Email, Slack, Discord, Telegram, Webhooks. Set up in 30 seconds.
Start free monitoring →