Cron expressions are the backbone of task scheduling across Unix/Linux systems, cloud platforms, and CI/CD pipelines. Whether you need to run a database backup every night at 3 AM, send weekly reports every Monday, or trigger a build pipeline every 15 minutes, cron expressions give you precise, declarative control over when your jobs execute. This comprehensive guide covers everything from basic 5-field syntax to advanced special characters, platform-specific differences, timezone handling, debugging techniques, and security considerations.
TL;DR
A cron expression is a time-based scheduling string with 5 fields (minute, hour, day-of-month, month, day-of-week). Extended formats add seconds or year fields. Use special characters like *, /, -, and comma for flexible scheduling. Platform differences exist between Linux crontab, AWS EventBridge, GitHub Actions, Kubernetes CronJob, and Vercel Cron. Always consider timezones, overlap prevention, and output logging when deploying cron jobs in production.
Key Takeaways
- Standard cron uses 5 fields; some systems (Quartz, Spring, AWS) add a 6th (seconds) or 7th (year) field.
- The asterisk (*) means "every value", slash (/) defines step intervals, dash (-) defines ranges, and comma (,) defines lists.
- Special characters L, W, and # are available in extended implementations like Quartz for last-day, weekday, and nth-weekday scheduling.
- GitHub Actions and many CI/CD platforms run cron in UTC only; always verify which timezone your scheduler uses.
- Use file locking (flock) or advisory locks to prevent overlapping cron job executions.
- Redirect stdout and stderr to log files and set up monitoring alerts for production cron jobs.
What Is a Cron Expression?
A cron expression is a compact, whitespace-separated string that tells a scheduler exactly when to run a task. The term "cron" comes from the Greek word "chronos" meaning time. The cron daemon (crond) has been a core part of Unix-like operating systems since the 1970s, and its expression syntax has become the universal standard for time-based scheduling.
The most widely used format is the 5-field expression, where each field represents a time unit. Some implementations extend this to 6 or 7 fields.
5-Field Format (Standard Cron)
The standard cron expression used in Linux crontab, Kubernetes CronJob, GitHub Actions, and Vercel Cron:
ββββββββββββββ Minute (0β59)
β ββββββββββββ Hour (0β23)
β β ββββββββββ Day of Month (1β31)
β β β ββββββββ Month (1β12)
β β β β ββββββ Day of Week (0β7)
β β β β β
* * * * *6-Field Format (Quartz / Spring)
Used by Java-based schedulers like Quartz, Spring @Scheduled, and AWS CloudWatch Events:
ββββββββββββββββ seconds (0β59)
β ββββββββββββββ Minute (0β59)
β β ββββββββββββ Hour (0β23)
β β β ββββββββββ Day of Month (1β31)
β β β β ββββββββ Month (1β12)
β β β β β ββββββ Day of Week (0β7)
β β β β β β
* * * * * *Field-by-Field Breakdown
Minute
Allowed values: 0 through 59. Specifies the minute(s) of each hour when the job should run.
0 β at the top of the hour
*/5 β every 5 minutes (0, 5, 10, 15, ...)
15,45 β at minute 15 and 45
10-20 β every minute from 10 through 20Hour
Allowed values: 0 through 23 (24-hour format). 0 is midnight, 12 is noon, 23 is 11 PM.
0 β midnight
9 β 9 AM
9-17 β every hour from 9 AM to 5 PM
*/4 β every 4 hours (0, 4, 8, 12, 16, 20)Day of Month
Allowed values: 1 through 31. Be careful with 29, 30, 31 as not all months have these days.
1 β first day of month
15 β 15th of the month
1,15 β 1st and 15th
L β last day (Quartz only)Month
Allowed values: 1 through 12 (or JAN-DEC in some implementations). 1 is January, 12 is December.
1 β January
*/3 β every 3 months (Jan, Apr, Jul, Oct)
6-8 β June through August
1,4,7,10 β quarterly (Jan, Apr, Jul, Oct)Day of Week
Allowed values: 0 through 7 (or SUN-SAT). Both 0 and 7 represent Sunday. 1 is Monday, 6 is Saturday.
0 or 7 β Sunday
1 β Monday
1-5 β Monday through Friday (weekdays)
0,6 β Saturday and Sunday (weekend)
5#3 β 3rd Friday (Quartz only)Special Characters Explained
Asterisk (*)
Matches every possible value for that field. Using * in the minute field means "every minute" (0 through 59).
* * * * * # every minute of every hour of every daySlash (/)
Defines step values (increments). */15 in the minute field means "every 15 minutes" (0, 15, 30, 45). You can also combine with a start value: 5/15 means "every 15 minutes starting at minute 5" (5, 20, 35, 50).
*/10 * * * * # every 10 minutes
5/15 * * * * # minutes 5, 20, 35, 50
0 */6 * * * # every 6 hours at minute 0Dash (-)
Defines an inclusive range. 9-17 in the hour field means "every hour from 9 AM to 5 PM". 1-5 in the day-of-week field means "Monday through Friday".
0 9-17 * * * # hourly from 9 AM to 5 PM
0 0 * * 1-5 # midnight, Monday through FridayComma (,)
Specifies a list of discrete values. 1,15 in the day-of-month field means "the 1st and 15th of the month". 0,30 in the minute field means "at minute 0 and minute 30".
0 0 1,15 * * # 1st and 15th at midnight
0 8,12,18 * * * # at 8 AM, noon, and 6 PML (Last)
Available in Quartz and some extended implementations. In the day-of-month field, L means "the last day of the month". In the day-of-week field, 5L means "the last Friday of the month".
# Quartz / Spring only:
0 0 L * ? # last day of every month at midnight
0 0 ? * 5L # last Friday of every month at midnightW (Weekday)
Available in Quartz and extended implementations. 15W in the day-of-month field means "the nearest weekday (Mon-Fri) to the 15th". If the 15th is a Saturday, the job runs on Friday the 14th.
# Quartz only:
0 0 15W * ? # nearest weekday to the 15th at midnight
0 0 1W * ? # nearest weekday to the 1st at midnightHash (#)
Available in Quartz. Specifies "the Nth weekday of the month". 5#3 in the day-of-week field means "the third Friday of every month". 1#1 means "the first Monday of each month".
# Quartz only:
0 10 ? * 5#3 # 3rd Friday at 10 AM
0 9 ? * 1#1 # 1st Monday at 9 AM
0 9 ? * 1#2 # 2nd Monday at 9 AMQuestion Mark (?)
Used in Quartz and AWS to indicate "no specific value" for day-of-month or day-of-week. Required when one of these two fields has a specific value and the other must be left open.
# AWS / Quartz:
0 10 ? * 2 # every Monday at 10 AM (? for day-of-month)
0 0 15 * ? # 15th of month at midnight (? for day-of-week)Common Cron Patterns with Examples
| Expression | Description |
|---|---|
| * * * * * | Every minute |
| */5 * * * * | Every 5 minutes |
| */15 * * * * | Every 15 minutes |
| 0 * * * * | Every hour (at minute 0) |
| 0 0 * * * | Daily at midnight |
| 0 6 * * * | Daily at 6:00 AM |
| 30 8 * * 1-5 | Weekdays at 8:30 AM |
| 0 9 * * 1 | Every Monday at 9:00 AM |
| 0 0 * * 0 | Every Sunday at midnight |
| 0 0 1 * * | First day of every month (midnight) |
| 0 0 1 1 * | January 1st at midnight |
| 0 0 1 */3 * | Every 3 months (quarterly) |
| 0 12 * * 1-5 | Weekdays at noon |
| 0 0 * * 1,4 | Monday and Thursday at midnight |
| 0 8-17 * * 1-5 | Hourly from 8 AM to 5 PM, weekdays |
| 0 */2 * * * | Every 2 hours |
| 0 22 * * 5 | Every Friday at 10 PM |
| 0 0 15 * * | 15th of every month at midnight |
Parse and test your cron expressions interactively
Open Cron Expression ParserCron in Different Environments
Linux Crontab
The classic cron implementation. Edit with "crontab -e", list with "crontab -l". Uses 5-field syntax. Runs in the system timezone by default. Supports MAILTO for emailing output.
# Edit current user's crontab
crontab -e
# List current crontab entries
crontab -l
# Example: backup database daily at 3 AM
0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# Set timezone (at top of crontab)
TZ=America/New_York
0 9 * * * /path/to/script.sh
# Send output via email
MAILTO=admin@example.com
0 6 * * 1 /path/to/weekly-report.shAWS EventBridge (CloudWatch Events)
AWS uses a 6-field format: minute, hour, day-of-month, month, day-of-week, year. Requires ? for either day-of-month or day-of-week. Supports UTC only by default (EventBridge Scheduler supports timezones). Rate expressions are an alternative: rate(5 minutes).
# AWS EventBridge 6-field format:
# minute hour day-of-month month day-of-week year
# Every 5 minutes
cron(*/5 * * * ? *)
# Daily at 10 AM UTC
cron(0 10 * * ? *)
# First Monday of every month at 8 AM
cron(0 8 ? * 2#1 *)
# Rate expression alternative
rate(5 minutes)
rate(1 hour)
rate(7 days)GitHub Actions
Uses standard 5-field POSIX cron syntax in the schedule trigger. Runs in UTC only. Minimum interval is every 5 minutes. Jobs may be delayed during high load. Defined in .github/workflows/*.yml under on.schedule.
# .github/workflows/scheduled.yml
name: Scheduled Job
on:
schedule:
# Runs at 06:00 UTC every day
- cron: '0 6 * * *'
# Runs every 15 minutes
- cron: '*/15 * * * *'
# Runs every Monday at 9 AM UTC
- cron: '0 9 * * 1'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Triggered by cron schedule"Kubernetes CronJob
Uses standard 5-field cron syntax in spec.schedule. Runs in the timezone of the kube-controller-manager (usually UTC). Starting from Kubernetes 1.27, you can set spec.timeZone (e.g., "America/New_York"). Supports concurrencyPolicy: Allow, Forbid, or Replace.
apiVersion: batch/v1
kind: CronJob
metadata:
name: database-backup
spec:
schedule: "0 3 * * *" # daily at 3 AM
timeZone: "America/New_York" # K8s 1.27+
concurrencyPolicy: Forbid # prevent overlapping
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: postgres:16
command: ["/bin/sh", "-c", "pg_dump ..."]
restartPolicy: OnFailureVercel Cron
Defined in vercel.json under the crons array. Uses standard 5-field syntax. Runs in UTC. Triggers a serverless function at the specified path. Free tier allows 2 cron jobs per project; Pro tier allows more.
// vercel.json
{
"crons": [
{
"path": "/api/daily-cleanup",
"schedule": "0 0 * * *"
},
{
"path": "/api/hourly-sync",
"schedule": "0 * * * *"
}
]
}Cron Expression Parsing Libraries
Node.js: cron-parser
The most popular cron parsing library for Node.js. Parses cron expressions and calculates the next N occurrence dates. Supports 5-field and 6-field (with seconds) formats.
import { parseExpression } from 'cron-parser';
const interval = parseExpression('*/15 * * * *');
console.log('Next:', interval.next().toString());
console.log('Next:', interval.next().toString());
console.log('Next:', interval.next().toString());
// With options
const options = {
currentDate: new Date('2026-01-01T00:00:00'),
tz: 'America/New_York'
};
const expr = parseExpression('0 9 * * 1-5', options);
for (let i = 0; i < 5; i++) {
console.log(expr.next().toString());
}Python: croniter
The standard Python library for iterating over cron schedule dates. Supports standard 5-field and extended 6-field expressions. Works well with datetime objects.
from croniter import croniter
from datetime import datetime
base = datetime(2026, 1, 1)
cron = croniter('0 9 * * 1-5', base)
# Get next 5 occurrences
for _ in range(5):
print(cron.get_next(datetime))
# Check if a specific time matches
print(croniter.match('0 9 * * 1', datetime(2026, 3, 2, 9, 0)))
# True (March 2, 2026 is a Monday)Other Languages
Go: robfig/cron (the de facto standard). Java: Quartz Scheduler (includes its own cron parser). Rust: cron crate. PHP: dragonmantank/cron-expression.
// Go: robfig/cron v3
import "github.com/robfig/cron/v3"
c := cron.New()
c.AddFunc("0 9 * * 1-5", func() {
fmt.Println("Weekday morning job")
})
c.Start()
// Rust: cron crate
use cron::Schedule;
let schedule = Schedule::from_str("0 9 * * 1-5").unwrap();
for dt in schedule.upcoming(Utc).take(5) {
println!("{}", dt);
}Timezone Handling in Cron Jobs
Timezone mismanagement is the number one source of cron job bugs. Different systems handle timezones differently:
- Linux crontab: Runs in the system timezone. Set the TZ variable at the top of crontab to override.
- GitHub Actions: Always UTC. No timezone configuration available.
- AWS EventBridge: UTC by default. EventBridge Scheduler supports IANA timezones.
- Kubernetes: Controller timezone (usually UTC). Use spec.timeZone (Kubernetes 1.27+) for per-job timezone.
- Vercel Cron: Always UTC.
Daylight Saving Time (DST) Considerations
When your cron job depends on local time and your timezone observes DST, jobs can be skipped or run twice during clock changes. Spring forward: a 2:30 AM job may be skipped. Fall back: a 1:30 AM job may run twice. Best practice is to run critical jobs in UTC and convert timestamps in your application logic.
# Best practice: run in UTC and convert in your app
TZ=UTC
0 8 * * * /path/to/job.sh # always 08:00 UTC
# Inside job.sh, convert to local time if needed:
LOCAL_TIME=$(TZ="America/New_York" date)Debugging Cron Jobs: Logging, Monitoring & Alerting
Logging Output
Always redirect stdout and stderr to a log file. In crontab:
# Redirect both stdout and stderr
*/5 * * * * /path/to/job.sh >> /var/log/myjob.log 2>&1
# With timestamp
*/5 * * * * echo "$(date): starting job" >> /var/log/myjob.log && /path/to/job.sh >> /var/log/myjob.log 2>&1
# Send output via email
MAILTO=dev@example.com
0 6 * * * /path/to/morning-report.shMonitoring
Use a dead-man-switch service (Cronitor, Healthchecks.io, Better Uptime) where your cron job pings a URL on success. If the ping does not arrive within the expected window, you get alerted. This catches silent failures where the job runs but produces no output.
# Dead-man switch with Healthchecks.io
0 3 * * * /path/to/backup.sh && curl -fsS --retry 3 https://hc-ping.com/YOUR-UUID
# Dead-man switch with Cronitor
0 3 * * * cronitor exec abc123 /path/to/backup.sh
# Custom alerting with webhook
0 3 * * * /path/to/backup.sh || curl -X POST https://hooks.slack.com/... -d '{"text":"Backup failed!"}'Alerting
Set MAILTO in crontab to receive email output. For cloud-based cron, integrate with PagerDuty, Opsgenie, or Slack webhooks. Monitor exit codes: a non-zero exit should trigger an alert.
Dry Run Testing
Before deploying, verify your cron expression by calculating the next several execution times. Use our online cron parser tool or the cron-parser library to generate upcoming dates programmatically.
# Verify with cron-parser (Node.js)
npx cron-parser "0 9 * * 1-5"
# Verify in Python
python3 -c "
from croniter import croniter
from datetime import datetime
c = croniter('0 9 * * 1-5', datetime.now())
for _ in range(10):
print(c.get_next(datetime))
"Cron vs Alternatives
systemd Timers
Modern Linux alternative to cron. Advantages: better logging via journalctl, dependency management, and resource control with cgroups. Create a .timer unit file paired with a .service file. Supports monotonic timers (run 5 minutes after boot) and calendar-based timers.
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup timer
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Database backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
# Enable and start
systemctl enable --now backup.timer
systemctl list-timersat Command
One-shot scheduler for running a command once at a specific time. Unlike cron, it does not repeat. Useful for scheduled maintenance or deferred tasks. Syntax: echo "command" | at 3:00 PM tomorrow.
# Schedule a one-time job
echo "/usr/local/bin/maintenance.sh" | at 3:00 AM tomorrow
echo "reboot" | at 2:00 AM Jan 15
# List pending jobs
atq
# Remove a scheduled job
atrm 42Windows Task Scheduler
The Windows equivalent of cron. GUI-based with XML-backed definitions. Supports triggers (time, event, logon) and conditions (idle, network). Can be managed via PowerShell with the ScheduledTasks module.
# PowerShell: create a scheduled task
$action = New-ScheduledTaskAction -Execute "C:\scripts\backup.ps1"
$trigger = New-ScheduledTaskTrigger -Daily -At 3am
Register-ScheduledTask -TaskName "DailyBackup" -Action $action -Trigger $trigger
# List scheduled tasks
Get-ScheduledTask | Where-Object {$_.State -eq 'Ready'}Quick Comparison
| Feature | cron | systemd timers | at | Task Scheduler |
|---|---|---|---|---|
| Recurring | Yes | Yes | No (one-shot) | Yes |
| Logging | Manual redirect | journalctl | Event Viewer | |
| Dependencies | None | After/Requires | None | Triggers |
| Resource limits | None | cgroups | None | Settings |
| Syntax | 5 fields | Calendar spec | Natural language | GUI / XML |
| Platform | Unix/Linux | Linux (systemd) | Unix/Linux | Windows |
Common Mistakes and Pitfalls
Forgetting Timezone
Your cron daemon and your mental model may use different timezones. Always verify with "date" on the server or check the scheduler documentation. CI/CD platforms almost always use UTC.
Running Every Minute Accidentally
The expression * * * * * runs your job 1,440 times per day. This is rarely what you want. Use */5 * * * * for every 5 minutes or 0 * * * * for every hour.
Confusing Day-of-Week Numbering
In standard POSIX cron, Sunday is 0 (and also 7). Monday is 1. But in Quartz, Sunday is 1 and Saturday is 7. Check your platform documentation.
| Day | POSIX cron | Quartz |
|---|---|---|
| Sunday | 0 (or 7) | 1 |
| Monday | 1 | 2 |
| Tuesday | 2 | 3 |
| Wednesday | 3 | 4 |
| Thursday | 4 | 5 |
| Friday | 5 | 6 |
| Saturday | 6 | 7 |
Not Handling Overlapping Executions
If a job takes 10 minutes but runs every 5 minutes, you get overlapping executions. Use flock, Kubernetes concurrencyPolicy: Forbid, or a distributed lock (Redis SETNX).
# Use flock to prevent overlapping
*/5 * * * * flock -n /tmp/myjob.lock /path/to/long-running-job.sh
# Kubernetes concurrencyPolicy
spec:
concurrencyPolicy: Forbid # skip if previous still runningIgnoring Output and Errors
Cron runs without a terminal. Environment variables, PATH, and shell initialization differ from interactive sessions. Always use absolute paths for commands and redirect output.
# BAD: relative path, no output capture
*/5 * * * * ./cleanup.sh
# GOOD: absolute path, full environment, output redirect
*/5 * * * * /usr/bin/env bash /home/deploy/scripts/cleanup.sh >> /var/log/cleanup.log 2>&1Specifying Both Day-of-Month and Day-of-Week
In standard cron, if both fields are set (not *), the job runs when EITHER condition matches (OR logic), not when both match (AND logic). This catches many developers off guard.
# This runs on the 15th OR on Mondays (OR logic, not AND)
0 0 15 * 1
# If you want the 15th AND it must be a Monday,
# use a wrapper script with date checks instead.Security Considerations for Cron Jobs
Principle of Least Privilege
Run cron jobs under a dedicated service account, not root. Use /etc/cron.allow and /etc/cron.deny to control which users can create crontabs.
# Restrict cron access
echo "deploy" >> /etc/cron.allow
echo "www-data" >> /etc/cron.deny
# Run as non-root user
su - deploy -c "crontab -e"Secure Script Paths
Use absolute paths in your crontab entries. A relative path could be hijacked by a malicious script placed in the working directory. Ensure scripts and their parent directories have proper permissions (owned by root or the service account, not world-writable).
# GOOD: absolute path
0 3 * * * /home/deploy/scripts/backup.sh
# Secure script permissions
chmod 750 /home/deploy/scripts/backup.sh
chown deploy:deploy /home/deploy/scripts/backup.shProtect Credentials
Never hardcode passwords or API keys in cron scripts. Use environment variables loaded from a secured file (chmod 600), a secrets manager (AWS Secrets Manager, HashiCorp Vault), or Kubernetes Secrets.
# Load credentials from a secured file
0 3 * * * . /home/deploy/.env && /home/deploy/scripts/backup.sh
# .env file permissions
chmod 600 /home/deploy/.env
chown deploy:deploy /home/deploy/.envAudit and Rotate Logs
Cron job logs can contain sensitive data. Use logrotate to manage log file sizes. Restrict log file permissions. Consider forwarding logs to a centralized system with access controls.
Monitor for Unauthorized Changes
Use file integrity monitoring (AIDE, OSSEC) to detect unauthorized modifications to crontab files. Review /var/spool/cron/ periodically.
See also: timestamp converter
Parse and test your cron expressions interactively
Open Cron Expression ParserFrequently Asked Questions
What is a cron expression and what are the 5 fields?
A cron expression is a time-based string that defines when a scheduled job runs. The 5 fields are: minute (0-59), hour (0-23), day of month (1-31), month (1-12), and day of week (0-7, where 0 and 7 both represent Sunday). Each field can use special characters like *, /, -, and comma to define complex schedules.
How do I write a cron expression for every 5 minutes?
Use */5 * * * *. The */5 in the minute field means "every 5th minute starting from 0". This triggers at minutes 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, and 55 of every hour, every day.
What is the difference between 5-field and 6-field cron expressions?
The standard 5-field format covers minute, hour, day-of-month, month, and day-of-week. The 6-field format (used by Quartz Scheduler, Spring, and AWS) prepends a seconds field. Some 7-field formats also append a year field. GitHub Actions, Kubernetes, and Linux crontab use the 5-field format.
How do cron expressions handle timezones?
Linux crontab uses the system timezone (overridable with the TZ variable). GitHub Actions and Vercel always use UTC. AWS EventBridge Scheduler supports IANA timezone names. Kubernetes 1.27+ supports spec.timeZone on CronJob resources. Always verify your scheduler timezone to avoid missed or doubled executions.
What do the special characters L, W, and # mean in cron?
These are Quartz/extended-only characters. L means "last": in day-of-month it is the last day; 5L in day-of-week is the last Friday. W means "nearest weekday": 15W fires on the closest Mon-Fri to the 15th. # means "nth day": 5#3 in day-of-week is the third Friday of the month.
How can I prevent overlapping cron job executions?
Use a file lock with flock: flock -n /tmp/myjob.lock /path/to/script.sh. In Kubernetes, set concurrencyPolicy: Forbid on the CronJob spec. For distributed systems, use a distributed lock via Redis SETNX or a database advisory lock.
What is the difference between cron and systemd timers?
Cron is the traditional Unix job scheduler with a simple expression syntax. systemd timers are the modern Linux alternative offering better logging (journalctl), dependency management, resource control via cgroups, and both monotonic (relative) and calendar (absolute) timer types. systemd timers require two unit files (.timer + .service) compared to a single crontab line.
Can I use cron expressions in Docker containers?
Yes, but the recommended approach is to use the host system cron or an orchestrator (Kubernetes CronJob, ECS Scheduled Tasks) rather than installing cron inside the container. If you must run cron in a container, ensure the cron daemon is the foreground process (cron -f) and that your Dockerfile installs cron and sets up the crontab correctly.
Conclusion
Cron expressions remain the universal language for time-based task scheduling. From a simple Linux crontab entry to sophisticated Kubernetes CronJobs with timezone support and concurrency policies, the core syntax has stood the test of time since the 1970s.
The keys to reliable cron job management are: understand your platform syntax and timezone, prevent overlapping runs, log all output, monitor execution with dead-man switches, and follow security best practices. Use our online cron expression parser to validate and test your expressions before deploying them to production.