The job was supposed to run at 02:15. It’s 09:40. Finance wants the report. Cron has strong opinions and a tiny environment. Most “cron isn’t working” tickets are PATH, permissions, wrong user, or the daemon quietly not running.
Here’s the checklist I use before blaming the scheduler gods. Keep chai nearby — this is usually a 15-minute mystery once you’re methodical.
Automation done well is a bridge into DevOps habits — after you fix today’s crontab, skim the SysAdmin → DevOps roadmap for moving critical jobs into reviewed, logged pipelines.
1) Is cron even running?
systemctl status cron # Debian/Ubuntu
systemctl status crond # RHEL-family
ps aux | grep -E '[c]ron'

Two quick checks: is the cron service enabled at boot, and is it running right now? Real output from an Ubuntu 24.04 server.
If the unit failed, fix that first with normal systemd + journalctl technique.
2) Which crontab did you edit?
-
crontab -e— current user’s crontab -
sudo crontab -e— root’s crontab -
/etc/crontaband/etc/cron.d/*— system files (they have a user field) -
/etc/cron.dailyetc. — run-parts scripts (must be executable, no dots in some distros’ run-parts rules)
crontab -l
sudo crontab -l
ls -la /etc/cron.d/ /etc/cron.daily/
Editing the wrong file is embarrassingly common. No shame — just check.
3) Timing syntax and timezones
# m h dom mon dow command
15 2 * * * /usr/local/bin/backup.sh
Confirm the server timezone:
timedatectl
date
UTC vs Asia/Kolkata has ruined more “it didn’t fire” threads than bad asterisks. Containers may differ from the host.
4) Environment: PATH and shell
Cron gives you a minimal environment. That fancy script that works interactively fails because python isn’t on cron PATH.
Rules that prevent pain:
- Absolute paths to binaries and scripts
- Set
PATH=at the top of the crontab if needed - Source a known env file explicitly inside the script
- Don’t rely on aliases or interactive shell functions
15 2 * * * /usr/bin/flock -n /tmp/backup.lock /usr/local/bin/backup.sh >>/var/log/backup.log 2>&1
flock prevents overlapping runs. Redirect stdout/stderr to a log you actually look at.
5) Permissions, SELinux, and executable bits
ls -l /usr/local/bin/backup.sh
sudo -u thecronuser /usr/local/bin/backup.sh
Run as the same user cron uses. If that fails, cron will fail too. SELinux denials show up in audit logs — don’t disable SELinux to “make cron work”; fix the context.
Scripts under /etc/cron.daily need to be executable and often must not have extensions depending on run-parts behavior.
6) Percent signs, newlines, and special characters
In crontab, % becomes a newline unless escaped. Quoting matters. If your command looks like line noise, refresh Linux special characters and put complexity inside a script file instead of an inline novel.
7) Mail, pam, and silent failure
Cron traditionally mails output to the user. If mail isn’t configured, you see nothing. Prefer explicit logs.
Also check:
grep CRON /var/log/syslog # Debian-ish
grep CROND /var/log/cron # RHEL-ish
journalctl -u cron -b
Did cron even attempt to start the job? If there’s no line at the scheduled time, you’re looking at schedule/daemon/user issues. If there is a line and the job exits non-zero, debug the script.
8) Disk full and readonly filesystems
Cron can’t write logs or temp files if the disk is full. If jobs vanish mysteriously after a growth spurt, run the disk full checklist.
9) Maybe you want a systemd timer
For new work on systemd distros, timers give better logging, dependencies, and systemctl list-timers.
systemctl list-timers --all
Cron still fine for simple schedules. Timers shine when you care about catch-up (Persistent=true) and unit dependency graphs.
Pro checklist (copy into the ticket)
- crond/cron service active
- correct crontab / cron.d entry
- time zone understood
- absolute paths + logging redirect
- manual run as cron user succeeds
- logs show execution attempt
- exit code / script error fixed
- prevent overlap with flock
- monitoring: alert if the job’s output artifact is stale
Security note: cron as root with world-writable scripts is a gift to attackers. Keep scripts locked down the same way you treat SSH access.
Worked example: backup script “runs” but empty files
Cron line fires; logs show the script started; backup size is 0 bytes. Manual run as root works. Cause: cron user lacked permission to read the data directory; the script ignored set -e and “succeeded.”
Fixes:
-
set -euo pipefailat top of bash scripts - Check exit codes of mysqldump/tar explicitly
- Alert if backup artifact size < threshold or mtime too old
- Run verification restores monthly (tiny sample)
Gotchas beyond the basics
If the checklist above found nothing, look at these.
Crontab environment gotchas beyond PATH
-
HOMEmay differ -
SHELLdefaults to/bin/sh(dash on Debian) — bashisms fail - Locale variables missing → weird sorting/date formats
- Secrets available in interactive pam sessions but not cron
* * * * * /usr/bin/env > /tmp/cron-env.txt
Compare that file to your interactive env. Remove the debug line afterward.
Distributed systems note
Never schedule the same critical job on five app nodes without locking or a leader. You’ll get five overlapping dumps and a sad database. Use a single scheduler host, systemd timer on one node, or an orchestrator.
Migrating a job to a timer
Create backup.service (oneshot) + backup.timer. Benefits: clearer logs, dependencies, systemctl list-timers, and consistent unit permissions. Keep cron for simple legacy jobs if you must — but don’t run both for the same task.
Observability for scheduled work
A job without a freshness check is a wish. Minimum: dead man’s switch (Healthchecks.io, Prometheus pushgateway + alert, or “file mtime older than 26h”). Cron failing silently is how backups die for three months unnoticed.
Daylight saving and odd schedules
Jobs scheduled in the DST gap may skip or double depending on the cron implementation and timezone. Prefer UTC on servers when your org can handle it psychologically — or document local-time behavior during transitions.
Wrappers that save careers
#!/usr/bin/env bash
set -euo pipefail
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
cd /opt/jobs/backup
/usr/bin/flock -n /run/backup.lock /opt/jobs/backup/run.sh | /usr/bin/logger -t backup-job
Call that wrapper from cron with an absolute path. Logging to syslog/journal means you can journalctl -t backup-job.
Permissions on /etc/cron.d files
Some distros ignore cron.d files that are group/world writable or have wrong names. If your drop-in never runs, check mode 644, root ownership, and no dots in the filename when run-parts is involved for daily directories.
Final sanity printout
At the scheduled minute, watch live:
journalctl -f | grep -i cron
If nothing appears, the schedule/daemon/user path is wrong. If something appears and the job fails, debug the script — not cron itself.
Practice
Lab exercise
Create a user crontab that runs every minute and appends UTC timestamps to ~/cron-lab.log. Watch it work. Break PATH by calling a bare command that only exists in your interactive profile. Fix with absolute paths. Add flock. Remove the every-minute job when finished so you don’t wake up to a gigabyte log.
FAQ
Why does it run manually but not from cron?
Environment (PATH, cwd, secrets), different user, or missing tty assumptions. Log env from the script with env when debugging.
Do cron jobs run when the server was off?
Classic cron: no (missed jobs are missed). anacron/systemd Persistent timers: can catch up. Know which you have.
How do I run every 5 minutes?
*/5 * * * * /path/to/job
Is @reboot reliable?
Mostly, after local filesystems are up — but network may not be. For networky jobs, prefer systemd with After=network-online.target.
Should secrets live in the crontab?
No. Use a root-only env file or a secrets manager. Crontab is often readable by the user and ends up in backups.
Sources
- crontab(5) manual page
-
systemd.special manual page, for
network-online.target
This article first appeared on sysadmin.co.in. The original has the latest updates.
Top comments (0)