How to Get Alerts When Cron Jobs Fail

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, you get nothing.

This guide covers five ways to set up failure alerts, from the simplest (built-in email) to the most reliable (heartbeat monitoring). Each method includes working code you can copy-paste and adapt.

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, and MAILTO can't detect it.

The other catch: Server-sent emails land in spam. Gmail, Outlook, and most providers aggressively filter mail from VPS IP addresses without proper SPF/DKIM/DMARC configuration.

2. Custom Slack webhook script

Create a Slack incoming webhook at api.slack.com/messaging/webhooks, then wrap your cron job in a script that posts to Slack on failure:

#!/bin/bash
# /usr/local/bin/cron-slack-wrapper.sh
# Usage: cron-slack-wrapper.sh "Job Name" /path/to/your/script.sh

JOB_NAME="$1"
shift
SLACK_WEBHOOK="https://hooks.slack.com/services/T00000000/B00000000/xxxxxxxxxxxx"

# Run the actual job
"$@" 2>&1
EXIT_CODE=$?

if [ $EXIT_CODE -ne 0 ]; then
  HOSTNAME=$(hostname)
  TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC")

  curl -s -X POST "$SLACK_WEBHOOK" \
    -H "Content-Type: application/json" \
    -d "{
      \"text\": \"*Cron job failed* on `$HOSTNAME`\",
      \"blocks\": [
        {
          \"type\": \"section\",
          \"text\": {
            \"type\": \"mrkdwn\",
            \"text\": \"*Cron job failed*\\nJob: `$JOB_NAME`\\nHost: `$HOSTNAME`\\nExit code: `$EXIT_CODE`\\nTime: $TIMESTAMP\"
          }
        }
      ]
    }"
fi

exit $EXIT_CODE
# crontab entry
0 2 * * * /usr/local/bin/cron-slack-wrapper.sh "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 Discord webhook in your server settings (Server Settings → Integrations → Webhooks). Discord webhooks accept a content field:

#!/bin/bash
# /usr/local/bin/cron-discord-wrapper.sh

JOB_NAME="$1"
shift
DISCORD_WEBHOOK="https://discord.com/api/webhooks/000000000/xxxxxxxxxxxx"

"$@" 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: `$JOB_NAME`\\nHost: `$(hostname)`\\nExit code: `$EXIT_CODE`\\nTime: $(date -u +'%Y-%m-%d %H:%M:%S UTC')\"
    }"
fi

exit $EXIT_CODE

Same limitation as Slack: Only fires when the job runs and fails. Can't detect a job that never started.

4. Telegram bot alerts

Setting up a Telegram alert bot takes about 3 minutes:

  1. Message @BotFather on Telegram and send /newbot
  2. Choose a name and username. BotFather gives you a token like 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw
  3. Start a chat with your bot and send any message
  4. Get your chat ID: curl https://api.telegram.org/bot<TOKEN>/getUpdates — look for chat.id in the response
#!/bin/bash
# /usr/local/bin/cron-telegram-wrapper.sh

JOB_NAME="$1"
shift
BOT_TOKEN="110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw"
CHAT_ID="123456789"

"$@" 2>&1
EXIT_CODE=$?

if [ $EXIT_CODE -ne 0 ]; then
  MESSAGE="*Cron job failed*%0AJob: `$JOB_NAME`%0AHost: `$(hostname)`%0AExit: `$EXIT_CODE`%0ATime: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"

  curl -s "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
    -d "chat_id=${CHAT_ID}" \
    -d "text=${MESSAGE}" \
    -d "parse_mode=Markdown"
fi

exit $EXIT_CODE

Note: Replace the bot token and chat ID with your own. The token above is an example from the Telegram docs.

5. Heartbeat monitoring with CronSafe

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.

# Add to the end of your cron job — 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

That's the entire setup. No wrapper script, no bot token, no webhook URL to manage per job. CronSafe monitors the ping interval and alerts you via email, Slack, Discord, Telegram, or webhook when the ping is missing.

This catches every failure mode:

  • Job crashes with non-zero exit code (the && prevents the ping)
  • Job hangs and never finishes (ping never arrives)
  • Crontab is cleared or crond is stopped (ping never arrives)
  • Server reboots and cron doesn't start (ping never arrives)
  • PATH is broken and the binary isn't found (ping never arrives)

CronSafe is free for 5 monitors with no time limit. Create a free account and you'll have your first monitor running in 30 seconds.

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. You need heartbeat monitoring to catch that case. Also verify that your server has a working MTA (sendmail, postfix, or msmtp) — 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 via your preferred channel. It's the only method that detects jobs that never started, not just jobs that started and failed.