You've registered a cron job, crontab -l shows it too, but nothing happens. No email arrives, no file is created, no log is written. Just silence. This silence is the most dangerous part of the story, because by default cron doesn't record any error anywhere you can see. What you need to do in this situation isn't to rewrite the job; you need to figure out whether cron ran at all.
First, prove whether the cron job runs or not
Before any changes, create a test job that has no dependency on the main script:
*/1 * * * * /bin/date >> /tmp/cron-test.log 2>&1
Wait a minute and then run cat /tmp/cron-test.log. If the file is filled, the cron service is healthy and the problem is inside your script. If the file isn't created, the problem is at the service layer: either crond isn't running on the server, or the user you logged in with isn't the same user the cron was registered for.
Here's where people make a mistake: the user registers the job in root's table with sudo crontab -e, then checks the output file with a normal user and, since the file isn't there, concludes cron is broken. In reality the job ran, but with root access and wrote to a different path. Always check with crontab -l -u username exactly which user's name the job is sitting under.
Absolute path, the silent killer of cron jobs
Cron runs with an almost empty environment. PATH is usually just /usr/bin:/bin, and variables like HOME or SHELL may not be set. That means when you write python3 manage.py in your script, cron doesn't know where python3 is, because it's not in its PATH. And the error goes nowhere.
The fix is simple: define an explicit PATH at the top of the crontab, and write the full path to the binary in the job itself.
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
*/5 * * * * /usr/bin/php /home/user/public_html/cron.php >> /home/user/cron.log 2>&1
A point that's rarely mentioned: % has a special meaning in crontab and turns into a newline. If you use date +%Y-%m-%d in the job command, cron splits it into pieces and the command fails with a strange error. You must escape % with a backslash: date +\%Y-\%m-\%d.
Where are you sending the output?
Cron sends every standard output and every standard error to the user's local email. If an MTA isn't installed on the server, that email sits in the queue or gets discarded, and you never find out. You have two options: either send the output to a file, or pump it into syslog.
*/10 * * * * /home/user/backup.sh 2>&1 | /usr/bin/logger -t mycron
Then watch the error live with journalctl -t mycron -f or grep mycron /var/log/syslog. This method is worth it when your script has several stages and you don't know which stage is failing. The cost is that logs accumulate on disk and, if you don't have log rotation (logrotate), they'll fill up the partition space after a few months. For high-frequency jobs, manage the log file with logrotate or use logger, which handles rotation itself.
Why the WordPress cron job doesn't run
In WordPress the story is one layer more complicated. By default, WordPress uses wp-cron.php, which is neither a real cron nor guaranteed; it only runs when a visitor comes to the site. If the site's traffic is low or the site is cached, scheduled jobs fall hours behind. The sign is that scheduled emails arrive late or backup plugins don't run at all.
The right way is to disable wp-cron and replace it with a real system cron. First add this line to wp-config.php:
define('DISABLE_WP_CRON', true);
Then create a system job that runs every five minutes with WP-CLI:
*/5 * * * * /usr/local/bin/wp cron event run --due-now --path=/home/user/public_html --quiet
If you're not familiar with WP-CLI, essential WP-CLI commands for managing WordPress from the terminal is a good starting point. And if you're not sure whether the problem is cron or WordPress itself, the WordPress wp-cron guide shows the difference between the two with examples.
A few reasons you look at less often
- Execute permission: the script must have
chmod +x, otherwise cron dies silently with a "Permission denied" error. - Windows line endings: if you saved the file with a Windows editor,
\r\nat the end of the first line makes cron read the binary name with an extra character and fail. Check withfile script.shorcat -A. - File lock: if the previous run hasn't finished yet and the next job arrives, two instances work on the same file at the same time. Prevent this with
flock. - Time zone: cron uses the system clock, not your local clock. If the server is on UTC and you've set the job for 2 AM Tehran time, you have a six-and-a-half-hour difference.
For sensitive jobs, take flock seriously:
*/15 * * * * /usr/bin/flock -n /tmp/backup.lock /home/user/backup.sh
The -n switch means if the lock isn't free, don't run and skip silently. This is exactly the right behavior you want for backups.
Which method should I choose?
| Method | Good for | The cost you pay |
|---|---|---|
| User crontab | Personal scripts, backups, temp file cleanup | Empty environment, need for absolute paths |
| systemd timer | Jobs that need structured logs and retry | Writing a unit file, more complexity |
| WordPress wp-cron | High-traffic sites with non-critical jobs | Unreliable execution dependent on visits |
| System cron + WP-CLI | Any WordPress site where scheduling matters | Need for SSH access and initial setup |
If I had only one choice, for every critical job I'd prefer a systemd timer, because journalctl keeps the output of every run with a timestamp and OnCalendar is testable. But for 90% of everyday tasks, crontab is enough and gets going faster. Just write paths as absolute and send the output somewhere you can read later.
If you're on shared hosting and don't have access to systemd or system logs, your options are more limited. In Linux hosting, crontab is usually configurable from the control panel, and the same rules of absolute paths and output redirection still apply. For a quick check of whether the problem is the network or DNS, ServerNet's free tools shorten the work, and if you see a DNS error, the DNS_PROBE_FINISHED_NXDOMAIN error guide explains the diagnostic path step by step.
Frequently asked questions
Why does my cron job show in crontab -l but not run?
The most common reason is an empty PATH in the cron environment. Cron runs with a much more limited environment than your interactive shell and can't find binaries like python3 or node. Write the full path to the binary in the job and add a PATH= line at the top of the crontab.
The second reason is registering the job under the wrong user. Check with crontab -l -u username exactly which user the job is registered for, and read the output file with that same user.
How do I know the cron job ran but errored?
Redirect standard output and error to a file: >> /home/user/cron.log 2>&1. Without this redirection, cron sends the output to local email and, if an MTA isn't installed, you lose it. For high-frequency jobs, sending to logger and reading with journalctl is cleaner.
Does WordPress wp-cron replace a real cron?
No. wp-cron only runs when someone visits the site, so on low-traffic or cached sites jobs run with a big delay. For reliable scheduling, set DISABLE_WP_CRON to true and create a system job with WP-CLI.
What's the difference between crontab and systemd timer, and which should I choose?
crontab is simpler and enough for most tasks, but it has no structured logs or retry mechanism. A systemd timer keeps the output of every run in the journal, supports dependencies between services, and its scheduling is testable with systemd-analyze calendar. If you have a critical job and need to know whether the last run succeeded, a systemd timer is the right choice.
Next step: right now, create a test job with /bin/date and make sure its output is written somewhere. Until this loop is closed, every other job you add is just a guess.
Comments 0
No comments yet — be the first!