Cron Jobs on VPS Setup Correctly Without Missing Executions or Duplicates
Cron is the standard task scheduler on Linux used to run scripts automatically at specified times. However, there are subtle details that cause many people to misconfigure it and wonder why their cron jobs don't run at all.
Crontab Syntax
# ┌── Minute (0-59)
# │ ┌── Hour (0-23)
# │ │ ┌── Day of month (1-31)
# │ │ │ ┌── Month (1-12)
# │ │ │ │ ┌── Day of week (0-7, both 0 and 7 are Sunday)
# │ │ │ │ │
# * * * * * command
| Example | Meaning |
|---|---|
* * * * * | Every minute |
0 * * * * | Every hour (at minute 0) |
0 2 * * * | Every day at 2 AM |
0 2 * * 0 | Every Sunday at 2 AM |
0 2 1 * * | First day of every month at 2 AM |
*/15 * * * * | Every 15 minutes |
0 9,17 * * 1-5 | 9 AM and 5 PM Monday–Friday |
Editing Crontab
crontab -e # Edit crontab for current user
crontab -l # View existing crontab
crontab -r # Remove entire crontab (be careful!)
# Edit crontab for another user (requires root)
crontab -u www-data -e
Most Common Problem: PATH
Cron runs scripts in a very limited environment. The cron PATH is different from what you have when typing in a terminal. Many commands may not be found at all.
Solution: Always use absolute paths
# Wrong
0 2 * * * backup.sh
# Correct
0 2 * * * /usr/local/bin/backup.sh
# Or define PATH in crontab
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
0 2 * * * backup.sh
Logging: Know If Your Cron Really Ran
By default cron sends output via email (which is often not configured). Redirect to a log file instead:
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
2>&1 sends stderr together with stdout to the same log file
Prevent Cron Overlapping (Multiple Concurrent Runs)
If cron runs every 5 minutes but your script takes 10 minutes, the next run will start before the previous one finishes. Use flock to prevent this:
*/5 * * * * flock -n /tmp/myjob.lock /usr/local/bin/myjob.sh
-n means non-blocking — if it can't acquire the lock it skips the run instead of waiting
Using /etc/cron.d/ for System-Wide Jobs
Instead of adding cron to a user's crontab, you can create a file in /etc/cron.d/. You must specify the user:
# /etc/cron.d/myapp
0 2 * * * www-data /var/www/myapp/scripts/cleanup.sh >> /var/log/myapp-cleanup.log 2>&1
Systemd Timers: Alternative to Cron
Systemd timers have advantages over cron in some ways:
- View logs with
journalctlimmediately without manual logging setup - Set dependencies on other services
- Run jobs that were missed when the server reboots (using
Persistent=true)
# Create service file: /etc/systemd/system/mybackup.service
[Unit]
Description=Daily Database Backup
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/backup.sh
# Create timer file: /etc/systemd/system/mybackup.timer
[Unit]
Description=Run Daily Backup at 2am
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true # Run immediately if server was down at scheduled time
[Install]
WantedBy=timers.target
systemctl enable --now mybackup.timer
systemctl list-timers # View all timers
journalctl -u mybackup.service # View logs
Test Cron Environment Before Deploying
The easiest way is to simulate cron's environment:
# Run script with same environment as cron
env -i HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin \
bash /usr/local/bin/backup.sh
# If script runs successfully here, cron will run it too
Verify Cron Job is Working Correctly
After setting up cron, verify that the daemon is running and jobs are actually executing with these commands:
# 1. Is cron daemon running?
systemctl status cron # Debian/Ubuntu
systemctl status crond # CentOS/RHEL
# 2. View current user's crontab
crontab -l
# 3. Check logs to see if cron was triggered
grep CRON /var/log/syslog | tail -20 # Debian/Ubuntu
journalctl -u cron --since "10 min ago" # systemd
# 4. Test a job that writes output to file
echo '* * * * * date >> /tmp/cron-test.log' | crontab -
sleep 70 && cat /tmp/cron-test.log # Should see timestamp increasing every minute
grep CRON shows cron was called but the job didn't run, it's usually a PATH or script permission issue — add 2>&1 redirect to capture errors in a log file so you can see the actual causeCommon Issues & Troubleshooting
Cron Not Running Even Though Syntax is Correct
Check in this order:
# 1. View cron logs
grep CRON /var/log/syslog | tail -20
journalctl -u cron | tail -20
# 2. Verify cron daemon is running
systemctl status cron
# 3. Check if crontab was saved
crontab -l
# 4. Try running the script as the cron user (if using root)
sudo -u root bash /usr/local/bin/myscript.sh
Cron Sending Output via Email but Nobody Sees It
If you don't have a mail server configured, redirect to a log file or discard output:
# Redirect both stdout and stderr to log
0 2 * * * /script.sh >> /var/log/myjob.log 2>&1
# Discard all output (if you don't need logs)
0 2 * * * /script.sh > /dev/null 2>&1
# Send only errors to log
0 2 * * * /script.sh 2>> /var/log/myjob-errors.log
Cron Using Wrong Timezone, Not Running at Right Time
Cron uses the system timezone. Check it:
timedatectl # View current timezone
# To run cron on a different timezone
CRON_TZ=Asia/Bangkok
0 9 * * * /usr/local/bin/morning-report.sh
Script Runs Fine in Terminal but Produces Empty Output in Cron
Usually the script needs additional environment variables like HOME, USER, or database passwords. Define them in crontab:
MYSQL_PASS=secretpassword
HOME=/root
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Frequently Asked Questions
Should I use Cron or Systemd Timer?
Cron is simpler and comes pre-installed on every system. Systemd Timer is better if you need good logging, dependency control, or persistent scheduling (run missed jobs when server comes back online). For a simple daily backup, cron is fine. For critical tasks, systemd timer is recommended.
Why doesn't cron run exactly at midnight even though I set 0 0 * * *?
The cron daemon polls every minute and may have a small delay (1–59 seconds) — this is normal. For higher precision, use systemd timer with AccuracySec=1s
How do I stop a cron job that's already running?
You must kill the process directly:
ps aux | grep yourscript.sh # Find the PID
kill PID # Stop the process
Summary
90% of "cron won't run" problems stem from incorrect PATH. Always use absolute paths for all commands, enable logging from the start, and test your script in the same environment cron uses. If you need higher reliability, consider switching to systemd timer.