Why Every Developer Needs to Understand Cron
Automated scheduling is fundamental to modern software operations. Whether you are rotating logs on a Linux server, triggering a CI pipeline, or scaling pods in Kubernetes, cron expressions are the lingua franca that tells a system when to act.
Despite their compact syntax, cron expressions trip up even experienced engineers. An off-by-one in the day-of-week field can mean the difference between a nightly backup and a silent data loss. This guide walks you through the syntax, demonstrates common patterns across real-world environments, and provides a cheat sheet you can bookmark for daily reference.
Anatomy of a Cron Expression
The Standard 5-Field Format
The classic Unix crontab uses five space-separated fields:
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12 or JAN-DEC)
│ │ │ │ ┌───────────── day of week (0-7, where 0 and 7 = Sunday, or SUN-SAT)
│ │ │ │ │
* * * * *
Each field accepts a number, a range, a list, or a step value. Together, the five fields define a repeating schedule down to the minute.
The 6-Field Format (With Seconds)
Some systems, notably Spring Boot's @Scheduled, Quartz Scheduler, and AWS EventBridge rate/cron rules, prepend a seconds field:
┌───────────── second (0-59)
│ ┌───────────── minute (0-59)
│ │ ┌───────────── hour (0-23)
│ │ │ ┌───────────── day of month (1-31)
│ │ │ │ ┌───────────── month (1-12)
│ │ │ │ │ ┌───────────── day of week (0-7)
│ │ │ │ │ │
* * * * * *
Always check the documentation for the platform you are targeting. Pasting a 6-field expression into a system that expects 5 fields (or vice versa) is one of the most frequent scheduling bugs.
Special Characters
| Character | Meaning | Example | Explanation |
|---|---|---|---|
* |
Any value | * * * * * |
Every minute |
, |
List | 1,15 * * * * |
At minute 1 and 15 |
- |
Range | 0 9-17 * * * |
Every minute from 9 AM to 5 PM |
/ |
Step | */5 * * * * |
Every 5 minutes |
? |
No specific value (day fields only) | 0 0 ? * MON |
Midnight every Monday (Quartz) |
L |
Last (day fields) | 0 0 L * * |
Last day of every month (Quartz) |
W |
Nearest weekday | 0 0 15W * * |
Nearest weekday to the 15th (Quartz) |
# |
Nth weekday | 0 0 * * 5#3 |
Third Friday of each month (Quartz) |
Tip: The
?,L,W, and#characters are extensions. Standard Unix cron only supports*,,,-, and/. If you are writing schedules for GitHub Actions or Linux crontab, stick with the first four.
Common Cron Patterns Cheat Sheet
Here is a quick-reference table of patterns that cover the vast majority of scheduling needs. Keep this handy, or use an online cron parser to validate expressions before deploying them.
| Schedule | Expression | Notes |
|---|---|---|
| Every minute | * * * * * |
Useful for health checks; use sparingly in production |
| Every 5 minutes | */5 * * * * |
Good default for polling jobs |
| Every 15 minutes | */15 * * * * |
Cache warm-up, metrics aggregation |
| Every hour at :00 | 0 * * * * |
Hourly reports |
| Every 6 hours | 0 */6 * * * |
Runs at 00:00, 06:00, 12:00, 18:00 |
| Daily at midnight | 0 0 * * * |
Nightly batch jobs, log rotation |
| Daily at 3:30 AM | 30 3 * * * |
Off-peak maintenance windows |
| Every weekday at 9 AM | 0 9 * * 1-5 |
Business-hours automation |
| Every Monday at 8 AM | 0 8 * * 1 |
Weekly reports |
| First day of each month at midnight | 0 0 1 * * |
Monthly billing runs |
| Every quarter (Jan, Apr, Jul, Oct) | 0 0 1 1,4,7,10 * |
Quarterly summaries |
| Every Sunday at 2 AM | 0 2 * * 0 |
Weekly full backups |
Cron Across Different Environments
One of the tricky parts of cron is that the syntax varies slightly depending on where you use it. Below is a comparison of four common environments.
1. Linux Crontab
The original. Edit with crontab -e and use the standard 5-field format.
# Rotate application logs every day at 2:30 AM
30 2 * * * /usr/sbin/logrotate /etc/logrotate.d/myapp
# Dump the database every Sunday at 1 AM
0 1 * * 0 /opt/scripts/pg_backup.sh >> /var/log/pg_backup.log 2>&1
Key detail: The PATH inside cron is minimal. Always use absolute paths for binaries and scripts. Redirect stderr so failures do not vanish silently.
2. GitHub Actions
GitHub Actions uses the 5-field POSIX cron format inside a schedule trigger. All times are UTC.
on:
schedule:
# Run integration tests every weekday at 06:00 UTC
- cron: '0 6 * * 1-5'
# Dependency audit every Monday at midnight UTC
- cron: '0 0 * * 1'
Caveats:
- GitHub does not guarantee execution at the exact minute. During periods of high load, runs may be delayed by several minutes.
- The minimum interval is approximately every 5 minutes, but schedules more frequent than every 15 minutes are discouraged.
- If no repository activity occurs for 60 days, scheduled workflows are automatically disabled.
3. Kubernetes CronJobs
Kubernetes CronJobs also use the standard 5-field format but add powerful options like concurrency policies and job history limits.
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-report
spec:
schedule: "0 0 * * *"
timeZone: "America/New_York" # K8s 1.27+
concurrencyPolicy: Forbid # skip if previous run is still active
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
template:
spec:
containers:
- name: report
image: myregistry/report-gen:latest
command: ["python", "generate_report.py"]
restartPolicy: OnFailure
Key detail: Before Kubernetes 1.27, all schedules ran in the kube-controller-manager's timezone (usually UTC). The timeZone field lets you specify an IANA timezone directly. Always set it explicitly to avoid surprises after cluster migrations.
4. AWS EventBridge (CloudWatch Events)
AWS EventBridge supports two schedule types: rate expressions and cron expressions. The cron variant uses a 6-field format with a mandatory year field and different day-of-week numbering (1 = Sunday through 7 = Saturday).
cron(minutes hours day-of-month month day-of-week year)
Example: Run a Lambda every weekday at 9:00 AM UTC.
cron(0 9 ? * MON-FRI *)
Caveats:
- You must use
?in either the day-of-month or day-of-week field (you cannot specify both). - The
yearfield is required but is almost always set to*. - EventBridge does not support the
/step syntax in all fields. Check the AWS docs for your specific use case.
Debugging and Validating Cron Expressions
Writing a cron expression is one thing. Being confident that it fires at the times you expect is another. Here are practical approaches:
Use a cron parser. Paste your expression into an interactive cron tool to see the next N execution times rendered in a human-readable list. This catches off-by-one errors immediately.
Dry-run locally. On Linux, list upcoming cron triggers with
systemctl list-timers(for systemd timers) or install a utility likecroniter(Python) to compute the next fire times programmatically.Log aggressively at first. When deploying a new cron job, log a timestamp at the very start of the script. Verify after a few cycles that the actual trigger times match your expectations.
Watch out for DST. If your cron daemon is timezone-aware, Daylight Saving Time transitions can cause jobs to fire twice or skip entirely. UTC schedules avoid this issue altogether.
For a deeper walkthrough on building expressions from scratch, check out the Cron Expression Generator Guide, which covers interactive generation, edge cases, and copy-paste templates.
Advanced Patterns
Once you are comfortable with the basics, here are a few patterns that solve less obvious problems.
Run on the Last Day of Every Month
Standard cron has no L keyword, but you can use a shell trick:
# Fires at midnight on the 28th-31st; the test ensures
# it only runs if tomorrow is the 1st (i.e., today is the last day).
0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /opt/scripts/month-end.sh
In Quartz or Spring, you can simply use 0 0 0 L * ?.
Run Every Other Wednesday
Cron has no built-in concept of "every other week." The cleanest approach is a wrapper that checks the ISO week number:
0 9 * * 3 [ $(($(date +\%V) \% 2)) -eq 0 ] && /opt/scripts/biweekly.sh
Stagger Multiple Jobs
If you have 10 workers that all poll at */5 * * * *, they will stampede the upstream service at the same instant. Offset them:
# Worker 1
0,5,10,15,20,25,30,35,40,45,50,55 * * * *
# Worker 2
1,6,11,16,21,26,31,36,41,46,51,56 * * * *
Or, if your scheduler supports random delay (systemd RandomizedDelaySec, Kubernetes startingDeadlineSeconds), use that instead of manual offsets.
Common Mistakes to Avoid
Confusing day-of-week numbering. Linux cron: 0 = Sunday. Quartz: 1 = Sunday. AWS: 1 = Sunday. Always verify for your platform.
Forgetting that
*/nstarts from 0, not from now. The expression*/10 * * * *runs at :00, :10, :20, etc., regardless of when the crontab was saved.Assuming minute-level precision. Managed services like GitHub Actions and AWS EventBridge may delay execution by seconds or minutes. Never build time-critical logic around exact cron firing times.
Ignoring overlapping runs. A job scheduled every minute that takes 3 minutes to complete will stack up. Use a lock file (
flock), KubernetesconcurrencyPolicy: Forbid, or an equivalent guard.Editing the wrong crontab.
crontab -eedits your user crontab. System-wide jobs go in/etc/cron.d/. Root jobs go in root's crontab. Mixing these up leads to permission errors that only surface at runtime.
Wrapping Up
Cron expressions pack a surprising amount of scheduling power into a handful of characters. The syntax is stable, portable, and supported across nearly every platform you will encounter in a modern stack, from bare-metal Linux servers to managed Kubernetes clusters and serverless functions.
The key to confidence is practice and validation. Bookmark a reliable cron parser, test expressions before deploying them, and always log the first few executions of a new schedule.
Happy scheduling.
Top comments (0)