schedule and clock automation

Cron Jobs on VPS Setup Correctly Without Missing Executions or Duplicates

June 2026 · 6 min read

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
ExampleMeaning
* * * * *Every minute
0 * * * *Every hour (at minute 0)
0 2 * * *Every day at 2 AM
0 2 * * 0Every Sunday at 2 AM
0 2 1 * *First day of every month at 2 AM
*/15 * * * *Every 15 minutes
0 9,17 * * 1-59 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:

# 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
Tip: If 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 cause

Common 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.