BytePane

Crontab Guru: Visual Cron Schedule Expression Editor

Linux18 min read

The Production Incident That Ran on a Friday

A team scheduled their weekly database cleanup job with the expression 0 0 15 * 5. Their intent: the 15th of each month at midnight. Their actual schedule: midnight on the 15th of every month and midnight every Friday. The cleanup ran 52 times a year instead of 12. Nobody noticed for three months because the job was idempotent — until the Friday before a major deployment when it locked the database at exactly the wrong moment.

That OR behavior between day-of-month and day-of-week is the single most common cron misconception among experienced engineers. Cron dates to 1970s Unix — Ken Thompson wrote the original version at Bell Labs, and the name comes from the Greek word "chronos" meaning time — but its syntax has accumulated subtleties across 50 years of implementations, each with slightly different behavior.

Crontab guru, created by Cronitor, exists to surface exactly these subtleties. Paste any expression and it shows the next five scheduled run times in plain English. This guide covers the complete cron syntax, every common gotcha, and how the major cloud platforms diverge from the standard — so you understand what crontab guru is telling you and why.

Key Takeaways

  • Cron's day-of-month and day-of-week use OR logic, not AND — both fields non-* means the job runs on either condition matching.
  • Timezone is the most common silent failure: cron uses the system timezone; GitHub Actions and AWS EventBridge default to UTC.
  • AWS EventBridge uses a 6-field format (adds a year field) and requires ? in one of day-of-month or day-of-week.
  • Cron jobs fail silently — always redirect stdout and stderr to a log file, and monitor the job externally.
  • Use absolute paths in cron commands — the cron environment has a minimal PATH that will not find commands your shell session can.

A Brief History: From Bell Labs to Kubernetes

Cron's origin is in Version 7 Unix (1977). Thompson's original implementation was simple: it woke up once per minute, read jobs from a single /etc/lib/crontab file, and ran anything scheduled for that minute. There was one crontab for the entire system — no per-user scheduling.

Paul Vixie extended cron in the 1980s, introducing per-user crontabs (/var/spool/cron/), the modern five-field expression format, and the special @ shorthand strings. Vixie cron is still the implementation bundled with Debian, Ubuntu, and their derivatives. Red Hat forked it as cronie in 2007, adding PAM and SELinux support.

Today the same five-field expression syntax appears in Kubernetes CronJobs, GitHub Actions schedule triggers, Google Cloud Scheduler, and dozens of application-level job schedulers. AWS EventBridge is the notable exception — it extended the format to six fields. All of these platforms share the core syntax but diverge in important ways in edge cases.

The Five-Field Crontab Syntax: Complete Reference

A standard crontab entry has five time-specification fields followed by the command to run:

┌───── minute (0–59)
│ ┌─── hour (0–23)
│ │ ┌─ day-of-month (1–31)
│ │ │ ┌ month (1–12 or JAN–DEC)
│ │ │ │ ┌ day-of-week (0–6 or SUN–SAT, 0 and 7 both = Sunday)
│ │ │ │ │
* * * * *  /path/to/command

Special Characters

CharacterNameMeaningExample
*WildcardEvery value in the field's range* in minutes = every minute
,ListMultiple specific valuesMON,WED,FRI in day-of-week
-RangeAll values from start to end (inclusive)1-5 in day-of-week = Mon–Fri
/StepEvery Nth value in the range*/15 in minutes = 0,15,30,45
?No specific valueOnly in some implementations (AWS, Quartz)Required in AWS EventBridge

@ Shorthand Strings

Vixie cron introduced shorthand strings that expand to fixed five-field expressions. These are widely supported in Linux cron, systemd OnCalendar syntax, and most application-level schedulers:

ShorthandEquivalentMeaning
@yearly / @annually0 0 1 1 *Once per year, January 1 at midnight
@monthly0 0 1 * *Once monthly, 1st day at midnight
@weekly0 0 * * 0Once weekly, Sunday at midnight
@daily / @midnight0 0 * * *Once daily at midnight
@hourly0 * * * *Once per hour at :00
@rebootN/AOnce when the cron daemon starts (not standard, but widely supported)

30 Practical Cron Expression Examples

Concrete expressions anchored to real scheduling tasks — the cases where a developer actually reaches for crontab guru to verify.

# ─── Basic Intervals ────────────────────────────────────────────
* * * * *         # Every minute (development/testing only)
*/5 * * * *       # Every 5 minutes
*/15 * * * *      # Every 15 minutes
0 * * * *         # Every hour on the hour
0 */2 * * *       # Every 2 hours
0 */6 * * *       # Every 6 hours (00:00, 06:00, 12:00, 18:00)

# ─── Daily Tasks ────────────────────────────────────────────────
0 0 * * *         # Daily at midnight (same as @daily)
0 9 * * *         # Daily at 9:00 AM
30 23 * * *       # Daily at 11:30 PM
0 2 * * *         # Daily at 2:00 AM — good window for backups

# ─── Weekday / Weekend ──────────────────────────────────────────
0 9 * * 1-5       # 9 AM Monday through Friday
0 9 * * MON-FRI   # Identical (named days also work)
0 10 * * 6,0      # 10 AM Saturday and Sunday
0 6 * * 1         # Every Monday at 6 AM

# ─── Monthly Tasks ──────────────────────────────────────────────
0 0 1 * *         # First of every month at midnight
0 0 15 * *        # 15th of every month at midnight
0 0 L * *         # Last day of month (only supported by some impls)
0 0 1 1 *         # January 1 at midnight (annual)

# ─── Specific Times ─────────────────────────────────────────────
0 9,12,15 * * *   # At 9 AM, noon, and 3 PM daily
0 8-18 * * 1-5    # Every hour from 8 AM to 6 PM on weekdays
5 4 * * 0         # Sunday at 4:05 AM
30 6 1 1,7 *      # Jan 1 and Jul 1 at 6:30 AM

# ─── Step Values ────────────────────────────────────────────────
0/30 * * * *      # Every 30 minutes (same as */30)
10/20 * * * *     # At :10, :30, :50 every hour
0 6 1/7 * *       # Every 7 days starting on the 1st

# ─── Health Checks / Monitoring ─────────────────────────────────
*/2 * * * *       # Every 2 minutes (frequent health check)
*/10 * * * *      # Every 10 minutes (standard monitoring)

# ─── Production-Hardened (with absolute paths and logging) ──────
0 2 * * * /usr/local/bin/pg_dump mydb > /var/log/backups/$(date +%Y%m%d).sql 2>&1
*/5 * * * * /usr/bin/python3 /opt/scripts/health_check.py >> /var/log/health.log 2>&1

Note: In crontab files, the % character has special meaning (it becomes a newline in the command). Escape it as \% when using date format strings inside crontab entries.

10 Cron Gotchas That Cause Production Incidents

These are the mistakes that create the category of incident where nothing crashes immediately — the job just silently misbehaves until someone notices three days or three months later.

1. The Day-of-Month + Day-of-Week OR Trap

# INTENT: Run on the 15th only when it's a Friday
0 0 15 * 5   # ACTUAL: Runs on the 15th of every month
             #         AND on every Friday
             # This is OR logic, not AND

# FIX: Use only one constraint
0 0 15 * *   # Runs on the 15th (regardless of day-of-week)

# OR: Check in script if day-of-week matches
0 0 15 * *   [ "$(date +%u)" = "5" ] && /path/to/script.sh

2. Timezone: The Silent Shift

# Diagnose: What timezone is your cron daemon using?
date                          # shows system time and timezone
timedatectl | grep "Time zone" # systemd systems
cat /etc/timezone             # Debian/Ubuntu

# Fix option 1: Set timezone in the crontab file (GNU cron, cronie)
CRON_TZ=America/New_York
0 9 * * * /opt/scripts/daily_report.sh

# Fix option 2: Force UTC everywhere and account for offset
# If your users are in US Eastern (UTC-5), 9 AM ET = 14:00 UTC
0 14 * * * /opt/scripts/daily_report.sh

3. The PATH Problem

# Cron's default PATH is typically only:
# /usr/bin:/bin
# (Not /usr/local/bin, not ~/.local/bin, not nvm/pyenv shims)

# WRONG — node/python/pip may not be found
* * * * * node /opt/app/process.js

# RIGHT — use absolute paths (find them with: which node, which python3)
* * * * * /usr/local/bin/node /opt/app/process.js
* * * * * /usr/bin/python3 /opt/scripts/run.py

# OR — set PATH at the top of the crontab
PATH=/usr/local/bin:/usr/bin:/bin
* * * * * node /opt/app/process.js

4. Silent Failures (No Output = No Error Visibility)

# WRONG — all errors are swallowed
0 2 * * * /opt/scripts/backup.sh

# RIGHT — capture both stdout and stderr with timestamps
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# BETTER — add timestamps via ts (moreutils) or bash
0 2 * * * /opt/scripts/backup.sh 2>&1 | ts '%Y-%m-%d %H:%M:%S' >> /var/log/backup.log

# ALSO — configure cron to email output (cron default when MAILTO is set)
[email protected]

5. Overlapping Job Executions

# Problem: job runs every 5 minutes but takes 8 minutes
# Concurrent instances will overlap, double-process records, etc.

# FIX: flock — acquires a file lock; exits immediately if already locked
*/5 * * * * /usr/bin/flock -n /var/lock/myjob.lock /opt/scripts/myjob.sh

# flock flags:
# -n: non-blocking (don't wait, just exit)
# -w 10: wait up to 10 seconds for the lock

6. Daylight Saving Time

When clocks "spring forward," a job scheduled for 2:30 AM (in a DST-observing timezone) will not run — that time doesn't exist. When clocks "fall back," a job at 1:30 AM may run twice. Use UTC (which never observes DST) for any job that must run reliably regardless of clock changes.

7. Month End Edge Cases

# This will NOT run in Feb, Apr, Jun, Sep, Nov (fewer than 31 days)
0 0 31 * *   # Only runs in Jan, Mar, May, Jul, Aug, Oct, Dec

# This runs TWICE in months with 29/30/31 days (day 28 + day 29/30/31)
# because the intent "last day of month" is not directly expressible
0 0 28-31 * *  # Incorrect for "last day of month"

# Correct approach: shell script that checks the last day
0 0 * * * [ "$(date +%d)" = "$(date -d "$(date +%Y-%m-01) +1 month -1 day" +%d)" ] && /opt/scripts/month_end.sh

8. The Field Order Memory Trick

# Developers frequently swap minute and hour
0 9 * * *   # Correct: runs at 9:00 AM
9 0 * * *   # Wrong intent: runs at 00:09 (9 minutes past midnight)

# Mnemonic: "Minutes are smaller than hours"
# Or: think of a clock — minutes go 0-59, hours 0-23

# Validate EVERY expression in crontab.guru before deploying.
# Seeing next-run-times eliminates field-swap errors instantly.

Platform-Specific Cron Syntax Differences

PlatformFormatTimezone DefaultKey Differences
Linux cron (Vixie/cronie)5-fieldSystem TZCRON_TZ var supported; @reboot, @daily etc.
Kubernetes CronJob5-fieldUTCtimeZone field added in k8s 1.25; spec.schedule is standard cron
GitHub Actions5-fieldUTCMinimum interval: every 5 min. May be delayed by queue under load.
AWS EventBridge (legacy)6-field + cron() wrapperUTC onlyAdds year field; DOW is 1–7 (1=Sun); ? required in DOW or DOM
AWS EventBridge Scheduler5-field or rate/atConfigurable TZNewer service; supports IANA timezones; also supports rate(5 minutes)
Google Cloud Scheduler5-field (App Engine also 6)Configurable TZFull IANA timezone support; uses unix-cron format

AWS EventBridge: The 6-Field Format

AWS EventBridge Rules use a 6-field cron expression wrapped in cron(). The fields are: minute, hour, day-of-month, month, day-of-week, year. Day-of-week uses 1–7 (1=Sunday), not the Unix 0–6. When day-of-month is specified, day-of-week must be ?, and vice versa — you cannot use * for both.

# Linux crontab — every weekday at 9 AM
0 9 * * 1-5

# AWS EventBridge equivalent (note: 2-6 = Mon-Fri in 1=Sun indexing)
cron(0 9 ? * 2-6 *)

# AWS EventBridge — first of every month at midnight
cron(0 0 1 * ? *)

# AWS EventBridge — every 15 minutes
rate(15 minutes)   # alternatively, use rate() for simple intervals

Kubernetes CronJob: Timezone Support

# Kubernetes CronJob manifest (k8s 1.25+ with timezone field)
apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-backup
spec:
  schedule: "0 2 * * *"       # standard 5-field cron expression
  timeZone: "America/New_York" # IANA timezone (added in k8s 1.25)
  concurrencyPolicy: Forbid    # equivalent of flock — prevents overlap
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: backup
              image: postgres:16
              command: ["/scripts/backup.sh"]
          restartPolicy: OnFailure

Cron vs Systemd Timers: When to Graduate

Traditional cron is a fire-and-forget daemon. Systemd timers are first-class systemd units with full integration into journald logging, cgroup resource management, and dependency ordering. On any systemd-based server (Debian, Ubuntu, RHEL, CentOS, Fedora), systemd timers are worth considering for production-critical scheduled work.

FeatureCronSystemd Timers
LoggingFile redirect or MAILTOjournald (queryable, structured)
Missed run recoveryNo — missed runs are droppedOnBootSec / Persistent=true
Resource limitsNoneCPUQuota, MemoryMax, etc.
Dependency orderingNoneAfter=network.target etc.
Status / monitoringNone built-insystemctl status, journalctl
Setup complexityOne line in crontabTwo unit files (.service + .timer)

For automating cron-style jobs at scale, our guide on cron jobs tutorial covers real-world production patterns including systemd timer setup, monitoring hooks, and failure alerting.

Frequently Asked Questions

What is crontab guru?

Crontab guru is a browser-based visual cron expression editor created by Cronitor. It validates cron expressions and shows the next 5 scheduled run times in plain English. All processing is client-side. It is the de facto validation tool before deploying any scheduled job.

What is the crontab syntax?

A crontab entry has 5 fields followed by a command: minute (0–59), hour (0–23), day-of-month (1–31), month (1–12), day-of-week (0–6). Special characters include * (any), / (step), - (range), and , (list). Example: 0 9 * * 1-5 runs at 9 AM every weekday.

Why is my cron job running at the wrong time?

The most common cause is timezone mismatch. Cron runs in the system timezone; GitHub Actions and AWS EventBridge use UTC by default. A job for 9 AM on a UTC server runs at 4 AM US Eastern. Verify with date and use crontab.guru to confirm next run times.

What does */5 mean in a cron expression?

The slash is the step operator. */5 in the minute field means "every 5 minutes" (0, 5, 10 ... 55). */2 in the hour field means every 2 hours (0, 2, 4 ... 22). Non-zero starts work too: 10/15 means at :10, :25, :40, :55.

How does cron handle day-of-month and day-of-week together?

When both fields are non-*, cron uses OR logic. 0 0 15 * 5 runs on the 15th of every month AND every Friday — not only Fridays that fall on the 15th. Use * for the field you don't want to constrain.

What is the difference between cron and systemd timers?

Systemd timers offer superior logging (journald), missed-run recovery (OnBootSec), resource limits via cgroups, and dependency ordering. Traditional cron fails silently with no built-in logging. For production workloads where observability matters, systemd timers are the better choice on systemd-based Linux.

Does AWS EventBridge use the same cron syntax as Linux?

No. AWS EventBridge uses a 6-field format wrapped in cron(): minute, hour, day-of-month, month, day-of-week, year. Day-of-week is 1–7 (1=Sunday, not 0). The ? character is required in either day-of-month or day-of-week. Standard Linux crontab expressions will not work directly.

Validate Your Cron Expressions

Use BytePane's cron expression validator to check your schedule before deploying. See the next 10 run times in plain English and catch field-order mistakes, timezone issues, and the day-of-week AND/OR trap — all before a single job runs in production.

Open Cron Expression Validator

Related Articles