You added a cron job, the time passed, and nothing happened. Or the script works when you run it by hand but fails from cron. Cron problems almost always come down to a short list of causes. Work through them in order.
Cron basics: crontab guide and cron expression examples.
1. Is cron running?
systemctl status cron # Debian/Ubuntu
systemctl status crond # RHEL/AlmaLinux/CentOS
sudo systemctl enable --now cron
Minimal containers often don’t run cron at all.
2. Did cron try to run it? Check the logs
grep CRON /var/log/syslog | tail -20 # Debian/Ubuntu
sudo tail /var/log/cron # RHEL family
journalctl -u cron --since "1 hour ago"
If you see CMD (…) lines for your job, cron ran it, and the problem is the command itself (continue below). No entries means the schedule or crontab is wrong.
3. Crontab syntax
crontab -l
- Five time fields, then the command:
*/15 * * * * /usr/local/bin/sync.sh. - System crontabs (
/etc/crontab,/etc/cron.d/*) have an extra user field:*/15 * * * * root /usr/local/bin/sync.sh. Forgetting it is a common mistake. Using it in a user crontab is another. - Files in
/etc/cron.d/must have valid names (no dots or extensions like.shor.conf), be owned by root, and not be group- or world-writable. - The crontab file must end with a newline.
Test schedules with crontab.guru-style tools, or see cron expression examples.
4. PATH and environment
Cron runs with a minimal environment: PATH=/usr/bin:/bin, no .bashrc, no variables from your login shell. Commands in /usr/local/bin, Node.js via nvm, Python virtualenvs, Composer and so on aren’t found.
Fixes:
- use absolute paths:
/usr/local/bin/node /opt/app/job.js; - set
PATH=at the top of the crontab; - load what you need explicitly in the script.
See cron environment variables.
5. Capture the output
By default, output is mailed to the user (often nowhere on a VPS). Log it instead:
*/15 * * * * /usr/local/bin/sync.sh >> /var/log/sync.log 2>&1
Then read the log. The error message usually explains everything. See pipes and redirection.
6. Permissions
- The script must be executable:
chmod +x /usr/local/bin/sync.sh, with a correct shebang (#!/bin/bash). - The cron user needs access to the files and directories the script uses.
- Is the user allowed to use cron? Check
/etc/cron.allowand/etc/cron.deny.
7. The % character
In crontab lines, % means “newline”, so date +%F breaks. Escape it:
0 2 * * * /usr/bin/tar czf /backups/site-$(date +\%F).tgz /var/www/site
Or move the logic into a script.
8. Different shell
Cron uses /bin/sh (dash on Debian/Ubuntu), not bash. Bash-only syntax ([[ ]], arrays, source) fails. Put SHELL=/bin/bash in the crontab, or call a bash script with a proper shebang.
9. Time zone and time
Cron uses the system time zone. If the server runs on UTC and you scheduled for local time, the job runs at a different hour than expected.
timedatectl
Set CRON_TZ=Europe/Berlin in the crontab (supported by cronie and recent Debian cron), or adjust the schedule. See timedatectl and time zones.
10. The job overlaps or hangs
A job that takes longer than its interval piles up. Use flock:
*/5 * * * * /usr/bin/flock -n /tmp/sync.lock /usr/local/bin/sync.sh
11. Server was off at the scheduled time
Cron doesn’t catch up on missed runs. For machines that aren’t always on, use anacron, or systemd timers with Persistent=true. See anacron and systemd timers.
12. Edited the wrong crontab
crontab -e as your user and sudo crontab -e (root) are different files. Check both: crontab -l and sudo crontab -l, plus /etc/cron.d/.
Quick checklist
| Check | Command |
|---|---|
| Service | systemctl status cron |
| Did it run? | grep CRON /var/log/syslog |
| Output | >> /var/log/job.log 2>&1 |
| Paths | Absolute paths / PATH= line |
| Shell | SHELL=/bin/bash |
% |
Escape as \% |
| Overlaps | flock -n |
Related: Bash scripting tutorial and cron jobs for web apps.