DEV Community

delay.run
delay.run

Posted on

Cron job not running? A step-by-step checklist (including “it works when I run it manually”)

You added a line to your crontab, waited, and nothing happened. Or the script works perfectly when you run it in your terminal, but cron never seems to run it. This is one of the most common Linux problems, and it nearly always comes down to a short list of causes. Work through them in this order and you will find it.

1. Is the cron daemon actually running?

Start with the obvious. On Debian and Ubuntu the service is called cron; on RHEL, Fedora, Rocky and Alma it is crond.

systemctl status cron      # Debian / Ubuntu
systemctl status crond     # RHEL family
Enter fullscreen mode Exit fullscreen mode

If it is inactive, start and enable it:

sudo systemctl enable --now cron
Enter fullscreen mode Exit fullscreen mode

Inside Docker containers there is usually no cron daemon at all unless you install and start one yourself. A crontab file in an image does nothing on its own.

2. Did cron try to run the job?

Cron logs every job it starts. Check the log before you change anything else, because it tells you whether the problem is the schedule or the command.

grep CRON /var/log/syslog          # Debian / Ubuntu
sudo tail -f /var/log/cron         # RHEL family
journalctl -u cron --since "1 hour ago"
Enter fullscreen mode Exit fullscreen mode
  • No line for your job at the expected time: the schedule or the crontab file is wrong (steps 3 and 4).
  • A line saying CMD (...) but nothing happened: cron ran it and the command failed (steps 5 to 8).

3. Check the schedule syntax

A standard crontab line has five time fields, then the command:

# m   h   dom mon dow   command
*/5   *   *   *   *     /home/app/bin/sync.sh
Enter fullscreen mode Exit fullscreen mode

Common mistakes:

  • Six fields. Standard cron has no seconds field. 0 */5 * * * * is valid in Spring or Quartz, but in crontab the sixth value becomes part of the command.
  • Day-of-month and day-of-week both set. 0 9 1 * 1 runs on the 1st of the month and on every Monday, not only on Mondays that fall on the 1st.
  • Expecting */7 in the minute field to mean "every 7 minutes forever". It restarts every hour, so you get :00, :07 … :56, then :00 again.

Paste the expression into a cron explainer if you are unsure, and confirm it says what you think.

4. Check which crontab you edited

There are several places cron reads from, and they use different formats:

  • crontab -e edits your user's crontab. No user column.
  • /etc/crontab and files in /etc/cron.d/ have an extra user column after the five time fields: 0 3 * * * root /usr/local/bin/backup.sh. Leave it out and the job silently fails.
  • Files in /etc/cron.d/ are ignored if the file name contains a dot (on Debian-based systems), so backup.cron is skipped while backup works. They also need to be owned by root and not group- or world-writable.
  • sudo crontab -e edits root's crontab, which is a different file from yours. Run crontab -l and sudo crontab -l to see both.

Also make sure the file ends with a newline. Some cron implementations ignore the last line if it doesn't.

5. “It runs manually but not in cron”: the environment

This is the big one. When you run a script in your terminal, you have your shell profile, your PATH, your virtualenv and your current directory. Cron has almost none of that. Its PATH is typically just /usr/bin:/bin, its shell is /bin/sh, and it starts in your home directory.

So a script that calls node, python3 from a virtualenv, aws, docker or anything in /usr/local/bin may simply not find the binary.

Fixes, in order of preference:

# Use absolute paths everywhere
*/5 * * * * /usr/local/bin/node /home/app/jobs/sync.js

# Or set PATH at the top of the crontab
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Enter fullscreen mode Exit fullscreen mode

If the script needs environment variables (API keys, DATABASE_URL), cron will not load them from .bashrc. Source a file explicitly or set them in the script.

To reproduce cron's environment in your terminal and see the real error:

env -i SHELL=/bin/sh PATH=/usr/bin:/bin HOME="$HOME" /bin/sh -c '/home/app/bin/sync.sh'
Enter fullscreen mode Exit fullscreen mode

6. Relative paths and the working directory

A script that reads ./config.yml or writes logs/out.txt works from the project folder but not from cron, which starts in $HOME. Either cd first or use absolute paths:

0 * * * * cd /home/app && ./bin/report.sh
Enter fullscreen mode Exit fullscreen mode

7. Percent signs and permissions

In a crontab line, % means "newline" and everything after the first one is sent to the command as standard input. This breaks date +%Y-%m-%d in a surprising way. Escape it as \%:

0 2 * * * /usr/bin/tar czf /backups/site-$(date +\%F).tgz /var/www
Enter fullscreen mode Exit fullscreen mode

Then check the script itself: is it executable (chmod +x), does it start with a correct shebang (#!/usr/bin/env bash), and can the cron user read every file it touches?

8. Capture the output

By default, cron emails output to the local user, which on most servers means it disappears. Redirect it to a file so you can see what happened:

*/5 * * * * /home/app/bin/sync.sh >> /home/app/logs/sync.log 2>&1
Enter fullscreen mode Exit fullscreen mode

The 2>&1 matters: most errors go to standard error. If you have a working mail setup, MAILTO=you@example.com at the top of the crontab sends output by email instead.

9. Time zone and reboot surprises

Cron uses the server's time zone. A job set for 0 9 * * * on a server in UTC runs at 9:00 UTC, not 9:00 where you are. Check with timedatectl. Some cron versions support CRON_TZ=Europe/Berlin at the top of the file.

@reboot jobs run when the cron daemon starts, which can be before the network or your database is ready. Add a short wait or a retry loop in the script.

The real problem: cron fails silently

Once your job works, there is a second issue: the next time it breaks, nothing will tell you. A crontab that stops gives no output and no error. The usual ways to cover that are:

  • Log to a file and have something watch the file or the job's last run time.
  • Use a "dead man's switch" style monitor that alerts when a job fails to check in.
  • Move the schedule off the server entirely and have a hosted scheduler call an HTTP endpoint in your app, so retries, run history and alerts live in one place.

If you go the HTTP route, delay.run is one option: it calls your URL on a cron schedule, logs each response and alerts you by email, Slack or Discord when runs keep failing.

Whichever you choose, the checklist above will get the job running. Making sure you hear about it next time it stops is what saves you the Monday-morning surprise.

Top comments (0)