Checklist for When cron Isn’t Running: Why Jobs Fail Under cron but Work by Hand, and How to Read the Logs
A batch job you’re sure you put in crontab isn’t running. Run it by hand in a terminal and it succeeds, but from cron nothing happens. cron problems tend to fail silently, with no error shown anywhere, so tracking down the cause can take a while. This article lays out the order to check things when cron isn’t running, split into “the expression”, “cron itself”, “logs”, “the environment”, “files and permissions” and “time zones”, and covers the typical reasons a job works by hand but fails under cron, and how to fix them.
First narrow it down: is cron not running, or is the command failing?
“cron isn’t working” covers two quite different problems. In one, cron never starts the command at the scheduled time. In the other, cron starts it on schedule but the command fails partway through. Where you look depends entirely on which it is, so separate the two first.
- 1
Check the logs for a record of it starting
If cron’s log (explained below) shows a “CMD (command)” line at that time, cron did start it, and the problem is on the command side. If there’s no line, suspect the expression, cron itself, or how the file is written.
- 2
Add a temporary every-minute test line
Use crontab -e to add “* * * * * date >> /tmp/cron-test.log 2>&1” temporarily, wait a minute or two, and see whether timestamps keep appearing in /tmp/cron-test.log. If they do, cron itself is working. Always remove the line when you’re done.
- 3
Move the real job to a time a few minutes away
With daily or weekly jobs, waiting for the next run can take days. Change the time to a few minutes from now just for the test, check the result, then change it back, and you’ll know right away.
Expression mistakes that make a job look like it isn’t running
As long as an expression is valid, cron runs it at those times without complaint, even if that isn’t what you meant. It’s not unusual for a job you thought wasn’t running to have been running at an unexpected time. Start by doubting the expression itself.
| Expression written | Intent | What actually happens |
|---|---|---|
| 0 9 31 * * | Last day of every month | Never runs in February, April, June, September or November, which have no 31st |
| 0 9 30 2 * | Last day of February | February 30 doesn’t exist, so it never runs |
| 0 9 1-7 * 1 | First Monday | Runs every Monday and on the 1st–7th of every month (OR) |
| 30 9 * * * (on a UTC server) | 9:30 Japan time | Runs at 18:30 Japan time |
| 9 30 * * * | 9:30 | 30 isn’t allowed in the hour field, so it errors and isn’t loaded |
| * 3 * * * | Once a day at 3:00 | Runs every minute from 3:00 to 3:59, 60 times |
Day-of-month/weekday combinations and month-end schedules are especially easy to get wrong; they’re covered in detail in cron’s day-of-month/weekday OR rule and how to write nth weekdays and month-end. Paste a crontab line into the cron expression checker and it translates the meaning into plain language and lists the next 10 run times, so you can see straight away when it’s actually scheduled to run.
Checking cron’s status and logs
If the expression is right but there’s no record of it starting, check whether the cron service has stopped and whether the logs show any errors. The service name and log location differ by distribution.
| What to check | Ubuntu / Debian | RHEL / CentOS / Rocky / Amazon Linux |
|---|---|---|
| Service status | systemctl status cron | systemctl status crond |
| Start the service | sudo systemctl enable --now cron | sudo systemctl enable --now crond |
| Logs (file) | grep CRON /var/log/syslog | sudo cat /var/log/cron |
| Logs (journal) | journalctl -u cron --since today | journalctl -u crond --since today |
- A “(username) CMD (command)” line: a record that the command was started at that time. If it’s there, cron is fine
- “(CRON) info (No MTA installed, discarding output)”: the command printed something, but there’s no mail system to send it, so it was thrown away. Your error message has very likely vanished here
- “(*system*) … WRONG FILE OWNER” or “INSECURE MODE”: the owner or permissions of a file in /etc/cron.d or similar are wrong, so it isn’t loaded
- “bad minute”, “bad hour” or “errors in crontab file”: a syntax error in the expression. That line isn’t loaded
Five reasons a job works by hand but fails under cron
If the log shows it started but there’s no result, the command is failing in cron’s environment. cron runs commands in a very minimal environment, unlike your login shell. When something works by hand but not under cron, that difference is almost always why.
| Cause | Symptom | Fix |
|---|---|---|
| Short PATH | command not found (for commands in /usr/local/bin or ~/.local/bin) | Use absolute paths, or set PATH= at the top of the crontab |
| Shell is /bin/sh | Errors from bash syntax (source, [[ ]], arrays) | Put #!/bin/bash at the top of a script and call it as a script |
| Doesn’t read .bashrc and similar | Environment variables (API keys, language version managers) are empty | Set the variables inside the script or at the top of the crontab |
| Working directory is home | Files given as relative paths aren’t found | cd /path/to/dir at the start of the script, or use absolute paths |
| % becomes a newline | The command is cut off partway through date +%Y%m%d | Escape it as \% inside crontab |
The quickest way to test in a cron-like environment is to run the command with almost no environment variables, as in “env -i HOME=$HOME PATH=/usr/bin:/bin SHELL=/bin/sh /bin/sh -c 'command'”. If it fails there, you know the difference in environment is the cause.
The % problem is easy to miss, so pay special attention to it. In a crontab command, % means “newline here, and pass the rest to standard input”. A line like “tar czf /backup/$(date +%Y%m%d).tar.gz /data” is cut off before the % and fails. Escape it as “date +\%Y\%m\%d”, or put the work in a shell script and have crontab just call the script; either is reliable.
Pitfalls with files, permissions and line endings
Even when the crontab syntax is correct, where files live or how their permissions are set can stop them being loaded or executed. Check the following in order.
- Does the script have execute permission? Check with ls -l for an x; if it’s missing, run chmod +x script.sh, or call it through a shell as in sh script.sh
- Is the #! on line 1 correct? Does the path in #!/bin/bash exist? Files edited on Windows end lines with CRLF and fail with “/bin/bash^M: bad interpreter”; convert them to LF with dos2unix or similar
- Does the last line of the crontab end with a newline? Some implementations ignore a final line without one
- Do files in /etc/cron.d have a user field? /etc/crontab and files in /etc/cron.d/ need at least seven fields: “minute hour day month weekday user command”
- File names and permissions in /etc/cron.d: on Debian and Ubuntu, files whose names contain a . (such as backup.cron) aren’t read. The owner should be root, with permissions others can’t write to (644)
- cron.allow and cron.deny: if /etc/cron.allow exists, only the users listed in it can use crontab
Time zone offsets: is it running 9 hours off?
It’s common for “not running” to turn out to be “running 9 hours off”. cron reads times in the time zone of the server (or container) it runs on. Cloud servers and container images often default to UTC, so a time written with Japan time in mind runs 9 hours late.
- 1
Check the server’s time zone
Run date or timedatectl and see whether it shows JST or UTC. If it’s UTC, either write times 9 hours earlier than Japan time, or change the time zone.
- 2
Restart cron after changing the time zone
You can change it with sudo timedatectl set-timezone Asia/Tokyo, but a cron that’s already running may keep the old time zone. Restart it with sudo systemctl restart cron (crond on RHEL-family systems).
- 3
When you want just one line in another time zone
cronie, used on RHEL-family systems, reads the lines after CRON_TZ=Asia/Tokyo in a crontab in that time zone. Standard cron on Debian and Ubuntu ignores it, though, so converting the time to UTC is the more reliable option.
The same offset happens with systems that read cron expressions outside your server, such as GitHub Actions, AWS EventBridge and Kubernetes. GitHub Actions is always UTC, so see running GitHub Actions cron on Japan time; for EventBridge, see how to write EventBridge cron expressions.
Make the next failure obvious
Once you’ve found and fixed the cause, setting things up so you notice the next failure straight away cuts investigation time dramatically.
- Always log the output: append >> /var/log/myjob.log 2>&1 to the line so errors are recorded too, and set up logrotate so the log doesn’t grow too large
- Set MAILTO: write MAILTO=you@example.com at the top of the crontab and any output is emailed to you (the server needs a way to send mail)
- Prevent overlapping runs: starting the next run before the previous one finishes causes problems. Take a lock, as in flock -n /tmp/myjob.lock command
- Monitor success from outside: hitting a monitoring service’s URL at the end of the job (a “heartbeat” check) means you’ll notice even when it doesn’t run at all
- Back up your crontab: run crontab -l > ~/crontab.bak regularly to keep a history of changes
To review crontab syntax and what the symbols mean, how to write crontab, with a cheat sheet will help. If you check the upcoming run times in the cron expression checker every time you change a setting, you can head off the “it ran at a different time than I thought” kind of trouble before it happens.
FAQ
- My crontab entry isn’t running. What should I check first?
- Start with cron’s log (grep CRON /var/log/syslog on Ubuntu, /var/log/cron on RHEL-family systems) and see whether there’s a CMD line at that time. If there is, the command is failing; if not, suspect a mistake in the expression or a stopped cron service. Temporarily adding “* * * * * date >> /tmp/cron-test.log” quickly tells you whether cron itself is running.
- Why does it work by hand but not from cron?
- cron runs commands in a minimal environment: PATH is roughly just /usr/bin:/bin, the shell is /bin/sh, and files like .bashrc aren’t read. Using absolute paths, setting needed environment variables inside the script, cd-ing into the working directory and escaping % as \% fixes it in most cases.
- Where can I find cron’s logs?
- On Ubuntu and Debian they go to /var/log/syslog, so filter with grep CRON /var/log/syslog. On RHEL, CentOS, Rocky Linux and similar it’s /var/log/cron. On systemd systems you can also use journalctl -u cron (-u crond on RHEL-family systems).
- Why does my cron job run 9 hours off?
- cron reads times in the server’s time zone, and the server is set to UTC. Japan time is 9 hours ahead of UTC, so a time written in Japan time runs 9 hours late. Change the time zone to Asia/Tokyo and restart cron, or write the time in UTC.
- A file I put in /etc/cron.d isn’t running.
- Files in /etc/cron.d need a user field between the five time fields and the command. On Debian and Ubuntu, a file is also skipped if its name contains a ., if it isn’t owned by root, or if other users can write to it. Check the logs for WRONG FILE OWNER or INSECURE MODE too.
Tools in this article
Related articles
Running GitHub Actions cron on Japan Time: UTC Conversion Table and Fixing Day Shifts and Delays
You wrote “0 9 * * 1-5” in your GitHub Actions schedule, but the workflow ran at 6 p.m. Japan time. That isn’t a typo: GitHub Actions always reads cron expressions in UTC (Coordinated Universal Time). This article covers how to convert a Japan Standard Time (JST) schedule into a UTC cron expression, with a ready-made table; why times between midnight and 8:59 a.m. shift the weekday and date back by one; how to split a schedule into two lines when one won’t do; and what to do when a schedule runs late or not at all.
Read article →How to Write crontab, with a Cheat Sheet: The Five Fields, Symbols, Common Examples and Commands
Recurring jobs like “back up every day at 3:00” or “run a monitoring script every 5 minutes” take a single line in crontab on Linux. But the five fields (minute, hour, day of month, month, day of week) and the symbols * , - / are easy to misread at first, and accidents are common: a job that runs 60 times an hour, or one meant to run every 7 minutes that ends up with a 4-minute gap. This article walks through the basics of crontab syntax and what each symbol means, a cheat sheet you can copy from, how to use crontab -e, -l and -r, and what to watch out for when writing the command part, mostly through examples.
Read article →cron’s Day-of-Month and Day-of-Week Are ORed: Writing Second Mondays, Month-End, Last Fridays and Every Other Week Correctly
You wrote “0 9 1-7 * 1” for “9:00 on the first Monday of every month”, and it ran every Monday plus every day from the 1st to the 7th. This very common cron mistake comes from a rule unique to cron: if you specify both the day of month and the day of week, the job runs on days that match either one. This article explains how the OR works and its exception, how to make crontab handle nth weekdays, month-end, last Fridays and every other week correctly, and how to use L, # and W in AWS EventBridge and Spring, with real run dates as examples.
Read article →