Skip to main content
Back to Blog

Understanding Cron Expressions: The Complete Guide to Scheduling

A developer's guide to mastering cron syntax, scheduling automated tasks, and avoiding common timing mistakes.

Kashyap Thaker
8 min
DevOpsSchedulingBackend
Understanding Cron Expressions: The Complete Guide to Scheduling

If you've ever had to schedule a recurring task—whether it's sending an email newsletter, backing up a database, or clearing cache—you've likely encountered a cron expression. They often look like a random assortment of stars and numbers (e.g., 0 15 * * *), but once you understand the pattern, they are incredibly powerful.

What is a Cron Expression?

A standard cron expression is a string comprising five (or sometimes six) fields separated by spaces. These fields represent a set of times or dates when a task should execute. Originally developed for Unix-like operating systems, the cron syntax is now ubiquitous across cloud providers, CI/CD pipelines, and application frameworks.

*    *    *    *    *    *
- - - - - -
| | | | | |
| | | | | +----- day of week (0 - 7) (Sunday=0 or 7)
| | | | +---------- month (1 - 12)
| | | +--------------- day of month (1 - 31)
| | +-------------------- hour (0 - 23)
| +------------------------- minute (0 - 59)
+------------------------------ second (0 - 59, only in six-field schedulers)

Special Characters Explained

The real power of cron comes from its special characters:

  • Asterisk (*): Means "every". A * in the hour field means "every hour".
  • Comma (,): Specifies a list of values. 1,15 in the minutes field means "at minute 1 and minute 15".
  • Hyphen (-): Defines a range. 1-5 in the day-of-week field means "Monday through Friday".
  • Slash (/): Specifies increments. */15 in the minutes field means "every 15 minutes".

Common Examples

ExpressionMeaning
0 * * * *Every hour, at the start of the hour
0 0 * * *Every day at midnight
0 9 * * 1-5At 9:00 AM, Monday through Friday
*/15 * * * *Every 15 minutes

More Patterns You Will Actually Need

Once the five fields click, most real schedules are small variations on a few shapes. Read them left to right: minute, hour, day of month, month, day of week.

ExpressionMeaning
*/5 * * * *Every 5 minutes
30 2 * * *Every day at 02:30
0 */6 * * *Every 6 hours, on the hour (00:00, 06:00, 12:00, 18:00)
0 8-18 * * 1-5On the hour from 08:00 to 18:00, Monday to Friday
0 0 1 * *At midnight on the first day of every month
0 0 * * 0At midnight every Sunday
0 0 1 1 *At midnight on 1 January, once a year
0 9,17 * * *At 09:00 and 17:00 every day

Most cron implementations also accept names (JAN to DEC, SUN to SAT) and shortcuts such as @hourly, @daily, @weekly, @monthly and @yearly. @reboot runs once at startup in the classic Vixie-style crontab. Support varies between schedulers, so check the documentation of the one you deploy to.

Gotchas That Cause Wrong Run Times

Steps restart at the top of each range

*/7 in the minute field does not mean "every seven minutes". It means minutes 0, 7, 14, 21, 28, 35, 42, 49 and 56 of every hour, so the gap between 56 and the next hour's 0 is only four minutes. The same applies to days: 0 0 */2 * * runs on days 1, 3, 5 and so on up to 31, then runs again on the 1st of the next month, so in a 31-day month the 31st and the 1st are back to back. Steps only give an even spacing when they divide the range evenly, which is why */5, */10, */15 and */30 are the safe ones for minutes.

Day of month and day of week are combined with OR

In standard Vixie-style cron, when both the day-of-month and day-of-week fields are restricted (neither is *), the job runs when either matches. So 0 9 13 * 5 does not mean "Friday the 13th". It means 09:00 on every 13th and on every Friday. If you need an AND, schedule the job daily and check the condition inside the script.

Short months and leap years

0 0 31 * * only fires in months that have a 31st, and 0 0 29 2 * only in leap years. For "the last day of the month" there is no portable five-field answer. Quartz-based schedulers support L in the day-of-month field, and with plain cron the usual approach is to run daily and test whether tomorrow is the first of the month.

Time zones and daylight saving

Cron evaluates the expression against a clock, and which clock depends on the scheduler. A classic crontab uses the server's local time zone, GitHub Actions scheduled workflows use UTC unless the workflow sets a timezone, and many cloud schedulers let you pick a zone. Around a daylight saving change, a job set for a time that is skipped or repeated may be skipped or run twice, depending on the implementation. For jobs where this matters, run the server in UTC and avoid scheduling between 01:00 and 03:00 local time. See the Unix timestamp guide for why UTC is the safer baseline for anything time-related.

Five Fields, Six Fields, Seven Fields

"Cron" is not one format. The same-looking string can mean different things in different tools:

  • Unix crontab (5 fields): minute, hour, day of month, month, day of week. Day of week is 0 to 7, where both 0 and 7 mean Sunday.
  • Quartz (6 or 7 fields): a seconds field is added at the front, and an optional year at the end. Quartz requires ? (no specific value) in either the day-of-month or day-of-week field, and numbers days of the week 1 to 7 starting at Sunday. It also adds L (last), W (nearest weekday) and # (for example 6#3, the third Friday).
  • Spring (6 fields): seconds first and no year field. Despite the Quartz-like extras (?, L, W, #), Spring's day of week is numbered 0 to 7 with Sunday as 0 or 7, as in Unix, not Quartz's 1 to 7.
  • Cloud schedulers: AWS EventBridge uses six fields (minutes, hours, day of month, month, day of week, year), requires ? in one of the two day fields, and numbers Sunday as 1. Always check the service's own syntax page rather than assuming the five-field form works.

A 5-field expression pasted into a 6-field scheduler with a leading seconds field will be shifted by one field, so the minute value becomes the seconds, and the job runs at the wrong time or is rejected. If a schedule behaves oddly after a migration, count the fields first.

Writing Reliable Cron Jobs

A correct expression is only half the job. Most cron incidents come from the environment around it.

# crontab -e
MAILTO=ops@example.com

# Nightly backup at 02:30, never overlapping, with output kept in a log
30 2 * * * flock -n /tmp/backup.lock /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

# A literal percent sign must be escaped in a crontab
0 0 * * * date +%Y-%m-%d >> /var/log/dates.log
  • Use absolute paths. Cron runs with a minimal environment and a short PATH, so a command that works in your shell may not be found. Use full paths to binaries and scripts.
  • Capture output. Redirect stdout and stderr (>> file 2>&1) or the output is mailed to the local user or discarded, and failures go unnoticed.
  • Prevent overlap. If a run can take longer than its interval, the next one starts on top of it. On Linux, flock -n from util-linux skips the run when a lock is held.
  • Escape %. In a crontab line an unescaped % is treated as a newline, which silently truncates commands that use date +%F.
  • Make jobs idempotent. Schedulers can run a job twice or miss one around restarts. A job that is safe to repeat is far easier to operate.
  • Keep secrets out of the crontab. Load them from the environment or a secrets store, as covered in environment variables best practices.
  • Spread the load. If hundreds of jobs all use 0 * * * *, they all start at once. Offset them by a few minutes, or add random jitter inside the script.

Debugging a Job That Did Not Run

  1. Check the field count and order. Paste the expression into the Cron Expression Generator to see it in plain English.
  2. Confirm the time zone. Compare the server's date output with the time you expected, and remember that UTC-based schedulers may be hours away from your local time.
  3. Run the command by hand with a clean environment. env -i /bin/sh -c 'your command' approximates cron's bare environment and quickly exposes missing paths and variables.
  4. Check the scheduler's log. On many Linux systems the cron service logs to syslog or the journal, which shows whether the job was started at all.
  5. Check permissions and the file itself. Scripts need the execute bit, and files in /etc/cron.d have their own format that includes a user column.

If the schedule is meant to trigger work for other systems, consider whether a webhook is a better fit than polling on a timer. Webhooks explained covers when push beats pull, and the API rate limiting guide is worth reading before you schedule frequent calls to someone else's API.

Generating and Validating Cron Expressions

Writing cron expressions by hand is notoriously prone to errors. A simple mistake, such as putting a value in the wrong field or confusing the day-of-month and day-of-week fields, can make a job run far more often (or on different days) than you intended.

To eliminate guesswork, use our Cron Expression Generator. It allows you to type natural language like "Every day at 3pm" and instantly get the correct 0 15 * * * expression. It can also produce a six-field expression with a leading seconds field for schedulers that support one.

Frequently Asked Questions

How do I run a cron job every 5 minutes?

Use */5 * * * *. The step in the minute field fires at minutes 0, 5, 10 and so on through 55 of every hour.

What does 0 0 * * * mean?

It runs once a day at 00:00 in the scheduler's time zone. Minute 0 and hour 0 are fixed, and the three asterisks mean every day of the month, every month and every day of the week.

Is Sunday 0 or 7 in cron?

In Unix crontab both 0 and 7 mean Sunday, and so they do in Spring. Quartz and AWS EventBridge number days 1 to 7 starting at Sunday instead, so convert carefully when moving a schedule between systems, or use the names SUN to SAT where supported.

Can cron run every 30 seconds?

Not with a standard five-field crontab, because the smallest unit is one minute. Use a scheduler with a seconds field, such as Quartz or Spring, or run a long-lived process with its own timer.

Why did my cron job run at the wrong time?

The usual causes are a time zone mismatch (UTC versus local), a 5-field expression used in a 6-field scheduler, a step value that does not divide its range evenly, or the day-of-month and day-of-week OR rule. Check each in that order.

Written by Kashyap Thaker, founder of 11Vertex.

Spotted an error? hello@kashyap.im · How we write this site