Why your cron job isn't running on Ubuntu or Debian, and how to fix it

Backups that stopped, a Laravel scheduler that never fires, a WordPress cron that does nothing. The command works when you type it, but not from cron. Here are the reasons we found, and how to spot each one.

We tested every case in this guide on Debian 13 with its standard cron package. We set up working and broken jobs side by side, let cron run them, and copied what happened and what cron logged. Ubuntu uses the same cron.

1. Look at the cron log first

Cron writes a line each time it starts a job, and it complains about files it can't read:

sudo journalctl -u cron --since "1 hour ago"

If your server writes /var/log/syslog, you can also use grep CRON /var/log/syslog. What to look for:

  • No CMD line for your job at all: cron never started it. See steps 2 and 3.
  • (root) CMD (...) but nothing happened: cron started it, but the command failed. See steps 4 to 6.
  • grandchild ... failed with exit status 127: the command wasn't found (step 4).
  • grandchild ... failed with exit status 126: the script isn't allowed to run (step 5).

Also check that cron itself runs: systemctl status cron.

2. Files in /etc/cron.d have strict rules

Many guides and apps put a file in /etc/cron.d/. Cron is picky about these files, and in our test it ignored a broken file completely. Not just the bad line, but every job in it.

  • No dots in the file name. Our /etc/cron.d/my.job never ran, and cron wrote nothing in the log about it. After we renamed it to myjob, it ran the next minute, without a restart. Use only letters, numbers, - and _. That also means no .cron or .conf at the end.
  • A user name after the time. In /etc/cron.d and /etc/crontab, each line needs the user that runs the job:
    */5 * * * * www-data php /var/www/example.com/cron.php
    Without it, the log said Error: bad username and Syntax error, this crontab file will be ignored.
  • An empty line at the end. If the last line has no line break after it, the log said Missing newline before EOF, this crontab file will be ignored. Most editors add it. echo -n and some upload tools don't.
  • Only root may change it. A file others can write to gave INSECURE MODE (group/other writable), and its job never ran. Fix it with sudo chown root: /etc/cron.d/myjob && sudo chmod 644 /etc/cron.d/myjob.

The same name rule applies to /etc/cron.daily (and hourly, weekly, monthly). These folders run scripts by name, and a script called backup.sh is skipped. Check which scripts will really run:

run-parts --test /etc/cron.daily

In our test, it listed backup2 but not backup.sh, and not a script without the "run" permission. Rename it without the dot, and make it runnable with chmod +x.

3. Is it in the right crontab?

There are two kinds of crontab:

  • Your own crontab (crontab -e). It has no user column, and jobs run as you. See it with crontab -l, or another user's with sudo crontab -l -u www-data.
  • System files (/etc/crontab and /etc/cron.d/*). They need the user column (step 2).

A line copied from one kind into the other breaks. In your own crontab, a user name would be read as the command.

4. "not found": cron's PATH is very short

When you log in, your shell knows many folders to look for programs in. Cron doesn't. In our test, jobs in /etc/cron.d and in a user's crontab had only:

PATH=/usr/bin:/bin

So a tool in /usr/local/bin (where many installers put things, like Composer or WP-CLI) failed with /bin/sh: 1: mytool: not found. Only /etc/crontab sets a longer PATH. There are two ways to fix it:

  • Use the full path in the job, like /usr/local/bin/wp. Find it with command -v wp.
  • Or set the PATH at the top of your cron file. In our test, this made the job work:
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    */5 * * * * www-data wp --path=/var/www/example.com cron event run --due-now

Cron also uses /bin/sh, not bash. Bash-only things like source or [[ ]] may fail. Put them in a script that starts with #!/bin/bash.

5. "Permission denied": the script can't run

If the job calls a script directly, like /opt/backup.sh, the script needs the "run" permission. Without it, the job failed with Permission denied (exit status 126):

sudo chmod +x /opt/backup.sh

Also check that the user in the job can read the files it needs. A job running as www-data can't read root's files.

6. The command is cut off at a % sign

In a crontab, % means "new line". Everything after the first % is cut off. In our test, date +%Y-%m-%d ran as just date +, and the log showed exactly that. Put a backslash in front of each one:

0 3 * * * root mysqldump mydb > /backup/db-$(date +\%F).sql

With \%, our job wrote the date as expected. This often breaks backup jobs that put the date in the file name.

7. It runs, but in the wrong folder

Cron starts each job in the user's home folder, not in your website's folder. In our test, a PHP script that wrote out.txt put it in /root/, not next to the script. Use full paths in your scripts, or change folder first:

* * * * * www-data cd /var/www/example.com && php artisan schedule:run

Save the output, or you'll never see the error

Cron emails the output of each job to the user. Most servers have no mail program, so in our test the output was simply thrown away, with no note in the log. Send it to a file instead:

0 3 * * * root /usr/local/bin/backup >> /var/log/backup-cron.log 2>&1

The 2>&1 part matters: errors go to the same file. If you write logs like this, make sure they can't grow forever: see stop logs filling the disk.

Stop slow jobs from piling up

If a job runs every minute but sometimes takes longer, two copies end up running at the same time. flock skips the new run while the old one is still busy:

* * * * * root flock -n /run/lock/import.lock /usr/local/bin/import

In our test, a job that took 90 seconds ran once, and the next minute's run was skipped instead of starting a second copy.

Test a job the way cron runs it

Instead of waiting, run the command with cron's short PATH, as the right user, from the home folder:

sudo -u www-data env -i HOME=/var/www PATH=/usr/bin:/bin sh -c 'your command here'

If it fails here, it will fail in cron too. For a quick real test, set the job to * * * * *, watch journalctl -u cron -f, then put the real time back.

Know when it stops

A cron job that stops running doesn't tell anyone. For backups and other important jobs, use a "heartbeat" check: the job calls a URL when it's done, and you get an alert when the call doesn't come. Our uptime monitoring guide shows how.

Quick list

  • No CMD line in the log: check the file name (no dots), the user column, the last newline and the file's owner.
  • Status 127 / not found: use full paths, or set PATH= at the top of the file.
  • Status 126 / Permission denied: chmod +x the script.
  • Command cut short: write \% instead of %.
  • Files end up in the wrong place: cd to the right folder first.
  • No idea what went wrong: add >> /var/log/myjob.log 2>&1.
  • Script in cron.daily never runs: remove the dot from its name, then check with run-parts --test.

Official sources

Next steps: set up the Laravel scheduler or a real cron for WordPress, and back up your site and test the restore.

ServerNest is being built to set up scheduled tasks for each site and show when one fails or stops running. Join early access to try it first.

← Back to the security blog