Crontab Guru

Translate cron schedule expressions into plain English.

minute   hour   day(month)   month   day(week)

Every minute

Cron expressions, from the five fields to production pitfalls

Cron is the scheduler that ships with practically every Unix-like system. Each line in a crontab pairs a schedule with a command, and the schedule is written as five fields separated by spaces: minute (0 to 59), hour (0 to 23), day of month (1 to 31), month (1 to 12 or JAN to DEC) and day of week (0 to 7, where both 0 and 7 mean Sunday, or SUN to SAT). The daemon wakes every minute, compares the current time with every schedule it knows, and runs whatever matches. The grammar is compact enough to write "9:00 on weekdays" as 0 9 * * 1-5 and cryptic enough that most people cannot read that back without help.

This page translates an expression into plain English as you type, using the cronstrue library inside your browser. It accepts the standard five-field form, the @daily style shortcuts, month and weekday names, and six-field Quartz-style expressions where the first field is seconds. An expression it cannot parse shows "Invalid cron expression". The three presets cover the schedules people ask for most: every five minutes, every midnight and weekday mornings. For a full walkthrough with more examples, read How Crontab Works.

Operators inside a field

SyntaxMeaningExample
*every value* * * * * runs every minute
5exactly this value30 2 * * * runs at 02:30 daily
1,15a list of values0 0 1,15 * * runs on the 1st and 15th
9-17an inclusive range0 9-17 * * * runs hourly from 09:00 to 17:00
*/15every Nth value from the start*/15 * * * * runs at :00, :15, :30 and :45
1-30/5a step inside a range1-30/5 * * * * runs at :01, :06, :11 and so on up to :26

Steps count from the first value of the range, not from an arbitrary offset. If you need "every 20 minutes starting at :10", write the list explicitly: 10,30,50 * * * *.

The day-of-month versus day-of-week trap

In Vixie cron (the implementation on Debian, Ubuntu and most other distributions) the two day fields are combined with OR whenever both are restricted. 0 0 15 * 1 therefore runs on the 15th of every month and on every Monday, not only on Mondays that fall on the 15th. The English translation above reads as if both conditions apply together, which matches how the expression looks but not how cron behaves, so treat that particular combination with care. Quartz-based schedulers (Java, Spring, Jenkins) avoid the ambiguity by requiring a ? in one of the two fields.

Timezones, IST and daylight saving

Cron uses the system timezone of the machine it runs on, and cloud images almost always ship set to UTC. A schedule of 0 9 * * * on such a server fires at 14:30 IST, not at nine in the morning. Either convert the time yourself (IST is UTC+5:30, so 03:30 UTC is 09:00 IST) or set the timezone explicitly: cronie honours a CRON_TZ=Asia/Kolkata line at the top of the crontab, Kubernetes CronJobs accept a timeZone field, and GitHub Actions schedules run in UTC only. India has no daylight saving, but servers set to a zone that does can skip or repeat an hour twice a year, which is one more reason to keep servers on UTC and do the conversion in your head.

Why the job works in a shell and fails in cron

  • Minimal environment. Cron does not load your profile. PATH is typically just /usr/bin:/bin, so call programs by absolute path or set PATH= at the top of the crontab.
  • Shell. Commands run under /bin/sh, not bash, unless you set SHELL=/bin/bash. Bash-only syntax fails silently.
  • Percent signs. A bare % in the command is turned into a newline. Escape it as \%, which matters for every date +\%F in a backup script.
  • Output. Anything written to stdout or stderr is emailed to the user if a mail system exists and otherwise discarded. Redirect it: >> /var/log/myjob.log 2>&1.
  • Overlap. Cron happily starts a second copy while the first is still running. Wrap long jobs with flock -n /tmp/myjob.lock /path/to/script so the second run exits at once.
  • Missed runs. If the machine was off at the scheduled minute, plain cron does not catch up. Use anacron or a systemd timer with Persistent=true for laptops and intermittently running hosts.

Working with the crontab

crontab -e                 # edit your own crontab
crontab -l                 # list it
sudo crontab -u deploy -l  # list another user's crontab
sudo crontab -r            # WARNING: deletes the crontab with no confirmation

# Example crontab
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=ops@example.com
# min hour dom mon dow  command
0    2    *   *   *    /opt/backup/run.sh >> /var/log/backup.log 2>&1
*/5  *    *   *   *    flock -n /tmp/sync.lock /opt/sync/pull.sh
0    0    28-31 * *    [ "$(date +\%d -d tomorrow)" = "01" ] && /opt/reports/month-end.sh

Files in /etc/cron.d/ and the system-wide /etc/crontab use a sixth column for the user the command runs as, placed between the schedule and the command. Copying a line from a personal crontab into /etc/cron.d/ without adding that user field is a classic way to get a job that never runs. The last example above is the standard trick for "last day of the month", which cron cannot express directly.

Shortcuts

@reboot runs once after boot, and @hourly, @daily (or @midnight), @weekly, @monthly and @yearly (or @annually) map to the obvious five-field forms. They are easier to read in a crontab, but the five-field version is more portable to other schedulers.

Other schedulers that speak cron

Kubernetes CronJob objects take the same five fields plus concurrencyPolicy and startingDeadlineSeconds to control overlap and late starts. GitHub Actions uses on: schedule: cron: in UTC, and its documentation warns that runs may be delayed when the platform is busy, so do not use it for anything time-critical. Apache Airflow accepts cron strings and presets, but triggers a run at the end of each interval rather than the start, which surprises almost everyone the first time. systemd timers use a different OnCalendar= syntax and give you systemctl list-timers plus journal logging for free, which makes them a good replacement on modern Linux hosts.

How to use

  1. Type or paste a cron expression into the box; the fields are minute, hour, day of month, month and day of week.
  2. Read the plain-English translation that appears below it. An unparseable expression shows "Invalid cron expression".
  3. Use the presets (every 5 minutes, every midnight, weekdays at 9 AM) as starting points and edit the fields.
  4. Remember that the translation is timezone-neutral: the job fires in the timezone of the server or scheduler, which is usually UTC on cloud machines.
  5. Copy the expression into crontab -e, a Kubernetes CronJob, a GitHub Actions workflow or any other cron-compatible scheduler.

Common use cases

  • Decoding an inherited crontab line before touching it during an on-call incident.
  • Scheduling a nightly database dump at a quiet hour, expressed in UTC for an Indian audience.
  • Writing a Kubernetes CronJob that cleans up temporary files every six hours.
  • Building a GitHub Actions workflow that rebuilds a static site every morning.
  • Checking that a "twice a month" report schedule really lands on the 1st and the 15th.
  • Reviewing a colleague's pull request that changes a cron string, without running it.

Frequently asked questions

Does this tool support six-field expressions with seconds?

Yes. If you enter six fields, the first one is treated as seconds in the Quartz style used by Java, Spring and Jenkins schedulers, and the question-mark placeholder is accepted. Classic Unix cron itself only understands five fields, so keep to five for a crontab file.

What does */15 * * * * mean?

Every 15 minutes, counted from the start of the hour: the job runs at :00, :15, :30 and :45. Steps always count from the first value in the field or range, so */20 gives :00, :20 and :40 and there is no way to start at :10 without listing the minutes explicitly.

How do day-of-month and day-of-week interact?

In standard Unix cron, when both fields are restricted (neither is *), the job runs when either one matches. 0 0 15 * 1 runs on every 15th and on every Monday. Quartz-based schedulers require a ? in one of the two fields to avoid this ambiguity.

My job runs at the wrong time. Why?

Almost always the timezone. Cron uses the server clock, and cloud servers normally run in UTC, which is 5 hours 30 minutes behind IST. Either write the schedule in UTC (09:00 IST is 03:30 UTC) or set CRON_TZ=Asia/Kolkata where your cron implementation supports it.

How do I run something on the last day of the month?

Cron has no last-day token. Schedule the job for days 28 to 31 and let a shell test decide: 0 0 28-31 * * [ "$(date +\%d -d tomorrow)" = "01" ] && /path/to/job. The percent sign must be escaped inside a crontab because a bare % is converted to a newline.

Why does the script work from my terminal but not from cron?

Cron runs with a minimal environment: a short PATH, /bin/sh instead of bash, no profile and no working directory you chose. Use absolute paths, set PATH and SHELL at the top of the crontab, and redirect output to a log file so you can see the actual error.

How do I stop overlapping runs of a slow job?

Cron does not check whether the previous run finished. Prefix the command with flock -n /tmp/jobname.lock so a second instance exits immediately while the first holds the lock, or move the job to a Kubernetes CronJob with concurrencyPolicy: Forbid.

What is the difference between @daily and 0 0 * * *?

Nothing in behaviour: both run at midnight in the server timezone. The shortcut is easier to read in a crontab, while the five-field form is understood by more schedulers, so use the shortcut locally and the explicit form in anything you share.

Advertisement