Web

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

Talha Aslan 20 min read 1 views

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:

  1. Minute: 0 to 59.
  2. Hour: 0 to 23.
  3. Day of month: 1 to 31.
  4. Month: 1 to 12, or the first three letters of the month name (jan, feb and so on).
  5. 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.sh

The 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-5 runs at the top of every working hour on weekdays.
  • Slash (/): Sets a step inside a range. */15 * * * * runs every 15 minutes, while 0-30/10 fires 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.
  • @yearly and @annually: Once a year, the same as 0 0 1 1 *.
  • @monthly: Once a month, the same as 0 0 1 * *.
  • @weekly: Once a week, the same as 0 0 * * 0.
  • @daily: Once a day, the same as 0 0 * * *.
  • @hourly: Once an hour, the same as 0 * * * *.

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:

  1. Run the task by hand first. If it fails in the terminal, it will fail in cron too.
  2. Next, find the full path of every program. Note the output of command -v php or which php.
  3. Open your table with crontab -e. On first use, some systems ask which editor you prefer.
  4. Write a comment line, then the schedule line, and send the output to a log file.
  5. Finally, save and exit. A message similar to "installing new crontab" confirms the table loaded.
  6. Check the line with crontab -l and 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>&1

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

CommandWhat it doesRisk and tip
crontab -eOpens the table in the editor from VISUAL or EDITOR.It checks syntax on save and offers to re-edit if something is wrong.
crontab -lPrints the current table.Redirect the output to a file to keep a backup.
crontab -rRemoves the current table completely.No confirmation, and it sits one key away from -e.
crontab -i -rAsks for a y/Y answer before removing.A safe habit where your system supports it.
crontab -u user -lShows 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).txt

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

LocationWho edits itUsername fieldBest fit
User crontab (crontab -e)The account ownerNo, the job runs as that userSite owner scripts, shared hosting
/etc/crontabrootYesThe distribution's own maintenance; keep manual edits minimal
/etc/cron.d/fileroot or the package managerYesGrouping one application's jobs in a separate file
cPanel Cron JobsThe cPanel accountNoShared hosting without SSH access
# /etc/cron.d/example-app* * * * * www-data /usr/bin/php /var/www/example.com/artisan schedule:run

Forgetting 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/bash if 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.sh

Curious 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:run

The 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.com

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

  1. Log in to cPanel and open Cron Jobs from the Advanced section.
  2. Enter an address in the Cron Email field, or leave it empty if you do not want notifications.
  3. Pick a ready-made interval from the Common Settings menu. It fills in the minute, hour, day, month and weekday fields for you.
  4. Type the full command path into the Command field and add the job.
  5. 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>&1

The 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:

  1. Is the service up? Check with systemctl status cron, or systemctl status crond on the RHEL family.
  2. Is the line really in the table? Look at crontab -l and make sure you are checking the right user's table.
  3. Did cron trigger it? Search the system log for a "CRON" record at that minute.
  4. Are the paths right? Confirm that programs and files use full paths.
  5. Are the permissions right? Make the script executable with chmod +x and confirm the user can reach the files.
  6. 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.
  7. Is there a percent sign? An unescaped % splits the command.
  8. Is access blocked? If cron.allow exists, your user must appear in it. If only cron.deny exists, 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>&1

The 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.allow and cron.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.

ExpressionMeaningTypical use
*/5 * * * *Every 5 minutesQueue processing and stock sync
*/15 * * * *Every 15 minutesWordPress scheduled events
0 * * * *Top of every hourCache warming, currency rates
17 2 * * *Daily at 02:17Database backup
0 9 * * 1-5Weekdays at 09:00Daily sales report
30 3 * * 0Sundays at 03:30Weekly log cleanup
0 4 1 * *1st of the month at 04:00Monthly invoices or archive
@rebootOnce after bootPreparing 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:

  1. Run the command by hand in the terminal first.
  2. Use full paths for programs and files.
  3. Send output to a log file.
  4. Escape percent signs or move them into a script.
  5. Wrap long-running jobs in flock -n.
  6. Check the time zone and clock sync.
  7. Run the job with the least privileged user that works.
  8. Back up with crontab -l before 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.

Frequently Asked Questions

How often can a cron job run at most?
Classic cron runs a job at most once per minute, because the service checks its tables every minute. If you need second-level timing, cron is the wrong tool. A long-running service or a queue worker fits better. On shared hosting, providers may also set a minimum interval, so check your host's documentation before scheduling very frequent jobs.
Where is the crontab file stored, and can I edit it directly?
User crontabs live in a folder under /var/spool, and the exact path depends on the distribution. We do not recommend editing those files directly. The crontab -e command opens the file, checks the syntax on save and notifies the service. Editing by hand skips those checks. For system jobs, edit files under /etc/cron.d with root rights instead.
Can I recover jobs I deleted with crontab -r?
No. crontab -r removes the table without asking, and cron has no undo feature of its own. If you saved the output of crontab -l to a file earlier, restore it with crontab followed by that file name. Without a copy, you need a server backup or help from your hosting provider. That is why a backup before every edit is a good habit.
Does DISABLE_WP_CRON make WordPress faster?
Partly, because WordPress stops checking for due tasks on every page load. The bigger win is reliability, since tasks run on your schedule regardless of visitor traffic. However, if you add the line and skip the system cron entry, scheduled posts and emails stop entirely. After the change, check the first few runs in your log file.
Should I use cron or a systemd timer?
Cron is enough for simple recurring jobs, and it exists on almost every system, including shared hosting. A systemd timer adds dependencies, catch-up for missed runs and logging built into the journal. On a VPS with root access, timers suit complex jobs well. On shared hosting, cron is usually the practical choice because timers are not available there.
How do I know my cron job actually runs?
The most reliable way is to have the job write its output with a timestamp to a log file. You can also search the system log for CRON entries to see whether the service triggered the job. For critical tasks, update a file's timestamp on success and let a monitoring tool check it, so silent failures surface early.
  • cron job
  • crontab
  • Linux server
  • cPanel
  • WordPress wp-cron
  • flock
  • server administration
Share:
Talha Aslan

Google Partner digital marketing expert. Hands-on with SEO, Google Ads, web design and e-commerce projects since 2012; every post here comes from that experience.

Next project

Let's talk about your project.

Your brief goes straight to Talha Aslan and team: strategy led by Talha, delivery by an experienced team. The first consultation is free; we listen and come back with a clear roadmap.