On this page
  1. Why “it ran” doesn’t mean “it worked”
  2. The four failure modes (and the fix for each)
    1. 1. The script failed but cron doesn’t know
    2. 2. The script produced output but nobody saw it
    3. 3. Cron’s environment is different from yours
    4. 4. Nobody checked whether the job ran at all
  3. The cleanest solution: don’t use bare cron
  4. Where AI coding assistants get this wrong
  5. Checklist
  6. FAQ
    1. Why does my script work in the terminal but fail in cron?
    2. How do I get cron to email me when a job fails?
    3. What’s the difference between this and heartbeat monitoring?
  7. Related topics
  8. Sources
how-to

Why Do Cron Jobs Fail Silently (and How to Fix It)?

Your cron job ran, output nothing, and did nothing. Here's why every cron job fails silently by default — and exactly how to fix it with logging, exit codes, managed schedulers, and external monitoring.

Quick answer

  • Cron fails silently because its only feedback mechanism is email, which most servers never deliver.
  • The fix: redirect output to a log file, make your scripts exit non-zero on failure, and add external monitoring.
  • Or skip bare cron entirely and use a managed scheduler with built-in alerts.

Why “it ran” doesn’t mean “it worked”

A cron job that runs and exits 0 looks healthy to cron. But exiting 0 doesn’t mean the job did its work. A backup script that created an empty file, a cleanup job that skipped the directory because the mount wasn’t ready, a health check that ran curl without checking the response code — all exit 0. All failures.

The cron daemon has exactly one feedback mechanism: email. If the job produces any stdout or stderr output, cron emails it to the crontab owner. On a typical cloud server, that email has nowhere to go. sendmail isn’t configured. The mail spool fills up. Nobody reads it.

So the job fails. Cron knows. Nobody else does.

The four failure modes (and the fix for each)

1. The script failed but cron doesn’t know

The script ran, hit an error, and… kept going. It didn’t set -e. It didn’t check return codes. It just plowed through and exited 0 at the end.

# This exits 0 whether curl succeeded or not
curl https://api.example.com/health

The fix: Exit non-zero on any failure.

#!/bin/bash
set -euo pipefail

# Now curl failing stops the script and exits non-zero
curl --fail https://api.example.com/health || exit 1

Add set -e (exit on error), set -u (error on undefined variables), and set -o pipefail (fail if any command in a pipe fails) to every cron script. If the script exits non-zero, cron captures the output and at least tries to email it.

2. The script produced output but nobody saw it

The job ran, produced error output, and cron tried to email it — to an address that doesn’t deliver. The errors are lost.

The fix: Redirect output to a log file.

# In crontab:
30 2 * * * /home/me/backup.sh >> /var/log/backup.log 2>&1

>> /var/log/backup.log 2>&1 appends both stdout and stderr to a file. Now there’s a record. Even better: use logger to send output to syslog, where your monitoring stack can pick it up.

3. Cron’s environment is different from yours

This is the #1 “works in my terminal, fails in cron” cause. Cron runs with a stripped-down environment — no PATH, no HOME in some implementations, no shell profile loaded. Your script can’t find python3, pg_dump, or any other command that relies on PATH.

The fix: Use absolute paths everywhere.

# Broken (cron can't find these):
python3 /home/me/backup.py
pg_dump mydb > backup.sql

# Fixed:
/usr/bin/python3 /home/me/backup.py
/usr/bin/pg_dump mydb > /home/me/backup.sql

Also set PATH explicitly at the top of your crontab or script:

PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
HOME=/home/me

4. Nobody checked whether the job ran at all

This is the most common failure mode in practice: the job stopped running weeks ago and nobody noticed. Maybe the server was rebuilt and the crontab wasn’t restored. Maybe a disk filled up and cron couldn’t write its lock file. Maybe someone commented out the line during debugging and forgot to uncomment it.

Cron has no built-in “this job hasn’t run in X hours” alert. You won’t know until you notice the stale backups or the overflowing log directory.

The fix: External monitoring.

The simplest approach is a heartbeat monitor like healthchecks.io. Your cron job pings a URL at the end of a successful run. If the ping doesn’t arrive on schedule, the service alerts you. This catches “didn’t run” (the server is down, the crontab was removed) and “ran but failed” (script exited non-zero before the ping line).

#!/bin/bash
set -euo pipefail

/usr/bin/python3 /home/me/backup.py
# Only ping on success:
curl --fail --silent https://hc-ping.com/your-uuid-here

For more detail, see the full How to Monitor Your Cron Jobs guide.

The cleanest solution: don’t use bare cron

For anything that matters — backups, billing runs, data cleanup — a managed scheduler is worth the few minutes of setup:

OptionWhat it gives you
GitHub Actions scheduled workflowsBuilt-in logging, failure notifications, retry, Git-tracked config
AWS EventBridge SchedulerRetries, dead-letter queues, CloudWatch integration
healthchecks.ioHeartbeat monitoring for any cron job, free tier, SMS/email/Slack alerts
cron-job.orgFree hosted cron with dashboard, email alerts on failure

Each of these alerts you when a job fails or doesn’t run. Bare cron never will.

Where this bites vibecoders

AI assistants generate crontab -e entries constantly — for backups, scrapers, cleanup scripts, report generation. They never add set -e, never redirect output to a log, never suggest a heartbeat monitor. The first cron job a vibecoder ships is a database backup that silently stops working the night the disk fills up, and they find out three weeks later when they need the backup.

Where AI coding assistants get this wrong

  • Writing 0 * * * * python3 script.py with no absolute path, no logging, no error handling.
  • Never suggesting set -e or exit-code checking.
  • Generating cron jobs with no monitoring hook — the assistant treats “scheduled” as “done.”
  • Picking random times (e.g., midnight UTC is peak load on many services).

Checklist

  • set -euo pipefail at the top of every cron script
  • Absolute paths for every command
  • Output redirected to a log file: >> /var/log/jobname.log 2>&1
  • Script exits non-zero on failure
  • Heartbeat ping (healthchecks.io or equivalent) at end of successful run
  • Consider a managed scheduler for anything that matters

FAQ

Why does my script work in the terminal but fail in cron?

Cron runs with a nearly empty environment — no PATH, no HOME in some cases, no shell aliases. Use absolute paths for every command (e.g., /usr/bin/python3 not python3) and set PATH and SHELL at the top of your script.

How do I get cron to email me when a job fails?

Set MAILTO=you@example.com at the top of your crontab. Then make sure your script exits non-zero on failure (exit 1). Cron only emails on output — silence plus exit 0 means no email, even if the job did nothing.

What’s the difference between this and heartbeat monitoring?

Heartbeat monitoring tells you the job ran. Exit codes tell you it ran correctly. You need both: exit non-zero for “ran but failed” and ping a heartbeat service for “didn’t run at all.” Exiting non-zero without a heartbeat means you still won’t know if the server was down.


Sources

Share: