A cron job that silently doesn’t fire gives you almost nothing to go on: no error, no exit code, no obvious log line. So debugging it is not detective work, it is a checklist — and the trick is to walk the checklist in the order that catches the most common causes first, instead of sprinkling sleep statements into a script that was never the problem.
Almost every non-firing cron job I have ever chased came down to one of seven things, and the first four account for the overwhelming majority. Work them in this order and you will usually pinpoint the cause in a couple of minutes. The stack here is ordinary Linux: the cron daemon, bash 5.2, and systemd-journald for the logs.
1. Is the daemon actually running?
A crontab entry does nothing on its own; a daemon has to read it and act on it. If that daemon is stopped, dead, or was never enabled, every job is a no-op and you will get exactly zero feedback. Check it first, because it is the cheapest thing to rule out. Note that the service is named differently across distributions — cron on Debian and Ubuntu, crond on RHEL, Alma and Fedora.
Free · 4 minutes
Is your engineering team shipping safely, or quietly accumulating risk?
Fourteen questions on how work gets from idea to production — cadence, testing, rollback, and the key-person risk in your delivery. Banded finding on screen, full sheet by email.
# Is the daemon even running, and enabled to start at boot?
systemctl status cron # Debian / Ubuntu
systemctl status crond # RHEL / Alma / Fedora
systemctl is-enabled cron # 'enabled' or you lose it on the next reboot
2. Is the crontab installed for the user you think?
Cron is per-user. A job you edited as yourself does not exist for deploy, and a job in root‘s crontab runs as root with root’s environment, not yours. The second most common failure is simply editing the wrong crontab — or editing a file that was never installed, because crontab mycron.txt replaces the crontab whereas opening the raw file in an editor does not.
crontab -l # the invoking user's crontab
sudo crontab -l -u deploy # a specific service account
ls -l /etc/cron.d/ # system crontabs live here (note the user field)
System crontabs in /etc/cron.d/ and /etc/crontab carry an extra field — the user to run as — between the schedule and the command. Forget it, or put a job for that format into a user crontab where the sixth field is treated as part of the command, and the line is quietly malformed.
3. Does the file end with a newline?
This one catches people who script their crontab deployment. The classic Vixie cron behaviour is that a final job line with no terminating newline is silently ignored — the daemon never registers it. If your last line is the job you care about and your editor or your templating tool did not leave a trailing newline, that is your bug, and it is invisible in every listing because the text looks correct. Always leave a blank final line, and if you generate crontabs from a template, assert on the trailing newline in the same place you enforce the rest of your config that actually runs.
4. Does the schedule field mean what you think?
The five time fields are minute, hour, day-of-month, month, day-of-week. The trap is the two day fields. When both day-of-month and day-of-week are restricted (neither is *), cron runs the job when either matches — it is an OR, not an AND. So 0 9 13 * 5 does not mean “09:00 on Friday the 13th”; it means “09:00 on the 13th of the month, and also 09:00 every Friday”. If your job fires far more often than intended, or a monthly job never lands on the day you expected, this is usually why.
The other schedule pitfall is timezone. Modern cron runs in the system’s local time; if the box is on UTC and you reasoned in local time, the job fires — just not when you were watching. Check timedatectl before you conclude the schedule is broken.
5. Is MAILTO swallowing the error?
Cron’s default feedback channel is email: any output on stdout or stderr is mailed to the job’s owner. On a server with no mail transfer agent configured — which is most servers now — that output goes nowhere, and a job that runs and fails looks identical to a job that never ran. The job is executing; you just cannot see it die. The fix is to stop relying on mail and redirect output to a file you control, which is item seven’s self-logging pattern.
6. Did the environment or PATH break it?
Cron does not run with your login shell’s environment. It runs with a deliberately minimal one, and the PATH it sets is typically just /usr/bin:/bin — none of the /usr/local/bin, language version managers, or virtualenv paths your interactive shell has. A script that works perfectly when you run it by hand and “does nothing” under cron is almost always calling a binary that is not on cron’s stripped-down PATH. The habit that avoids it: reference absolute paths in the script, or set an explicit PATH at the top of the crontab.
7. Where the run is actually logged
Cron records every job it starts, whether or not the job produces output. That record is your ground truth for “did the daemon try to run this at all”, which cleanly separates a scheduling problem from a script problem. On a systemd host, read the journal; on older setups, cron logs through syslog.
# systemd journal (Debian names the unit 'cron', RHEL 'crond')
journalctl -u cron --since "today"
journalctl _COMM=crond --since "-1h"
# older / syslog-based hosts
grep CRON /var/log/syslog # Debian / Ubuntu
grep CRON /var/log/cron # RHEL
If you see a CRON line at the scheduled minute, the daemon fired the job and your problem is inside the script — PATH, permissions, a missing file. If you see nothing at that minute, the daemon never ran it, and the cause is one of items one to four above. That single observation is the fork that saves the most time.
The entry that debugs itself
Most of this checklist disappears if the job is written to leave evidence in the first place. Set the environment explicitly, capture both streams to a log you own, and stamp each run so a silent no-op is visible on sight.
# /etc/cron.d/nightly-sync (system crontab: note the user field)
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
# min hour dom mon dow user command
15 2 * * * deploy /usr/local/bin/nightly-sync.sh >> /var/log/nightly-sync.log 2>&1
# Keep a blank line below this comment: a final job line with no trailing
# newline is silently ignored by Vixie cron.
Have the script itself write a start and finish line with a timestamp; then “did it run?” is answered by tail, not by a checklist. That same discipline is what makes scheduled work measurable rather than merely hopeful — the difference between a job you trust and one you keep poking at is observability, the same argument behind instrumenting operational metrics you can actually report on. And because a cron job will eventually run twice — a slow predecessor, a manual re-run, a clock adjustment — the job it triggers should be safe to repeat, which is the whole point of designing work to be idempotent rather than fragile on retry.
Cron is not mysterious; it is just quiet. Give it a voice — a log line and an explicit environment — and the checklist becomes a formality you rarely need.
Build and rescue work
Hands-on delivery of this kind is handled by Sixteen Pillars Studio.
Free interactive tool
Website compliance checklist
What your site has to do, based on what it actually does
Answer as much or as little as you like — the list builds as you go. Nothing is stored against your name and no email is required.
Everything that applies
Ordered by what to do first: legal requirements you can close quickly, then larger pieces of work, then what is expected rather than required. Not exhaustive, and not a legal audit.
Dated PDF, yours to keep or circulate.
Most technology problems are not technology problems. They are control problems.
The systems exist. The investment has been made. The question is whether leadership can understand, direct, evidence, and sustain what those systems produce. Find out where control exists — and where it only appears to.