Guide
DevOps
Automation

Cron Jobs and Scheduling Basics: How Unix Task Scheduling Works

Cron is the Unix standard for scheduled tasks. A cron expression is five space-separated fields — minute, hour, day-of-month, month, day-of-week — that together define when a command runs. One wrong field triggers jobs at the wrong time, silently, in production. Understanding the syntax precisely is a core DevOps skill.

TL;DR
  • Five fields, left to right: minute (0–59), hour (0–23), day of month (1–31), month (1–12), day of week (0–7). A * means "any value"; */n means "every nth"; a-b is a range; a comma-separated list selects specific values.
  • Special strings like @daily, @hourly, and @reboot are readable aliases for common schedules supported by most modern cron implementations.
  • Day-of-month and day-of-week use OR logic. If both are non-wildcard, cron fires when either condition is true — not both. This is the most common source of unexpected scheduling.
  • Cron runs in server local time. A job at 0 9 * * * fires at 9 AM in whatever timezone the server is configured with — UTC, US/Eastern, or anything else. Servers that observe daylight saving time can cause jobs to skip or double-fire when the clocks change.
  • Always preview the next five run times before deploying a new schedule. The Cron Expression Validator shows the plain-English description and the next five run dates so you can catch mistakes before they reach production.
  • Cron is available beyond Linux. GitHub Actions uses cron syntax in on: schedule: triggers. Kubernetes uses it in CronJob manifests. AWS EventBridge and Google Cloud Scheduler both support the same five-field format.

Validate your schedule instantly with the Cron Expression Validator →

What Is Cron and How Does It Work?

Cron is a daemon (background process) that runs on Unix and Linux systems. It wakes up every minute, reads a set of schedule definitions called crontabs(cron tables), and executes any command whose schedule matches the current time. The cron daemon itself requires no configuration beyond being running — all scheduling logic lives in the crontab files.

Each user on a system can have their own crontab, edited with crontab -e. System-wide cron jobs are typically stored in /etc/cron.d/, /etc/cron.daily/, /etc/cron.hourly/, and related directories. On modern systems, systemd timers are an alternative to cron for system-level scheduling — they support more fine-grained control and better logging, but cron syntax remains dominant in DevOps tooling.

The Cron Daemon

Wakes every minute. Reads crontab files. Executes commands whose schedule matches the current minute, hour, day, month, and weekday.

Commands run as the user who owns the crontab entry.

Crontab Files

One line per job: a cron expression followed by the command to run. Comments start with #. The MAILTO variable controls email output from jobs.

Edit with crontab -e. List with crontab -l.

The Schedule

Five fields define exactly when a job runs. The granularity is one minute — cron cannot schedule sub-minute jobs. For finer intervals, use a loop inside the command.

Cron fires at most once per minute, even if expressions overlap.

Anatomy of a Cron Expression

A standard cron expression is exactly five space-separated fields. The fields are evaluated left to right:

Cron expression structure

  ┌─── minute        (0 – 59)
  │  ┌── hour          (0 – 23)
  │  │  ┌─ day of month (1 – 31)
  │  │  │  ┌ month      (1 – 12 or JAN–DEC)
  │  │  │  │  ┌ day of week (0 – 7, 0 and 7 = Sunday, or SUN–SAT)
  │  │  │  │  │
  *  *  *  *  *   command
FieldPositionAllowed valuesNotes
Minute1st0 – 59The minute within the hour when the job runs
Hour2nd0 – 2324-hour clock. 0 = midnight, 12 = noon, 23 = 11 PM
Day of month3rd1 – 311-indexed. Values above the month's actual length are skipped
Month4th1 – 12 or JAN–DEC1-indexed. JAN=1, DEC=12. Names are case-insensitive
Day of week5th0 – 7 or SUN–SAT0 and 7 both mean Sunday. SUN=0 or 7, MON=1, SAT=6

Field Value Syntax

SyntaxMeaningExampleMeans
*Any value — every valid value for this field* in hour fieldEvery hour
*/nEvery nth value — step from the minimum*/15 in minute fieldAt minutes :00, :15, :30, :45
a-bRange — all values from a to b inclusiveMON-FRI in day-of-weekMonday through Friday
a,b,cList — specific values only1,15 in day-of-monthOn the 1st and 15th of the month
a-b/nStep within a range8-18/2 in hour fieldEvery 2 hours from 8 AM to 6 PM: 8, 10, 12, 14, 16, 18

Special Strings (Cron Aliases)

Most modern cron implementations (Vixie cron, cronie, systemd timers, GitHub Actions, Kubernetes) support special string aliases that replace the five-field expression with a human-readable name. These are more self-documenting and less error-prone for common schedules.

StringEquivalentMeaning
@yearly / @annually0 0 1 1 *Once a year — midnight on January 1st
@monthly0 0 1 * *Once a month — midnight on the 1st of each month
@weekly0 0 * * 0Once a week — midnight on Sunday
@daily / @midnight0 0 * * *Once a day — midnight
@hourly0 * * * *Once an hour — at the top of the hour
@reboot(at startup)Once, when the cron daemon starts. Useful for startup scripts.

Note: @reboot is not supported everywhere — check your cron implementation before relying on it in production. GitHub Actions does not support @reboot.

Common Cron Patterns Reference

These patterns cover the majority of real-world scheduling use cases. Use the Cron Expression Validator to confirm the description and next run times for any expression you use.

ExpressionSchedule
* * * * *Every minute
*/5 * * * *Every 5 minutes
*/15 * * * *Every 15 minutes
0 * * * *Every hour (at :00)
0 9 * * *Every day at 9:00 AM
0 9 * * MON-FRIEvery weekday (Monday–Friday) at 9:00 AM
0 9 * * 1-5Same as above using numeric day values
0 0 * * *Every day at midnight
0 0 1 * *At midnight on the 1st of every month
0 0 1 1 *At midnight on January 1st every year
30 18 * * FRIEvery Friday at 6:30 PM
0 2 * * 0Every Sunday at 2:00 AM
0 6,18 * * *Every day at 6:00 AM and 6:00 PM
0 8-17 * * MON-FRIEvery hour between 8 AM and 5 PM on weekdays
0 0 1,15 * *At midnight on the 1st and 15th of each month
*/30 9-17 * * MON-FRIEvery 30 minutes during business hours (9–5) on weekdays

Cron in Different Environments

Linux Cron Daemon

The classic cron daemon (Vixie cron, cronie, or fcron on most Linux systems) reads crontab files from /var/spool/cron/crontabs/ (user crontabs) and /etc/cron.d/ (system crontabs). Commands run in a minimal shell environment — thePATH is not the same as an interactive login session, so absolute paths are recommended.

Example crontab entries

# Run database backup every day at 2 AM
0 2 * * * /usr/local/bin/backup-db.sh >> /var/log/backup.log 2>&1

# Clear temp files every Sunday at midnight
0 0 * * 0 find /tmp -type f -mtime +7 -delete

# Send weekly report every Monday at 9 AM
0 9 * * MON /usr/local/bin/weekly-report.py

GitHub Actions Schedule Trigger

GitHub Actions supports the schedule event, which uses standard cron syntax. The schedule always runs in UTC. GitHub's minimum interval is 5 minutes (* * * * * is valid syntax but GitHub throttles it). Scheduled workflows may be delayed by up to 15–30 minutes during high-load periods.

GitHub Actions — .github/workflows/nightly.yml

on:
  schedule:
    - cron: '0 2 * * *'   # Every day at 2:00 AM UTC
  push:
    branches: [main]

jobs:
  nightly-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: npm test

Kubernetes CronJob

The Kubernetes CronJob resource creates Job objects on a cron schedule. The timezone defaults to the cluster's local timezone (often UTC). As of Kubernetes 1.27, an explicit timeZone field is available (stable in 1.29). TheconcurrencyPolicy field controls what happens when a scheduled run overlaps with a still-running previous job (Allow, Forbid, or Replace).

Kubernetes CronJob manifest

apiVersion: batch/v1
kind: CronJob
metadata:
  name: database-backup
spec:
  schedule: "0 2 * * *"
  timeZone: "UTC"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: backup
              image: myapp/backup:latest
          restartPolicy: OnFailure

Validate YAML syntax: YAML Validator — ensures your CronJob manifest has no indentation errors before applying with kubectl apply.

Cloud Schedulers

Cloud-native schedulers use the same cron syntax with explicit timezone support:

  • AWS EventBridge (CloudWatch Events) — supports cron expressions with a sixth field for year (optional), e.g. cron(0 2 * * ? *). Note: day-of-month and day-of-week are mutually exclusive in AWS cron — use ? for the unused field.
  • Google Cloud Scheduler — uses standard five-field Unix cron with full timezone support via IANA timezone strings (e.g. America/New_York).
  • Azure Logic Apps / Functions — uses standard cron with an optional sixth field for seconds.

Common Cron Mistakes

Day-of-month and day-of-week are combined with OR, not AND

When both the day-of-month and day-of-week fields are non-wildcard, most cron implementations fire if either condition is true. "0 9 1 * MON" runs at 9 AM on the 1st of every month AND at 9 AM every Monday — not just Mondays that fall on the 1st. To target only a specific weekday-of-month combination, you need application-level logic, not cron alone.

Off-by-one in month or day-of-week numbering

Month is 1-indexed (1 = January, 12 = December). Day of week is 0-indexed in most cron implementations (0 = Sunday), but 7 is also Sunday. Using "0 9 * 0 *" to mean "January" is wrong — that sets month to 0, which is typically invalid. Using "0 9 * * 7" for Sunday works on most systems but is non-standard.

Timezone mismatch between cron and application logic

Cron runs in the server's local timezone unless explicitly configured otherwise. If the server runs UTC but the intended schedule is 9 AM Eastern (UTC-5 or UTC-4 depending on DST), the expression "0 9 * * *" will fire at 4 AM Eastern. Set CRON_TZ or TZ in the crontab, or use an explicit UTC offset in your expression.

Daylight Saving Time can cause jobs to double-fire or skip

On servers that observe DST, when clocks "spring forward" by 1 hour, any job scheduled in the skipped hour will not run that day. When clocks "fall back", any job scheduled in the repeated hour runs twice. This is particularly problematic for daily backup jobs, billing runs, and report generation. Use UTC for all scheduled jobs to avoid DST surprises entirely.

Step syntax "*/n" counts from the field minimum, not from midnight

"*/4" in the hour field means hours 0, 4, 8, 12, 16, 20 — it starts from 0 (midnight), not from the current time or from 1. "1-23/4" would give hours 1, 5, 9, 13, 17, 21. Use ranges with steps when you need a specific starting point.

Not redirecting cron output leads to silent failures

By default, cron emails stdout and stderr output to the crontab owner. On servers without a local mail transfer agent, this output is silently discarded. If a job fails, you may never know. Always redirect output explicitly: "command >> /var/log/myjob.log 2>&1" or configure logging within the command itself.

Assuming the cron environment matches the interactive shell

Cron runs commands with a minimal PATH (usually /usr/bin:/bin). Commands that work in your interactive shell may fail in cron because the binary is in /usr/local/bin or a user-specific bin directory. Use absolute paths (/usr/local/bin/node instead of node), or set PATH explicitly at the top of your crontab.

Try the Tools

All tools run entirely in your browser. No configuration data is uploaded. Browse all DevOps tools →

Frequently Asked Questions

Related Reading