Developer Tools
February 14, 20267 min readBy SoftStash Team

Cron Expressions Explained: How to Read and Write Cron Schedules

Cron syntax is cryptic until you understand the five fields. Learn to read and write cron expressions, avoid common mistakes, and parse any schedule instantly in your browser.

croncron expressionschedulerDevOpsautomationLinux

If you have ever deployed a web application, configured a CI/CD pipeline, or managed a Linux server, you have almost certainly encountered a cron expression. Five asterisks staring back at you from a configuration file. A cryptic string like 0 2 * * 0 buried in a GitHub Actions workflow. An AWS EventBridge schedule that nobody on the team fully understands anymore. Cron expressions are everywhere — and they are genuinely confusing if you have not taken the time to learn the system behind them.

This guide is the reference you should bookmark. It covers everything from the history of cron and where it shows up in modern infrastructure, to every special character, 10 annotated real-world examples, common mistakes, and a full reference table. By the end, you will be able to read any cron expression at a glance and write new ones with confidence.

What Is Cron?

Cron is a Unix-based job scheduler that runs commands or scripts automatically at specified times and intervals. The name comes from Chronos, the Greek personification of time — an apt choice for a tool whose entire purpose is time-based automation. The original cron was introduced in Unix Version 7 in 1979, written by Ken Thompson at Bell Labs, and it has been a staple of Unix-like operating systems ever since.

The scheduler works by reading configuration files called crontabs (cron tables) — plain text files where each line defines a scheduled task. A background daemon process (crond) wakes up every minute, checks all active crontabs, and runs any jobs whose schedule matches the current time. It is a beautifully simple design that has remained fundamentally unchanged for over four decades.

Where You Encounter Cron Today

Cron is not just a relic of the Unix past. The cron expression syntax is the de facto standard for expressing recurring schedules across the modern software stack:

  • Linux and macOS crontab: The original use case. Run crontab -e on any Linux or macOS machine to edit your personal cron schedule.
  • GitHub Actions: Workflow files use cron syntax under the schedule: trigger to run CI/CD pipelines on a recurring basis.
  • AWS EventBridge (formerly CloudWatch Events): Triggers Lambda functions, ECS tasks, and other AWS services on a schedule using a 6-field cron variant.
  • Kubernetes CronJobs: The CronJob resource runs batch workloads inside a cluster on a cron schedule.
  • CI/CD pipelines: GitLab CI, CircleCI, Jenkins, and Bitbucket Pipelines all support scheduled runs using cron expressions.
  • Vercel and Netlify: Both platforms support cron-triggered serverless functions for tasks like cache invalidation, data fetching, and nightly builds.
  • Database maintenance: PostgreSQL's pg_cron extension, MySQL Event Scheduler, and managed database services use cron syntax for vacuuming, indexing, and backup jobs.
  • Application-level schedulers: Libraries like node-cron, APScheduler (Python), Quartz (Java), and Sidekiq (Ruby) all use cron expressions to define recurring background jobs.

In short: if you work in any area of software development or system administration, cron expressions are something you will encounter regularly. Learning them once pays dividends everywhere.

The Five-Field Structure

A standard cron expression consists of exactly five fields separated by spaces, each representing a unit of time. Together, they define when a job should run. Here is the canonical visual representation:

┌───────────── minute (0–59)
│ ┌─────────── hour (0–23)
│ │ ┌───────── day of month (1–31)
│ │ │ ┌─────── month (1–12)
│ │ │ │ ┌───── day of week (0–7)
│ │ │ │ │
* * * * *

Reading left to right: minute, hour, day of month, month, day of week. An asterisk (*) in any field means "every possible value for this field." So * * * * * means "every minute of every hour of every day" — the most permissive schedule possible.

Field 1: Minute (0–59)

The minute field controls which minute(s) within an hour a job fires. A value of 0 means on the hour, 30 means at the half hour, and * means every minute. This is the most granular field in standard cron — the smallest scheduling unit is one minute.

Field 2: Hour (0–23)

The hour field uses 24-hour time. 0 is midnight, 9 is 9am, 17 is 5pm, and 23 is 11pm. There is no AM/PM — everything is in 24-hour format. Remember that cron always runs in the timezone of the server unless explicitly configured otherwise.

Field 3: Day of Month (1–31)

Controls which day(s) of the month a job runs. 1 is the first, 15 is the fifteenth, 31 is the thirty-first. Be careful with values like 31 — in months with fewer days (February, April, June, etc.), a job scheduled for the 31st simply will not run that month. Some implementations support the special L character to mean "last day of the month" regardless of how many days the month has.

Field 4: Month (1–12)

The month field uses numeric values (1 for January through 12 for December) or three-letter abbreviations (JAN, FEB, MAR, APR, MAY, JUN,JUL, AUG, SEP, OCT, NOV, DEC) in most implementations. An asterisk means "every month."

Field 5: Day of Week (0–7)

This field specifies which day(s) of the week the job should run. The numbering here is a common source of confusion: both 0 and 7 represent Sunday in most cron implementations (a legacy quirk from the original Unix design). Monday is 1, Tuesday is 2, and Saturday is 6. Three-letter abbreviations (SUN, MON, TUE, WED, THU, FRI,SAT) are supported in most modern cron tools.

Important: When both day-of-month and day-of-week are specified (not *), most cron implementations treat them as an OR condition — the job runs if either condition matches. This is a subtle but critical behavior that catches many developers off guard.

Special Characters

The real power of cron expressions comes from six special characters that let you express complex schedules concisely. Understanding these is the key to fluency.

* — Wildcard (Every Value)

An asterisk means "match every possible value in this field." In the minute field, * means every minute (0 through 59). In the month field, it means every month. It is the default "I don't care about this field" value.

* * * * *    # Runs every single minute, all day, every day

, — List (Multiple Values)

A comma separates a list of specific values. The field matches if the current time matches any value in the list. This is how you schedule a job to run at multiple discrete times without using a range.

0 9,13,17 * * *    # Runs at 9:00 AM, 1:00 PM, and 5:00 PM every day
0 0 1,15 * *       # Runs at midnight on the 1st and 15th of every month

- — Range (From Through To)

A hyphen defines an inclusive range of values. The field matches every value between the start and end, inclusive. This is ideal for expressing things like "during business hours" or "on weekdays."

0 9-17 * * *      # Runs at the top of every hour from 9 AM through 5 PM
0 0 * * 1-5       # Runs at midnight every Monday through Friday

/ — Step (Every N Units)

A forward slash defines a step value. */5 means "every 5 units starting from the minimum." You can also combine it with a range: 0-30/10 means "every 10 units between 0 and 30" (i.e., 0, 10, 20, 30). Steps are one of the most commonly used special characters.

*/5 * * * *       # Every 5 minutes (0, 5, 10, 15, ... 55)
*/15 * * * *      # Every 15 minutes (0, 15, 30, 45)
0 */6 * * *       # Every 6 hours (0:00, 6:00, 12:00, 18:00)
0/15 * * * *      # Same as */15 — starts from 0, every 15 minutes

L — Last (Some Implementations Only)

The L character is supported in some cron implementations (notably Quartz Scheduler in Java and some Linux cron variants) to mean "last." In the day-of-month field, L means the last day of the current month — whether that is the 28th, 29th, 30th, or 31st. It solves the problem of scheduling "end-of-month" tasks without knowing the month's length in advance.

0 0 L * *         # Midnight on the last day of every month (Quartz/some crons)

? — No Specific Value (Quartz/Java Cron)

The question mark is used in Quartz Scheduler (Java) and some other tools when you want to specify a day-of-week without also specifying a day-of-month, or vice versa. Since it does not make logical sense to specify both (say "the 15th AND a Wednesday"), one of them should be set to ? to indicate "I don't care." Standard Unix cron does not use this character — you use * instead.

0 9 15 * ?        # Quartz: 9 AM on the 15th, day-of-week unspecified
0 9 ? * MON       # Quartz: 9 AM every Monday, day-of-month unspecified

10 Real-World Cron Examples

The best way to solidify your understanding is to study real examples with context for why each schedule was chosen. Here are ten patterns you will encounter (and use) regularly.

1. Every Weekday at 9:00 AM

0 9 * * 1-5

The minute is 0 (top of the hour), hour is 9 (9 AM), day-of-month and month are wildcards, and day-of-week is 1-5 (Monday through Friday). Used for daily standup reminders, report emails sent at the start of the business day, and morning data sync jobs that should not run over the weekend.

2. Every 15 Minutes

*/15 * * * *

The step syntax */15 in the minute field gives you runs at 0, 15, 30, and 45 minutes past every hour, around the clock. Common for health check pings, cache warming, webhook retries, and any near-real-time polling task where you need freshness but true real-time is overkill or not available.

3. Every Day at Midnight

0 0 * * *

Minute 0, hour 0, everything else wildcard. This is one of the most common cron patterns in existence. Used for daily report generation, log rotation, database archiving, clearing temporary files, sending daily digest emails, and any "once a day" task that should run outside business hours.

4. First Day of Every Month at Midnight

0 0 1 * *

Day-of-month is 1, everything else is wildcard (except fixed minute/hour). This runs on January 1st, February 1st, March 1st, and so on. The go-to schedule for monthly invoice generation, billing cycle triggers, SaaS subscription renewals, and monthly analytics roll-ups.

5. Every Sunday at 2:00 AM

0 2 * * 0

Day-of-week 0 is Sunday, and hour 2 is 2 AM — a time when traffic is typically at its lowest. Used for weekly full database backups, sitemap regeneration, content re-indexing for search, and heavy batch processing jobs that would impact performance during the week.

6. Weekdays at 8:30 AM, 12:30 PM, and 5:30 PM

30 8,12,17 * * 1-5

This combines a list in the hour field with a range in the day-of-week field. The minute 30means it fires at the half-hour mark. Used for scheduled notification batches (push notifications, email digests), three-times-daily data sync jobs, and any workflow where you want regular touchpoints throughout the business day without hammering every hour.

7. January 1st at Midnight

0 0 1 1 *

Day-of-month 1 and month 1 (January) together pin this to New Year's Day. Used for annual tasks like yearly subscription renewals, archiving the previous year's data, generating annual compliance reports, and resetting yearly quotas or counters in applications.

8. Every 5 Minutes During Business Hours on Weekdays

*/5 9-17 * * 1-5

A compound expression combining a step (*/5), a range in hours (9-17), and a range in day-of-week (1-5). This gives you aggressive monitoring or polling — every 5 minutes from 9 AM to 5 PM on Monday through Friday — while going quiet overnight and on weekends to save resources and avoid alert fatigue.

9. Every 6 Hours

0 */6 * * *

The step in the hour field (*/6) gives four evenly-spaced runs per day: midnight, 6 AM, noon, and 6 PM. Used for data synchronization between systems, refreshing long-lived API tokens or OAuth credentials before they expire, and periodic cache invalidation for content that changes a few times a day but does not need minute-level freshness.

10. 15th and Last Day of Every Month

0 0 15,L * *

A comma list in the day-of-month field combining a fixed date (15) and the last-day shorthand (L). This is the classic semi-monthly payroll schedule — pay periods that end on the 15th and on the last day of the month. Note that L requires an implementation that supports it (Quartz, some Linux crons); standard crontab does not support L.

Common Mistakes and Gotchas

Cron expressions have several well-known pitfalls that cause production incidents. Understanding them upfront will save you a painful debugging session at 2 AM.

Day-of-Week Numbering Is Not Universal

Most Unix cron implementations treat both 0 and 7 as Sunday. But some tools (including certain application-level libraries) start the week on Monday, making 1 = Monday and 7 = Sunday. Always verify the numbering convention for the specific tool you are using, and prefer using three-letter abbreviations (MON, TUE, etc.) when the implementation supports them to eliminate ambiguity.

Cron Runs in the Server's Timezone

This is probably the most common source of cron bugs in production. 0 9 * * * means 9 AM in the timezone of the machine running the job — which may be UTC, US/Eastern, or anything else. Always document the timezone assumption in a comment next to the cron expression. For cloud-based schedulers, explicitly configure the timezone if the platform supports it.

# Good practice: always document the timezone
# Runs at 9 AM US/Eastern (UTC-5 or UTC-4 during DST)
0 14 * * 1-5   # 9 AM ET expressed in UTC

GitHub Actions Cron Always Runs on UTC

GitHub Actions uses standard 5-field cron syntax under the on: schedule: key, but the scheduler always operates in UTC — there is no timezone configuration option. If you want a job to run at 9 AM Eastern time, you need to schedule it at 0 14 * * * (UTC). Also note that GitHub Actions scheduled workflows may run up to 15 minutes late during periods of high demand.

The Step Syntax Applies to Its Field, Not Minutes

A common misreading: */5 in the hour field means every 5 hours — not every 5 minutes. The step always applies to the unit of the field it is in. */5 in the minute field is every 5 minutes; in the hour field, every 5 hours; in the month field, every 5 months.

Jobs That Run Longer Than Their Interval Can Overlap

Cron is a fire-and-forget scheduler. If you schedule a job every 5 minutes and a job instance takes 7 minutes to complete, a second instance will start while the first is still running. This can cause race conditions, duplicate processing, and data corruption. Use a file lock or an advisory lock in your database to prevent concurrent execution of the same job.

Missing Fields vs. Wildcards Are Not Always Equivalent

In some extended cron implementations (particularly Quartz), omitting a field and using *are treated differently. Always use all required fields explicitly and never rely on defaults for critical production schedules.

Non-Standard Extensions: 6-Field Cron

The standard Unix cron has five fields, with minute as the finest granularity. Several systems extend this with additional fields:

Seconds Field (Prepended)

Many application-level schedulers (node-cron, Quartz, Spring Scheduler) add a seconds field at the beginning, giving you 6 fields. This enables sub-minute scheduling down to the second. The fields are: second minute hour day-of-month month day-of-week.

# 6-field cron with seconds prepended (Quartz / node-cron)
0 */5 * * * *    # Every 5 minutes (second=0, minute=*/5, ...)
30 0 9 * * 1-5   # Weekdays at 9:00:30 AM

AWS EventBridge (6 Fields with Year)

AWS EventBridge uses a 6-field format where a year field is appended at the end:minute hour day-of-month month day-of-week year. It also requires using ?for either day-of-month or day-of-week (never both as wildcards at the same time). AWS EventBridge does not support the */ step syntax in the same way as Unix cron.

# AWS EventBridge cron format (6 fields, year at end)
cron(0 9 ? * MON-FRI *)    # Weekdays at 9 AM UTC, any year
cron(0 0 1 * ? *)           # First day of every month at midnight
Quick tip: When copying a cron expression between platforms, always verify the field count and any platform-specific syntax differences. A valid Unix cron expression may be invalid (or mean something different) in AWS EventBridge, Quartz, or a node-cron context.

How to Use the SoftStash Cron Parser

Writing a cron expression from scratch is one skill — validating that you wrote it correctly is another. The SoftStash Cron Parser makes it trivial to verify any expression before it goes anywhere near production.

Paste any 5-field (or 6-field) cron expression into the tool and instantly get:

  • A human-readable description of the schedule ("Every weekday at 9:00 AM") so you can verify your intent matches your expression at a glance.
  • The next 5–10 scheduled run times listed out explicitly, so you can see exactly when the job will fire and confirm there are no surprises.
  • Instant feedback on invalid syntax — helpful if you have a typo or are working with an expression someone else wrote.

Everything runs entirely in your browser — no expression is sent to any server. It is the fastest way to sanity-check a schedule before deploying to GitHub Actions, Kubernetes, or any other platform.

Cron Expression Reference Table

Use this table as a quick reference. Bookmark this page and come back to it whenever you need to look up a pattern or verify what an expression means.

ExpressionHuman-Readable MeaningTypical Use Case
* * * * *Every minuteHigh-frequency polling, testing
*/5 * * * *Every 5 minutesHealth checks, cache warming
*/15 * * * *Every 15 minutesData sync, webhook retries
0 * * * *Every hour on the hourHourly aggregations, API calls
0 */6 * * *Every 6 hoursToken refresh, data sync
0 0 * * *Every day at midnightDaily reports, log rotation
0 9 * * 1-5Weekdays at 9:00 AMBusiness-hours jobs, reminders
0 2 * * 0Every Sunday at 2:00 AMWeekly backups, maintenance
0 0 1 * *First of every month at midnightMonthly invoices, billing
0 0 1,15 * *1st and 15th of every monthSemi-monthly payroll
0 0 1 1 *January 1st at midnightAnnual tasks, yearly reset
30 8,12,17 * * 1-5Weekdays at 8:30, 12:30, 17:30Notification batches
*/5 9-17 * * 1-5Every 5 min during business hours (weekdays)Active monitoring, polling

Validate Your Cron Expressions Before You Deploy

Cron expressions are compact and powerful, but their terseness means a single typo can silently produce a completely different schedule. A job you intended to run monthly might run daily. A backup you meant to trigger every Sunday might never run at all. The cost of a wrong schedule in production can range from a missed report to a billing job that fires hundreds of times.

The two-minute habit of pasting your expression into a validator and reviewing the next few scheduled run times before deploying is one of the highest-value practices in DevOps and backend engineering. It catches mistakes before they become incidents.

Validate Any Cron Expression Instantly — Free, Private, In-Browser

Paste your expression, get a human-readable description, and see the next scheduled run times. Nothing leaves your browser.

Open the Cron Parser →

🛠️

Try the Tools — 100% Free, No Sign-Up

Everything runs in your browser. No uploads. No accounts. No ads.

Explore All Tools →