How to Create a Cron Job: Crontab Syntax, Examples and Fixes

What is a cron job and how do you create one?
A cron job is a scheduled task that the cron service runs automatically at the minute, hour, day of month, month and weekday you define on Linux and Unix-like systems. To create one, you open your table with crontab -e, add a single line with five time fields and the full path of the command, then save.
In practice, recurring work runs without anyone touching a keyboard thanks to cron. Think nightly backups, morning reports, cache cleanup or triggering WordPress events on a fixed schedule. For example, an online store often relies on cron for stock syncs, abandoned cart emails and sitemap generation. So when cron breaks, a lot of work you assumed was "automatic" quietly stops.
In this guide we cover the syntax, the crontab commands, environment pitfalls, the cPanel interface, the WordPress wp-cron switch and how flock prevents overlapping runs. We are a digital marketing and web team, not a hosting company. That is why we base every command on official documentation such as the crontab(5) manual page. Still, the man 5 crontab output on your own distribution has the final word, because cron implementations differ in small ways.
How does cron work behind the scenes?
Cron is a service that starts at boot and waits in the background. Then, every minute, it checks its schedule tables and runs the lines that match that minute. Those tables are called crontabs, short for "cron tables".
Each user can also have a personal crontab. In addition, there is a system-wide /etc/crontab file plus the files inside /etc/cron.d/. You do not open a user crontab like a regular text file. Instead, you use the crontab command. It checks the syntax after you edit and tells the service about the new table.
Every active line in a crontab is one of two things: an environment variable assignment or a scheduled command. Lines starting with a hash (#) are comments, and cron skips them. That means you can also document each job right above it.
The manual page also stresses a detail that trips people up. Specifically, cron requires every entry to end with a newline character. If you do not press Enter after the last line, that job may never run.
In short, the logic is simple. The service watches the clock and the table says what runs when. Almost every problem comes from how the line is written or from the command behaving differently inside cron's environment.
How do you read crontab syntax?
In practice, a user crontab line has five time fields followed by the command. You read the fields from left to right in this order:
- Minute: 0 to 59.
- Hour: 0 to 23.
- Day of month: 1 to 31.
- Month: 1 to 12, or the first three letters of the month name (jan, feb and so on).
- Day of week: 0 to 7, where both 0 and 7 mean Sunday. Names such as mon or tue also work.
# minute hour day-of-month month day-of-week command30 3 * * * /usr/local/bin/nightly-backup.shThe line above runs the script every day at 3:30 AM. The asterisk means "every value in this field". As a result, day of month, month and weekday stay unrestricted.
However, one rule in the manual surprises many people. If you restrict both day of month and day of week, cron runs the job when either one matches. For instance, 0 9 13 * 5 does not mean "Friday the 13th". It fires on the 13th of every month and on every Friday at 9:00. If you need that kind of condition, check the date inside your script instead.
What do the asterisk, comma, hyphen and slash mean?
Four special characters make the time fields flexible. You can also combine them to build fairly precise schedules on a single line.
- Asterisk (*): Every value from first to last. An asterisk in the hour field means "every hour".
- Comma (,): Builds a list.
0 8,12,18 * * *runs three times a day, at 8:00, 12:00 and 18:00. - Hyphen (-): Defines an inclusive range.
0 9-17 * * 1-5runs at the top of every working hour on weekdays. - Slash (/): Sets a step inside a range.
*/15 * * * *runs every 15 minutes, while0-30/10fires at minutes 0, 10, 20 and 30.
Be careful with steps, though. */7 in the minute field looks like "every seven minutes". However, counting restarts at 0 each hour, so the last gap after minute 56 comes out shorter. Therefore steps that divide 60 evenly (5, 10, 15, 20, 30) give more predictable results.
Some cron implementations also add extra syntax. For example, the cronie manual describes a tilde (~) that picks a random value inside a range. That extension, however, does not exist everywhere. So if you want a portable line, stick to the four characters above.
What do @reboot, @daily and the other shortcuts do?
Instead of writing five fields every time, you can use the short names the manual defines. They make tables easier to read, especially when a whole team shares them.
@reboot: Runs once after the system restarts.@yearlyand@annually: Once a year, the same as0 0 1 1 *.@monthly: Once a month, the same as0 0 1 * *.@weekly: Once a week, the same as0 0 * * 0.@daily: Once a day, the same as0 0 * * *.@hourly: Once an hour, the same as0 * * * *.
Here is the catch: @daily means midnight. So if many jobs pile up at midnight, disk and database take the load in the same minute. That is why we suggest spreading heavy work to odd minutes such as 17 2 * * *.
@reboot is handy but limited. During boot, the network, the database or mounted disks may not be ready yet. For an app that has to keep running, a systemd service therefore fits better than cron. We walk through that approach in our Node.js deployment guide with Nginx and systemd.
How do you create a cron job step by step?
On a VPS or server with SSH access, your first cron job takes a few minutes. If you have not done the basic server setup yet, start with our Ubuntu Server initial setup guide. Then follow this order:
- Run the task by hand first. If it fails in the terminal, it will fail in cron too.
- Next, find the full path of every program. Note the output of
command -v phporwhich php. - Open your table with
crontab -e. On first use, some systems ask which editor you prefer. - Write a comment line, then the schedule line, and send the output to a log file.
- Finally, save and exit. A message similar to "installing new crontab" confirms the table loaded.
- Check the line with
crontab -land read the log after the first run.
# Database backup every night at 02:1717 2 * * * /usr/local/bin/db-backup.sh >> /home/deploy/logs/db-backup.log 2>&1If you do not want to wait for the first run, set */2 * * * * temporarily and check the log two minutes later. Then switch the schedule back to its real value. To plan how often backups run and where they go, our website backup strategy guide is a good starting point.
What should you watch out for with crontab -e, -l and -r?
According to the crontab(1) manual, three basic options cover nearly all daily work. One of them, however, can wipe the whole table in one keystroke.
| Command | What it does | Risk and tip |
|---|---|---|
crontab -e | Opens the table in the editor from VISUAL or EDITOR. | It checks syntax on save and offers to re-edit if something is wrong. |
crontab -l | Prints the current table. | Redirect the output to a file to keep a backup. |
crontab -r | Removes the current table completely. | No confirmation, and it sits one key away from -e. |
crontab -i -r | Asks for a y/Y answer before removing. | A safe habit where your system supports it. |
crontab -u user -l | Shows another user's table. | Only a privileged user (root) can do this. |
Because "e" and "r" sit next to each other on the keyboard, accidental crontab -r happens more often than you might think. So we recommend taking a copy before every edit:
crontab -l > ~/crontab-backup-$(date +%F).txtYou run that line in the terminal, not inside the crontab. We explain why in the percent sign section below, because the reason is easy to miss.
What is the difference between user and system crontabs?
There are several places to put cron jobs, and each has its own line format and ownership model. The manual treats jobs in /etc/crontab and /etc/cron.d/ as system jobs. Those files therefore expect a username field right after the time fields.
| Location | Who edits it | Username field | Best fit |
|---|---|---|---|
User crontab (crontab -e) | The account owner | No, the job runs as that user | Site owner scripts, shared hosting |
/etc/crontab | root | Yes | The distribution's own maintenance; keep manual edits minimal |
/etc/cron.d/file | root or the package manager | Yes | Grouping one application's jobs in a separate file |
| cPanel Cron Jobs | The cPanel account | No | Shared hosting without SSH access |
# /etc/cron.d/example-app* * * * * www-data /usr/bin/php /var/www/example.com/artisan schedule:runForgetting the username field is the most common mistake in system files. Cron then treats the first word of your command as the username. On the other hand, if you add that field to a user crontab, cron tries to run the username as a command. In other words, always check where the line lives.
Why can cron not find your command, and how do you fix PATH?
"It works in the terminal but not in cron" almost always comes down to the environment. When you log in, your shell loads profile files, PATH, locale settings and version managers. Cron, however, loads very little of that.
According to the manual, cron sets SHELL to /bin/sh. It takes LOGNAME and HOME from the owner's user account entry. PATH gets a short default that depends on the distribution and is much shorter than your login PATH. As a result, tools in /usr/local/bin, like composer, wp or a node binary from a version manager, may be missing inside cron.
You have three ways to fix it:
- Add an explicit PATH line at the top of the table.
- Set
SHELL=/bin/bashif your scripts use bash-only syntax. - Most robust of all: write every program with its full path, both in the line and inside the script.
SHELL=/bin/bashPATH=/usr/local/bin:/usr/bin:/binMAILTO=""*/10 * * * * /usr/local/bin/example-task.shCurious what cron actually sees? Add * * * * * env > /tmp/cron-env.txt once, read the file a minute later, then delete the line. That way you work with real data instead of guesses.
Why do absolute paths matter so much?
When a cron job runs, the working directory is not the folder you happened to be in. It is usually the user's home directory. That is why relative commands like ./backup.sh or php artisan either fail inside cron or act on the wrong files.
The rule is simple, so there is no excuse to skip it. Write the full path of both the program and the file on the crontab line. If your script reads or writes files, change into the working directory at the top and exit if that fails.
#!/bin/bashset -euo pipefailcd /var/www/example.com || exit 1/usr/bin/php artisan schedule:runThe set -euo pipefail line stops the script from carrying on silently after an error. Picture a database dump that fails while the script still deletes the old backup and prints "done". Instead, you would only notice on restore day. For dump and restore details, see our mysqldump and pg_dump guide.
PHP needs the same care. Shared hosting often ships several PHP versions, and plain php may not point to the version your site uses. We explain how to check the active version in our cPanel PHP version guide. Then use that version's full path in the cron line as well.
How do you send cron output to a log file?
By default, cron emails any output a job prints to the table owner. The MAILTO variable sets that address. If the server has no mail setup, the output disappears. If it does, your inbox then fills with hundreds of useless messages. So you need to manage output on purpose.
>> /path/task.log 2>&1: Appends both normal output and errors to the file.> /dev/null 2>&1: Throws all output away. Use it only for jobs you monitor some other way.MAILTO="": Turns off email for the jobs in that table.
The order of 2>&1 at the end matters. First you send standard output to the file, then you point the error stream at standard output. Otherwise, if you flip the order, errors still vanish.
Log files grow over time too. For long-lived jobs, we suggest rotating them with logrotate or having the script stamp each line with a date. Cron also keeps its own records. On Debian and Ubuntu you find them in the system log under "CRON", and on systemd distributions you can read them with journalctl. For bulk log review, you can also try our log file analyzer.
Why does a percent sign break a cron line?
One of the least known rules in the crontab manual concerns the percent sign. An unescaped % in the command part turns into a newline. Everything after the first percent sign goes to the command as standard input. That is why a line with date +%F works perfectly in the terminal and gets cut in half inside cron.
# Wrong: everything after % becomes input0 1 * * * /usr/bin/tar czf /backup/site-$(date +%F).tgz /var/www/example.com# Right: escape the percent sign with a backslash0 1 * * * /usr/bin/tar czf /backup/site-$(date +\%F).tgz /var/www/example.comA cleaner option is to move all date formatting into the script. Then the crontab line stays simple, and the script behaves the same in the terminal and in cron. That is the setup we prefer: the crontab answers "when", and the script answers "how".
Quotes also cause a similar trap. Cron hands the line to /bin/sh, so nested quotes, variable expansion and special characters raise the error risk as they pile up. Moving any logic that does not fit on one line into its own script file makes it easier to read and to debug.
How do you add a cron job in cPanel?
On shared hosting you often have no SSH access. So in that case you add a cron job through the cPanel interface. According to the cPanel documentation, the screen sits in the Advanced section under the name Cron Jobs.
- Log in to cPanel and open Cron Jobs from the Advanced section.
- Enter an address in the Cron Email field, or leave it empty if you do not want notifications.
- Pick a ready-made interval from the Common Settings menu. It fills in the minute, hour, day, month and weekday fields for you.
- Type the full command path into the Command field and add the job.
- Edit or delete existing jobs from the list further down the same page.
The cPanel docs give two clear warnings. First, leave enough time between runs for a job to finish. Otherwise the server may start a new run while the previous one is still going. Second, use rm in cron with great care, since a wrong path can delete data in your home directory.
If you do not want a specific job to send email, append >/dev/null 2>&1 to the command. The PHP path in that command depends on your host, so use the one their documentation gives. In addition, some hosts limit very frequent jobs. Ask their support team rather than guessing that limit.
How do you replace WordPress wp-cron with a real cron job?
WP-Cron, which WordPress uses for scheduled posts, plugin updates and email queues, is not a real cron. The WordPress developer documentation explains that WP-Cron fires on each page load. On a low traffic site tasks run late, while on a busy site the check repeats far more often than needed.
The fix is to turn off the page load trigger and hand the work to the system scheduler. First, the docs tell you to add this line to wp-config.php:
define( 'DISABLE_WP_CRON', true );Then you set up a real cron entry. The official page shows an example that calls wp-cron.php over HTTP and uses */15 * * * * for a 15 minute interval. If WP-CLI is installed on the server, you can run due events from the command line without any HTTP request:
*/15 * * * * cd /home/user/public_html && /usr/local/bin/wp cron event run --due-now >> /home/user/logs/wp-cron.log 2>&1The wp path varies between servers, so confirm it with command -v wp. Also note that if you add DISABLE_WP_CRON but forget the cron entry, scheduled posts stay unpublished and emails never go out. If WordPress email is the issue, our WP Mail SMTP setup guide can help.
Where should you start when a cron job does not run?
When a cron job misbehaves, following a fixed checklist saves time compared with random changes. We suggest this order:
- Is the service up? Check with
systemctl status cron, orsystemctl status crondon the RHEL family. - Is the line really in the table? Look at
crontab -land make sure you are checking the right user's table. - Did cron trigger it? Search the system log for a "CRON" record at that minute.
- Are the paths right? Confirm that programs and files use full paths.
- Are the permissions right? Make the script executable with
chmod +xand confirm the user can reach the files. - Are the line endings right? Scripts edited on Windows carry CRLF endings, which cause a "bad interpreter" error. Convert them to LF. Also leave a newline after the last crontab line.
- Is there a percent sign? An unescaped
%splits the command. - Is access blocked? If
cron.allowexists, your user must appear in it. If onlycron.denyexists, your user must not.
Above all, change only one thing per step. If you change three settings at once, you will not know which one fixed the problem or which one caused a new one.
How can time zones shift your schedule?
"I set the job for 9:00 but it ran at 6:00" is nearly always a time zone issue. Cron follows the server's system clock and local time zone. For example, many cloud servers ship with UTC. If you work in New York or London, your local time differs from UTC for at least part of the year. So a 0 9 * * * job on a UTC server may run hours earlier or later than you expect.
You have two options here:
- Change the server time zone with
timedatectl set-timezone America/New_York(or your own zone) and restart the cron service. - Keep the server on UTC and calculate schedules in UTC. For systems that serve several countries, this approach causes fewer surprises.
Some cron implementations support a CRON_TZ variable for a per-table time zone, and the manual page describes it. However, not every distribution supports it. Search your own man 5 crontab page before you rely on it. Daylight saving changes deserve the same caution, since an hour can repeat or vanish on those nights.
Clock drift is also a separate risk. If the system clock runs a few minutes fast or slow, jobs fire at the wrong time and log timestamps stop lining up. To prevent that, make sure time sync is active. Details are in our NTP and Linux time sync guide.
How do you stop overlapping runs with flock?
A job that runs every five minutes sometimes takes seven. Cron then starts a second copy before the first one ends. When two copies write to the same files or the same table, data can get corrupted. That is exactly the case the cPanel docs warn about.
On Linux, flock from the util-linux package solves this with a lock file. According to the flock(1) manual, the -n option makes it fail instead of waiting when the lock is not free. In other words, if the previous copy still runs, the new one never starts.
*/5 * * * * /usr/bin/flock -n /tmp/stock-sync.lock /usr/local/bin/stock-sync.sh >> /home/deploy/logs/stock.log 2>&1The manual describes two more options:
-w seconds: Waits for the lock for that many seconds, then gives up.-E code: Sets the exit code when the lock is not acquired. The default is 1. It also helps you tell lock conflicts apart from real errors.
In addition, give each job its own lock file name. If two different jobs share one lock, one blocks the other for no reason. Also, the kernel releases the lock when the process ends or crashes, so you never need to delete lock files by hand.
What should you watch for in cron security?
Cron runs commands regularly while you are away. In the wrong hands that power becomes a persistence tool. That is why cron tables are among the first places a security review checks.
- Least privilege: Run the job with a user that has just enough rights, not root. A web app's job should run as the web app's user.
- Script permissions: If other users can edit a script that root runs, those users gain root indirectly. Make the script and its folder writable only by the owner.
- Passwords: Keep database passwords out of the crontab line. Otherwise they can show up in the process list and in logs. Use a config file only the owner can read.
- Access lists: Limit who may use crontab through
cron.allowandcron.deny. - Regular review: If you spot a cron line you do not recognize, especially one that downloads and runs something from outside, treat it as a serious warning sign.
Narrowing server access is also the first step in protecting cron. For SSH hardening see our swap file and SSH hardening guide. For brute force attempts, read our Fail2ban setup guide.
Common cron expressions at a glance
The table below collects the schedules we need most often on websites, along with how to read them. Adjust the values to your own work, and move heavy jobs to odd minutes.
| Expression | Meaning | Typical use |
|---|---|---|
*/5 * * * * | Every 5 minutes | Queue processing and stock sync |
*/15 * * * * | Every 15 minutes | WordPress scheduled events |
0 * * * * | Top of every hour | Cache warming, currency rates |
17 2 * * * | Daily at 02:17 | Database backup |
0 9 * * 1-5 | Weekdays at 09:00 | Daily sales report |
30 3 * * 0 | Sundays at 03:30 | Weekly log cleanup |
0 4 1 * * | 1st of the month at 04:00 | Monthly invoices or archive |
@reboot | Once after boot | Preparing temp folders |
Remember that the hours follow the server's time zone. Before going live, check the next few run times on paper or with a trustworthy calculator. That simple step stops a "monthly" job from running every day.
When should you not manage cron yourself?
Cron is powerful but unforgiving. In some cases it is better to leave the work to your hosting provider or server admin. To be honest, we tell clients this often.
- On managed hosting, the provider usually schedules backups and maintenance already. So setting up the same job twice causes conflicts.
- If the job needs root and you have little root experience, one mistake can affect the whole system.
- When the job drives a critical flow such as payments, invoices or customer data, do not set it up without monitoring, alerts and a rollback plan.
- If your shared host enforces a frequency limit, talk to support instead of pushing against it.
Asking about cron access, SSH and backup policy before you pick a host prevents many of these issues. Our guide to choosing web hosting has a checklist. And if you want your site built on the right foundation from day one, we plan scheduled tasks as part of our web design service.
Cron job checklist: what to confirm before you go live
Setting up a cron job looks like a one-line task. Yet whether that line keeps working quietly for years depends on a few habits. To sum up, run through this list for every new job:
- Run the command by hand in the terminal first.
- Use full paths for programs and files.
- Send output to a log file.
- Escape percent signs or move them into a script.
- Wrap long-running jobs in
flock -n. - Check the time zone and clock sync.
- Run the job with the least privileged user that works.
- Back up with
crontab -lbefore editing.
These eight steps prevent most of the cron problems we run into before they ever show up. In short, a well-built cron job stays invisible. Backups pile up, reports arrive, WordPress posts go live on time, and you never have to think about any of it.



