Cron is the standard task scheduler on Linux and macOS. It runs commands automatically on a schedule you define. Backups at 2 AM, log cleanup every Sunday, database optimization once a month. You write one line in a crontab file and the system handles the rest.
The syntax is compact but not intuitive. The five-field time expression looks like line noise until you learn what each field means. This guide breaks down the syntax, walks through common schedules with copy-paste examples, and covers the practical issues that cause cron jobs to silently fail.
The five-field cron syntax#
Every cron job is a single line with two parts: a time expression and a command.
* * * * * /path/to/command
The five asterisks represent five time fields, left to right:
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-7, where 0 and 7 are Sunday)
│ │ │ │ │
* * * * * command
An asterisk means “every.” So
* * * * *
means every minute of every hour of every day. Each field can be replaced with a specific value, a range, a list, or a step value.
Field values
| Field | Allowed values | Notes |
|---|---|---|
| Minute | 0-59 | |
| Hour | 0-23 | 0 is midnight, 13 is 1 PM |
| Day of month | 1-31 | |
| Month | 1-12 | Can also use names: jan, feb, etc. |
| Day of week | 0-7 | 0 and 7 are both Sunday. Can also use names: mon, tue, etc. |
Special characters
Asterisk (*) – matches every value in the field.
* * * * * # every minute
Comma (,) – specifies a list of values.
0 8,12,18 * * * # at 8:00, 12:00, and 18:00
Hyphen (-) – specifies a range.
0 9-17 * * * # every hour from 9:00 to 17:00
Slash (/) – specifies a step value. Used with ranges or asterisks.
*/5 * * * * # every 5 minutes
*/15 * * * * # every 15 minutes
0 */2 * * * # every 2 hours (at minute 0)
These can be combined:
0,30 9-17 * * 1-5 # at :00 and :30, 9 AM to 5 PM, Monday through Friday
Common schedules#
These are the schedules that cover the vast majority of real-world cron jobs.
Every minute
* * * * * /path/to/command
Use this sparingly. Running a command 1,440 times per day generates load and fills logs fast. Health checks and queue processors are the common legitimate use cases.
Every 5 minutes
*/5 * * * * /path/to/command
A common interval for checking services, processing queues, or running WordPress cron. The
*/5
means “every minute that is divisible by 5” (0, 5, 10, 15, …).
Every 15 minutes
*/15 * * * * /path/to/command
Every 30 minutes
*/30 * * * * /path/to/command
Note:
*/30
fires at minute 0 and minute 30 of every hour. It is not “every 30 minutes from when the cron job was created.” Cron does not track when you added the entry. It evaluates the time expression against the current system time.
Hourly
0 * * * * /path/to/command
Runs at minute 0 of every hour. If you use
* * * * *
thinking it means hourly, it runs every minute. The
0
in the minute field is what makes it hourly.
Daily at a specific time
0 2 * * * /path/to/command
Runs at 2:00 AM. The time is based on the server’s timezone. If your server is UTC and you want 2:00 AM Eastern, you need to calculate the offset.
Daily at midnight
0 0 * * * /path/to/command
Twice daily
0 6,18 * * * /path/to/command
Runs at 6:00 AM and 6:00 PM.
Weekly (every Sunday)
0 3 * * 0 /path/to/command
Runs at 3:00 AM every Sunday. You can use
7
instead of
0
for Sunday, or use day names:
0 3 * * sun /path/to/command
Weekly (every Monday)
0 3 * * 1 /path/to/command
Weekdays only (Monday through Friday)
0 9 * * 1-5 /path/to/command
Runs at 9:00 AM, Monday through Friday.
Monthly (first of the month)
0 4 1 * * /path/to/command
Runs at 4:00 AM on the 1st of every month.
Quarterly
0 4 1 1,4,7,10 * /path/to/command
Runs at 4:00 AM on the 1st of January, April, July, and October.
Yearly (January 1st)
0 0 1 1 * /path/to/command
Runs at midnight on January 1st.
Editing the crontab#
crontab -e
To edit the current user’s cron jobs:
crontab -e
This opens the crontab file in your default editor (usually nano or vi). Add one job per line. Save and exit. The cron daemon picks up changes automatically – no restart needed.
If it is your first time, the system may ask which editor to use. Choose nano if you are not familiar with vi.
crontab -l
To list all cron jobs for the current user:
crontab -l
This prints the contents of the crontab to the terminal. If no crontab exists, it prints “no crontab for username.”
Edit another user’s crontab
To edit cron jobs for a specific user (requires root):
sudo crontab -u www-data -e
To list another user’s cron jobs:
sudo crontab -u www-data -l
This is common for web server tasks. PHP scripts that need to run on a schedule should typically be in the
www-data
user’s crontab (or whatever user PHP-FPM runs as) so they have the correct file permissions.
Remove all cron jobs
crontab -r
This deletes the entire crontab for the current user with no confirmation prompt. There is no undo. If you want to temporarily disable all jobs without deleting them, comment them out with
#
in
crontab -e
instead.
Back up the crontab
Before making significant changes:
crontab -l > ~/crontab-backup.txt
To restore:
crontab ~/crontab-backup.txt
System crontab vs user crontab#
There are two types of crontab files on a Linux system.
User crontabs are edited with
crontab -e
. Each user has their own. Commands run as that user. These are stored in
/var/spool/cron/crontabs/
(Ubuntu/Debian) or
/var/spool/cron/
(CentOS/RHEL). Do not edit these files directly.
The system crontab is
/etc/crontab
. It has a sixth field between the time expression and the command: the username to run the command as.
0 2 * * * root /usr/local/bin/backup.sh
The
root
between the schedule and the command specifies which user executes it. User crontabs do not have this field because the user is implicit.
Additionally, scripts placed in
/etc/cron.daily/
,
/etc/cron.hourly/
,
/etc/cron.weekly/
, and
/etc/cron.monthly/
directories run on those schedules automatically without needing crontab entries. The scripts must be executable and should not have file extensions.
Environment variables in cron#
Cron jobs run in a minimal environment that is different from your interactive shell. This is the single most common reason cron jobs fail. A command that works perfectly when you run it manually in SSH fails silently when cron runs it.
PATH is minimal
When you log in via SSH, your shell loads
~/.bashrc
,
~/.bash_profile
, and
/etc/profile
, which set up your PATH to include directories like
/usr/local/bin
,
/home/user/.local/bin
, and others. Cron does not load these files. Its default PATH is typically just:
/usr/bin:/bin
A command like
wp cron event run
works in your shell because WP-CLI is installed in
/usr/local/bin/wp
, which is in your PATH. Cron cannot find it because
/usr/local/bin
is not in cron’s PATH.
Fix: use full paths to commands.
# Bad - cron may not find the command
0 2 * * * wp cron event run --due-now
# Good - full path to the binary
0 2 * * * /usr/local/bin/wp cron event run --due-now
To find the full path of a command:
which wp
Or set PATH at the top of the crontab:
PATH=/usr/local/bin:/usr/bin:/bin
0 2 * * * wp cron event run --due-now
Variables set at the top of the crontab apply to all jobs below them.
SHELL defaults to /bin/sh
Cron uses
/bin/sh
by default, not bash. If your command uses bash-specific syntax (arrays,
[[
conditionals, process substitution), it will fail. Either set the shell in the crontab:
SHELL=/bin/bash
Or invoke bash explicitly:
0 2 * * * /bin/bash /path/to/script.sh
MAILTO
By default, cron emails the output of every job to the crontab owner. If the mail system is not configured (common on most servers), these emails queue up silently and waste disk space.
To disable email notifications:
MAILTO=""
To send to a specific address:
MAILTO="admin@example.com"
Place these at the top of the crontab, before any job entries.
Setting custom variables
You can define any environment variable at the top of the crontab:
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
WP_PATH=/var/www/html
0 2 * * * cd $WP_PATH && wp cron event run --due-now
Logging cron output#
By default, cron captures the standard output and standard error of each job and tries to email it. If you are not receiving those emails (you probably are not), the output is lost.
Redirect output to a log file
0 2 * * * /path/to/backup.sh >> /var/log/backup.log 2>&1
>>
appends standard output to the log file.
2>&1
redirects standard error to the same file. This captures both normal output and error messages.
Use
>>
(append) rather than
>
(overwrite) so you keep a history of runs rather than only the most recent one.
Log with timestamps
Cron itself does not add timestamps to output. Add them in the command:
0 2 * * * echo "$(date): Starting backup" >> /var/log/backup.log 2>&1 && /path/to/backup.sh >> /var/log/backup.log 2>&1
Or better, add timestamps inside the script itself.
Discard output entirely
If you do not care about the output and want to suppress the email:
0 2 * * * /path/to/command > /dev/null 2>&1
This sends both stdout and stderr to
/dev/null
. Only do this for commands you trust to work reliably, because you will never know if they start failing.
Check the system log
Cron logs job execution (but not output) to the system log. On Ubuntu:
grep CRON /var/log/syslog
On CentOS/RHEL:
grep CRON /var/log/cron
This shows when each job started. It does not show whether the job succeeded or what it output.
Common mistakes#
Forgetting that cron uses a different PATH
Already covered above, but worth repeating because it is the number one cause of cron job failures. If a command works in your shell but not in cron, it is almost always a PATH issue.
Missing execute permission on scripts
If your cron job runs a script:
chmod +x /path/to/script.sh
Without execute permission, cron cannot run the script. You will see “Permission denied” in the system log.
Incorrect working directory
Cron jobs start in the user’s home directory, not in the directory where the script lives. If your script references relative file paths, it will look for them relative to the home directory.
# Bad - relative path depends on working directory
0 2 * * * ./backup.sh
# Good - absolute path or explicit cd
0 2 * * * /home/user/scripts/backup.sh
0 2 * * * cd /home/user/scripts && ./backup.sh
For WordPress WP-CLI commands, always
cd
to the WordPress directory first:
0 2 * * * cd /var/www/html && /usr/local/bin/wp cron event run --due-now
Jobs overlapping
If a cron job takes longer to run than the interval between runs, you get multiple instances running simultaneously. A backup that takes 20 minutes on a 15-minute schedule results in overlapping runs competing for resources.
Use
flock
to prevent overlapping:
*/15 * * * * /usr/bin/flock -n /tmp/mybackup.lock /path/to/backup.sh
The
-n
flag makes flock exit immediately if the lock is already held. The job simply skips that interval instead of queuing up.
Editing /var/spool/cron directly
Never edit crontab files directly in
/var/spool/cron/
. Always use
crontab -e
. The
crontab -e
command validates syntax before saving and notifies the cron daemon of changes. Direct file edits skip validation and may not be picked up by the daemon until it is restarted.
Percent signs in commands
In crontab, the
%
character is interpreted as a newline. If your command contains a literal percent sign (common in date format strings), it breaks the command.
# Bad - % is interpreted as newline
0 2 * * * /path/to/command --date=$(date +%Y-%m-%d)
# Good - escape the percent signs
0 2 * * * /path/to/command --date=$(date +\%Y-\%m-\%d)
Or put the command in a script and call the script from cron, which avoids the percent sign issue entirely.
Practical examples#
WordPress cron replacement
Replace WordPress’s built-in wp-cron with a real cron job. In wp-config.php, disable the web-triggered cron:
define('DISABLE_WP_CRON', true);
Then add to the server crontab:
*/5 * * * * cd /var/www/html && /usr/local/bin/wp cron event run --due-now >> /var/log/wp-cron.log 2>&1
This runs pending WordPress scheduled tasks every 5 minutes. It is more reliable than wp-cron.php because it does not depend on site traffic and does not add overhead to page loads.
Database backup
0 3 * * * /usr/bin/mysqldump -u backup_user wordpress | /bin/gzip > /backups/wordpress-$(date +\%Y\%m\%d).sql.gz 2>> /var/log/backup-errors.log
Runs at 3:00 AM daily. Dumps the WordPress database, compresses it, and saves with a date-stamped filename.
rsync backup on a schedule
0 4 * * * /usr/bin/rsync -az --delete /var/www/html/ /backups/website/ >> /var/log/rsync-backup.log 2>&1
Runs at 4:00 AM daily. Syncs the website directory to a backup location. Only changed files are transferred on subsequent runs. See How to sync directories with rsync for the full breakdown of rsync flags and directory sync patterns.
Clean up old log files
0 5 * * 0 /usr/bin/find /var/log/myapp/ -name "*.log" -mtime +30 -delete
Runs at 5:00 AM every Sunday. Deletes log files older than 30 days.
Remote command via SSH
0 2 * * * /usr/bin/ssh user@example.com "cd /var/www/html && wp cron event run --due-now" >> /var/log/remote-cron.log 2>&1
Runs a command on a remote server at 2:00 AM. Requires SSH key authentication since cron cannot enter a password. See How to run commands over SSH for syntax details and how to handle quoting and environment variables in remote commands.
Cron jobs on Hostney#
On Hostney, cron jobs are managed through the control panel rather than through SSH and
crontab -e
. Access the scheduler under the Cron Jobs section in the control panel.
You set the schedule using the same five-field syntax covered in this guide, and the command runs inside your account’s isolated container with the correct PATH and permissions already configured. Output and errors are visible in the control panel’s logs.
For SSH access to your container (useful for testing commands before scheduling them), see the Terminal Access section in the control panel.