Skip to content

Cron Syntax Deep Dive

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Cron Syntax Deep Dive. We cover key concepts, practical examples, and best practices to help you master this topic.

Master cron syntax with advanced patterns: ranges, steps, lists, non-standard macros, month names, day names, and complex multi-field expressions for precise scheduling.

What You Learn

You will learn advanced cron syntax including complex ranges, step values with offsets, combining multiple operators in one field, named month and day abbreviations, and non-standard macros like @hourly.

Why It Matters

Basic cron syntax handles simple schedules, but real-world scheduling requires precision: every 90 minutes, every weekday at 8:15 AM except holidays, the last day of every month. Advanced syntax makes these possible.

Real-World Use

DodaTech uses complex cron: threat signature updates every 3 hours offset by 15 minutes (15 */3 * * *), report generation on the last weekday of each month (0 9 * * 1-5 with external script check), and staggered health checks every 7 minutes starting at minute 2 (2-59/7 * * * *).

Field Ranges and Steps

# Range: dash between values
# Run every hour during business hours (9 AM to 5 PM)
0 9-17 * * * /opt/scripts/business.py

# Step: slash after range or asterisk
# Every 15 minutes
*/15 * * * * /opt/scripts/quarter_hour.py

# Every 2 hours from 8 AM to 6 PM
0 8-18/2 * * * /opt/scripts/bi_hourly.sh

# Every 10 minutes between 9 AM and 5 PM
*/10 9-17 * * * /opt/scripts/ten_min.py

Combining Operators

# Comma-separated list of values
# Run at 8 AM, 12 PM, and 5 PM
0 8,12,17 * * * /opt/scripts/meals.sh

# Mixed: range and individual values
# Run every hour from 9 AM to 12 PM, and also at 5 PM
0 9-12,17 * * * /opt/scripts/mixed.py

# Complex: multiple lists
# Run on the 1st and 15th of Jan, Jun, Dec
0 0 1,15 1,6,12 * /opt/scripts/quarterly.sh

Step Values with Offset

# Step with offset: start at N, then every M
# Every 7 minutes starting at minute 2 (2, 9, 16, 23, ...)
2-59/7 * * * * /opt/scripts/staggered.sh

# Every 90 minutes (0:00, 1:30, 3:00, 4:30, ...)
# This requires two entries:
0 0-22/3 * * * /opt/scripts/90min.sh
30 1-23/3 * * * /opt/scripts/90min.sh

# Every 45 minutes (3 entries needed)
0 0-23/3 * * * /opt/scripts/45min.sh
45 0-23/3 * * * /opt/scripts/45min.sh
30 1-23/3 * * * /opt/scripts/45min.sh

Named Months and Days

# Cron accepts three-letter abbreviations for months and days
# Run at 9 AM on Monday through Friday
0 9 * * mon-fri /opt/scripts/weekdays.py

# Run on the first day of January, July, and December
0 0 1 jan,jul,dec * /opt/scripts/milestones.sh

# Run every Sunday at midnight
0 0 * * sun /opt/scripts/weekly_summary.py

# Avoid using named days/months in scripts for portability
# Numeric equivalents:
# sun=0, mon=1, tue=2, wed=3, thu=4, fri=5, sat=6
# jan=1, feb=2, ..., dec=12

Non-Standard Macros

# Some cron implementations support these shortcuts:

# @reboot: Run once at startup
@reboot /opt/scripts/startup.sh

# @yearly or @annually: Run once a year (0 0 1 1 *)
@yearly /opt/scripts/yearly_renewal.sh

# @monthly: Run once a month (0 0 1 * *)
@monthly /opt/scripts/monthly_cleanup.py

# @weekly: Run once a week (0 0 * * 0)
@weekly /opt/scripts/weekly_report.sh

# @daily or @midnight: Run once a day (0 0 * * *)
@daily /opt/scripts/daily_backup.sh

# @hourly: Run once an hour (0 * * * *)
@hourly /opt/scripts/hourly_health.py

Complex Real-World Examples

# First weekday of each month at 9 AM
# Cron cannot do "first weekday" directly; use script logic
0 9 1-7 * * /opt/scripts/first_weekday.sh
# Inside first_weekday.sh:
# if [ $(date +\%u) -le 5 ]; then ...; fi

# Last day of each month at 11:59 PM
59 23 28-31 * * /opt/scripts/last_day.sh
# Inside last_day.sh:
# if [ "$(date +\%d -d tomorrow)" = "01" ]; then ...; fi

# Every 30 seconds (cron minimum is 1 minute, use sleep)
* * * * * /opt/scripts/every_30s.sh
# Inside every_30s.sh:
# do_work; sleep 30; do_work

# Every 6 hours at minutes 0, 15, 30, 45
0,15,30,45 */6 * * * /opt/scripts/details.py

Validating Cron Syntax

#!/usr/bin/env python3
import sys
import re

def validate_cron(expression):
    fields = expression.strip().split()
    if len(fields) != 5:
        return False, f"Expected 5 fields, got {len(fields)}"

    patterns = [
        (r'^(\*|\d+|\d+-\d+|\*/\d+|\d+-\d+/\d+)((,\d+|\d+-\d+|\*/\d+)*)$', 0, 59),
        (r'^(\*|\d+|\d+-\d+|\*/\d+|\d+-\d+/\d+)((,\d+|\d+-\d+|\*/\d+)*)$', 0, 23),
        (r'^(\*|\d+|\d+-\d+|\*/\d+|\d+-\d+/\d+)((,\d+|\d+-\d+|\*/\d+)*)$', 1, 31),
        (r'^(\*|\d+|\d+-\d+|\*/\d+|\d+-\d+/\d+)((,\d+|\d+-\d+|\*/\d+)*)$', 1, 12),
        (r'^(\*|\d+|\d+-\d+|\*/\d+|\d+-\d+/\d+)((,\d+|\d+-\d+|\*/\d+)*)$', 0, 7),
    ]

    for i, (field, pattern, min_val, max_val) in enumerate(zip(fields, patterns)):
        field_pattern, vmin, vmax = patterns[i]
        if not re.match(field_pattern, field):
            return False, f"Field {i+1} syntax invalid: {field}"

    return True, "Valid cron expression"

if __name__ == '__main__':
    expr = sys.argv[1] if len(sys.argv) > 1 else "*/15 * * * *"
    valid, msg = validate_cron(expr)
    print(f"{expr}: {msg}")

Expected output:

$ python3 validate_cron.py "*/15 9-17 * * 1-5"
*/15 9-17 * * 1-5: Valid cron expression

$ python3 validate_cron.py "0 25 * * *"
0 25 * * *: Field 2 syntax invalid: 25

Common Mistakes

1. Off-by-One in Day-Of-Week

Sunday is both 0 and 7. Monday is 1, Tuesday is 2, etc. Using 7 instead of 0 for Sunday causes unexpected behavior.

2. Overlapping Ranges

A range like 0 9-5 * * * is invalid because 9 > 5. Cron cannot reverse ranges. Use 0 9-23,0-5 * * * or two entries.

3. Month and Day-of-Week AND Logic

When both day-of-month and day-of-week are specified (not *), the job runs when EITHER matches. This is often unexpected.

4. Forgetting Percent Signs

The % character has special meaning in cron. It must be escaped with \% in commands.

5. Step Value Confusion

*/15 means every 15 units, but 0-30/15 means every 15 within the range 0-30, giving 0 and 30 only.

Practice Questions

1. What does 2-59/7 * * * * mean?

At minutes 2, 9, 16, 23, 30, 37, 44, and 51 of every hour.

2. How do you schedule a job every 90 minutes?

Two entries: 0 0-22/3 * * * and 30 1-23/3 * * *.

3. What is the difference between 0 0 * * 0 and 0 0 * * 7?

Both run at midnight on Sunday. Sunday is represented by both 0 and 7.

4. How do you run a job on the last day of every month?

Use 59 23 28-31 * * with a script that checks if tomorrow is the 1st.

Challenge

Write cron expressions for: every 7 minutes starting at minute 3, every 2 hours during business hours (8 AM to 6 PM), every Monday and Wednesday at 8:30 AM, the first day of each quarter (Jan 1, Apr 1, Jul 1, Oct 1), and every 45 minutes.

FAQ

Can I use both day-of-month and day-of-week?

Yes, but the job runs when EITHER matches (OR logic). If you need AND logic, use a wrapper script that checks both conditions.

What is the maximum step value?

The maximum step is the field's maximum value. For minutes, max step is 59. For hours, max step is 23. Larger steps are effectively the same as the field maximum.

Are cron macros like @daily portable?

Not all cron implementations support @reboot, @daily, etc. Some use run-parts. For portability, expand macros to their five-field equivalents.

How do I schedule a job for specific dates like holidays?

Cron cannot handle dynamic dates. Use a wrapper script that checks a holiday calendar file and exits early on holidays.

What is the Vixie cron syntax?

Vixie cron is the most common implementation. It supports standard five-field syntax, environment variable setting, and %Y/%m/%d in commands (escaped as \%).

Mini Project: Complex Scheduler

#!/usr/bin/env python3
import sys
from datetime import datetime

def parse_cron_expression(expr):
    """Parse and describe a cron expression in plain English."""
    fields = expr.split()
    if len(fields) != 5:
        return "Invalid: requires 5 fields"

    descriptions = {
        'minute': ('minute', 0, 59),
        'hour': ('hour', 0, 23),
        'day': ('day of month', 1, 31),
        'month': ('month', 1, 12),
        'weekday': ('day of week', 0, 7),
    }

    result = []
    for i, (field, (name, lo, hi)) in enumerate(zip(fields, descriptions.values())):
        if field == '*':
            result.append(f"every {name}")
        elif field.startswith('*/'):
            result.append(f"every {field[2:]} {name}s")
        elif '-' in field and '/' in field:
            rng, step = field.split('/')
            result.append(f"every {step} {name}s from {rng}")
        elif '-' in field:
            result.append(f"{name}s {field}")
        elif ',' in field:
            result.append(f"{name}s {field}")
        else:
            result.append(f"{name} {field}")

    return ', '.join(result)

if __name__ == '__main__':
    examples = [
        "*/15 9-17 * * 1-5",
        "0 8,12,17 * * *",
        "2-59/7 * * * *",
        "0 0 1 1,4,7,10 *",
        "30 8 * * 1-5",
    ]
    for ex in examples:
        print(f"{ex:20s} -> {parse_cron_expression(ex)}")

Expected output:

*/15 9-17 * * 1-5   -> every 15 minutes, hours 9-17, every day of month, every month, days of week 1-5
0 8,12,17 * * *     -> minute 0, hours 8,12,17, every day of month, every month, every day of week
2-59/7 * * * *      -> every 7 minutes from 2-59, every hour, every day of month, every month, every day of week
0 0 1 1,4,7,10 *    -> minute 0, hour 0, day of month 1, months 1,4,7,10, every day of week
30 8 * * 1-5        -> minute 30, hour 8, every day of month, every month, days of week 1-5

What's Next

Now that you understand advanced cron syntax, explore special cron strings like @reboot and @daily, then learn about crontab file management.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro