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

MethodCatches failuresCatches non-runsSetup time
MAILTOYesNo2 min
Slack webhookYesNo10 min
Discord webhookYesNo10 min
Telegram botYesNo10 min
Heartbeat monitoringYesYes30 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_CODE

Python:

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_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

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_CODE

Note: 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/abc123

The 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 →