How to Build Cron Expressions
Cron expressions are the standard way to define recurring schedules in Linux, cloud platforms, CI/CD pipelines, and task schedulers. The syntax is compact but not intuitive, building a visual cron generator shows you exactly when your job will run, catches common errors before deployment, and removes the guesswork from the most error-prone part of automation. Once you understand the five fields, the special characters, and the most common pitfalls, you can specify any recurring schedule with confidence.
A short history of cron
The first cron came from Brian Kernighan in Version 7 Unix, around 1979. It re-read its configuration every minute and ran whatever was due. Paul Vixie rewrote it in 1987 into what is now called Vixie cron, which is the version most Linux distributions still ship. Vixie cron added per-user crontabs, environment variables, the @reboot keyword, and several quality-of-life features that made the format usable for non-administrators.
The 5-field syntax has barely changed in over forty years. Amazon EventBridge, Google Cloud Scheduler, Kubernetes CronJob, GitHub Actions, GitLab CI, Jenkins, Airflow, n8n, and dozens of other systems all consume the same compact format with only minor extensions. That stability is the reason cron is worth learning once and then never re-learning. The skill transfers everywhere automation runs on a clock.
Cron syntax
A standard cron expression has 5 fields, separated by spaces. Each field controls one slice of time, and a job runs whenever every field matches the current moment.
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12, or JAN-DEC)
│ │ │ │ ┌───────────── day of week (0-6, Sun=0, or SUN-SAT)
│ │ │ │ │
* * * * *
The fields are ANDed together for hour, minute, and month, but day-of-month and day-of-week are ORed in Vixie cron. That means 0 12 1 * 1 fires on both the 1st of every month AND every Monday at noon, not only on Mondays that happen to be the 1st. This trap catches almost everyone the first time.
Common cron schedules
The patterns you will reach for most often:
| Schedule | Expression | Meaning |
|---|---|---|
| Every minute | * * * * * |
Runs every 60 seconds |
| Every 5 minutes | */5 * * * * |
At :00, :05, :10, :15... |
| Every 15 minutes | */15 * * * * |
At :00, :15, :30, :45 |
| Every hour | 0 * * * * |
At the top of every hour |
| Every 2 hours | 0 */2 * * * |
At 00:00, 02:00, 04:00... |
| Daily at midnight | 0 0 * * * |
Once per day at 00:00 |
| Daily at 9 AM | 0 9 * * * |
Once per day at 09:00 |
| Twice a day | 0 9,21 * * * |
At 09:00 and 21:00 |
| Every Monday at 8 AM | 0 8 * * 1 |
Weekly on Monday |
| Weekdays at 6 PM | 0 18 * * 1-5 |
Monday through Friday |
| First of every month | 0 0 1 * * |
Monthly at midnight on the 1st |
| Every quarter | 0 0 1 */3 * |
Jan 1, Apr 1, Jul 1, Oct 1 |
| Every weekday morning | 0 7 * * 1-5 |
07:00 Mon-Fri |
| Sunday at noon | 0 12 * * 0 |
Weekly on Sunday |
Many systems also accept shorthand aliases that expand to the equivalent 5-field expression: @yearly, @monthly, @weekly, @daily, @hourly, and @reboot. They are concise but not universal, so check your platform before relying on them.
How to build a cron expression
- Pick the granularity: do you need every minute, every hour, once a day, once a week, or once a month? Start at the coarsest setting that meets your need.
- Use the visual controls: select minute, hour, day, month, and weekday values from the dropdowns. Or start with a preset like "every hour" or "daily at midnight" and adjust.
- Preview the next run times: the generator shows the next 5 execution times, which lets you confirm the schedule fires when you expect.
- Sanity-check the timezone: the preview should match the timezone of the server or scheduler that will run the job, not your local time.
- Copy the expression and paste it into your crontab, GitHub Actions YAML, AWS EventBridge rule, or whichever scheduler you use.
- Test with a short interval first before committing the final schedule. A quick
*/5 * * * *proves the job fires; once you see two or three runs land, swap in the real expression.
Special characters and operators
Cron supports a small but powerful set of operators inside any field.
| Character | Meaning | Example |
|---|---|---|
* |
Every value | * * * * * = every minute |
*/n |
Every nth | */15 * * * * = every 15 min |
, |
Multiple discrete values | 0 8,12,18 * * * = 8am, noon, 6pm |
- |
Inclusive range | 0 9-17 * * * = every hour 9am-5pm |
n-m/k |
Range with step | 0 9-17/2 * * * = 9, 11, 13, 15, 17 |
? |
No specific value (Quartz only) | 0 0 ? * MON (Java schedulers) |
L |
Last (AWS, Quartz) | L in DoM = last day of the month |
W |
Nearest weekday (AWS, Quartz) | 15W = weekday nearest the 15th |
# |
Nth weekday (AWS, Quartz) | MON#2 = second Monday of the month |
@hourly |
Shorthand | Same as 0 * * * * |
Vanilla Vixie cron only supports the first five rows. The advanced operators (L, W, #, ?) come from Quartz, the Java scheduling library, and were adopted by AWS EventBridge and a few other cloud schedulers. They are not portable, so do not mix them with code that has to run on a generic Linux box.
Cron on different platforms
Cron is a family of related syntaxes, not a single standard. Knowing which dialect your scheduler speaks saves hours of debugging.
| Platform | Fields | Notes |
|---|---|---|
| Vixie cron (Linux) | 5 | The classic. */n, ranges, lists, no advanced operators |
| BSD cron | 5 | Like Vixie but slight environment differences |
| crontab.guru | 5 | Web-based parser that mirrors Vixie semantics |
| GitHub Actions | 5 | Vixie syntax, runs in UTC, at least 5 minute resolution |
| GitLab CI | 5 | Vixie syntax, runs in instance timezone |
| AWS EventBridge | 6 | Adds year. Day-of-week uses 1-7 (Sun=1), supports L/W/# |
| Google Cloud Scheduler | 5 | Vixie syntax plus timezone configuration |
| Kubernetes CronJob | 5 | Vixie syntax with @ shortcuts |
| Quartz (Java) | 6 or 7 | Adds seconds at the front and optional year |
| systemd timers | OnCalendar format | Not cron, but solves the same problem with clearer syntax |
If you write a schedule that has to run on more than one platform, stick to the conservative 5-field subset that every system understands. Reach for L, W, or # only when you know the destination supports them.
Common pitfalls
- Day-of-month and day-of-week are ORed, an expression like
0 9 15 * 1fires every Monday AND on the 15th of every month, not only on the 15th when it falls on a Monday. To intersect, you typically need an outer wrapper or a different scheduler. - Timezone confusion, server crontabs almost always run in UTC. If you want 9 AM Eastern time, that is
0 14 * * *UTC in winter but0 13 * * *UTC in summer because of daylight saving. Use a scheduler that supports timezone hints, or normalize everything to UTC. - Daylight saving transitions, jobs scheduled at 02:30 local time may run twice when clocks fall back and not at all when they spring forward. Schedule sensitive jobs outside the 01:00-03:00 window or use UTC.
- The Vixie
MAILTOtrap, if your job prints anything to stdout, Vixie cron emails the output to the user that owns the crontab. On servers without a mail relay this fills/var/spool/mailquickly. Redirect output to a log file with>>/var/log/myjob.log 2>&1. - Environment is not your login shell, cron runs with a stripped environment: no
PATHfrom your.bashrc, no virtualenv, nonvm. Set the variables you need at the top of the crontab or call your script with an absolute path. - Percent signs need escaping, an unescaped
%in a Vixie crontab is interpreted as a newline character in the command. Always escape it as\%if your command needs a literal percent sign, for example in adate +"%Y-%m-%d"invocation. - Long-running jobs overlap, cron does not skip a run because the previous one is still going. If your job can take longer than the interval, wrap it in a lock file (
flock,setlock) or use a job runner that handles concurrency. - Overflow at 59 minutes,
*/40 * * * *does not fire every 40 minutes. It fires at minute 0 and minute 40 of every hour because step values wrap around the field's bounds. For true 40-minute intervals you need a richer scheduler. - Forgetting
0in the minute field,* 9 * * *runs every minute from 09:00 through 09:59, not once at 09:00. The minute field needs an explicit value if you want a single firing per hour. - Cron is unreliable on laptops, anacron exists for that reason. Vixie cron does not catch up on missed runs after a sleep, so a daily backup scheduled at 03:00 will not run if the laptop was closed at that hour. Use anacron, systemd timers with
Persistent=true, or a launchd plist on macOS.
Alternatives to cron
For some workloads cron's coarse minute resolution and lack of bookkeeping start to hurt. The most common upgrades:
| Tool | Strength | When to choose it |
|---|---|---|
| systemd timers | Clear OnCalendar syntax, persistent across reboots, integrates with units | You already run systemd and want richer logging |
| Anacron | Catches up on missed runs after sleep | Laptops or machines that are not always on |
| Airflow / Dagster | DAG dependencies, retries, observability | Multi-step data pipelines |
| Temporal | Stateful workflows, exactly-once guarantees | Long-running orchestration across services |
| AWS EventBridge | Managed, integrates with Lambda, S3, SQS | Anything cloud-native on AWS |
| GitHub Actions | Free for public repos, runs on hosted runners | CI-adjacent scheduled jobs |
| serverless functions on cron triggers | No server to maintain | Lightweight tasks that fit in a Lambda |
Cron remains the right answer for the vast majority of one-shot recurring jobs. The other tools shine when you need state, retries, dependencies, or coordination across machines.
Privacy and the cron generator
The cron expression generator runs entirely in your browser. The schedule you build, the preview of next run times, and the copied expression never touch our servers. There is no log of which expressions were generated, no telemetry on which presets are popular, and no way for anyone to reconstruct what schedule you were working on. Cron expressions are not personal data on their face, but the schedule of a job (a nightly database export, a weekly billing run, a hourly sync to a partner) can reveal a lot about how a business operates. Keeping that information client-side avoids accidentally leaking infrastructure patterns to a third party. For a task as routine as picking a schedule, the privacy default should match the sensitivity of what those schedules represent.
Frequently Asked Questions
What is the cron expression format?
A standard cron expression has 5 fields separated by spaces, representing minute (0-59), hour (0-23), day of month (1-31), month (1-12), and day of week (0-6, where 0 is Sunday). An asterisk (*) means "every" value in that field.
What does */5 mean in cron?
The */5 syntax means "every 5th." In the minute field, */5 means every 5 minutes (0, 5, 10, 15...). In the hour field, */5 means every 5 hours. It works in any field.
Are cron expressions the same on all platforms?
The 5-field format is standard across Linux cron, AWS EventBridge, GitHub Actions, and most scheduling systems. Some platforms add a sixth field for seconds or year. Check your platform's documentation.
How do I schedule something for the last day of every month?
Standard cron does not have a "last day" keyword. Use a workaround like running daily and checking the date in your script, or use platform-specific extensions (AWS EventBridge supports L for "last").
Why did my cron job not run at the expected time?
The most common cause is timezone confusion. Server cron usually runs in UTC, not your local time. Other causes include the server being asleep at the scheduled minute, the user crontab not being installed, or PATH/environment differences between your shell and cron's stripped-down environment.
What is the difference between 0 in the day-of-week field and 7?
Both 0 and 7 represent Sunday in classic Vixie cron, which uses 0-6 plus an alias for 7. Some implementations (notably AWS EventBridge) use 1-7 with Sunday as 7 and Monday as 1, so always check your platform's documentation before assuming.