Cron Expression Examples You Can Copy and Test Locally
Copy-paste cron expression examples for every 5 minutes, weekdays and month ends, plus the gotchas and a Python matcher to test any schedule.
A cron expression is five fields: minute, hour, day of month, month, day of week. */5 * * * * runs every 5 minutes, 0 9 * * 1-5 runs at 9:00 on weekdays, and 0 0 1 * * runs at midnight on the first of each month. Everything below uses that standard five-field format, as documented in the crontab(5) manual page.
The examples are grouped by what you're trying to do. Further down is a small matcher you can run to see when any expression fires, and a list of the mistakes that bite people most often. Other schedulers (cloud services, Java libraries, CRMs) have their own variations on this syntax, so check their docs before pasting an expression across.
The five fields at a glance#
┌──────── minute 0-59
│ ┌────── hour 0-23
│ │ ┌──── day of month 1-31
│ │ │ ┌── month 1-12 (or jan-dec)
│ │ │ │ ┌ day of week 0-7 (0 or 7 is Sunday, or sun-sat)
* * * * * command
Four pieces of syntax cover almost everything:
*means "first to last" for that field.a-bis an inclusive range.8-11in the hours field means 8, 9, 10 and 11.a,b,cis a list. Lists can contain ranges, like0-4,8-12./nis a step. It works after a range (0-23/2) or after an asterisk (*/2).
Month and weekday names are allowed (first three letters, case doesn't matter), including ranges and lists: mon,wed,fri, jan-mar.
Cron expression examples: every N minutes and hours#
The most common search is "every 5 minutes", so here it is, with its relatives.
| Expression | Runs |
|---|---|
* * * * * |
Every minute |
*/5 * * * * |
Every 5 minutes |
*/15 * * * * |
Every 15 minutes (0, 15, 30, 45) |
0,30 * * * * |
Twice an hour, on the hour and half hour |
0 * * * * |
Every hour, on the hour |
0 */2 * * * |
Every two hours (0:00, 2:00, 4:00 ...) |
23 0-23/2 * * * |
23 minutes past every second hour, starting at midnight |
5 0 * * * |
00:05 every day |
A complete crontab line adds the command after the fifth field:
*/5 * * * * $HOME/bin/check.sh >> $HOME/tmp/check.log 2>&1
Redirecting output matters. Unless you set MAILTO="", cron mails whatever the job prints to the crontab's owner.
Weekdays, weekends and specific days#
| Expression | Runs |
|---|---|
0 9 * * 1-5 |
09:00 Monday to Friday |
0 22 * * mon-fri |
22:00 on weekdays, using names |
0 10 * * 6,0 |
10:00 on Saturday and Sunday |
0 10 * * sat,sun |
Same, using names |
5 4 * * sun |
04:05 every Sunday |
0 8 * * mon,wed,fri |
08:00 on Monday, Wednesday, Friday |
0 0 * * 0 |
Midnight each Sunday (same as @weekly) |
Sunday can be written as 0 or 7. I'd use names (sun, mon-fri) in anything a teammate will read later, since 1-5 makes people count on their fingers.
Monthly, yearly and month-end schedules#
| Expression | Runs |
|---|---|
15 14 1 * * |
14:15 on the first of every month |
0 0 1,15 * * |
Midnight on the 1st and 15th |
0 0 1 */3 * |
Midnight on the 1st, every third month starting in January |
0 0 1 jan-mar * |
Midnight on the 1st of January, February and March |
0 0 1 1 * |
Midnight on January 1st (same as @yearly) |
There are also nicknames that replace all five fields: @reboot (once after reboot), @yearly and @annually (0 0 1 1 *), @monthly (0 0 1 * *), @weekly (0 0 * * 0), @daily (0 0 * * *) and @hourly (0 * * * *).
Last day of the month is the one people ask for that standard cron can't express directly. The day-of-month field has no "last" value. A common workaround is to schedule the job for days 28 to 31 with 0 0 28-31 * *, then have the script itself check whether tomorrow is the first and exit if it isn't. The matcher below shows why you need that guard: the expression fires on four days in a 31-day month.
Timezone, environment and the percent sign#
The CRON_TZ variable sets the time zone for a crontab. You write times in that zone, while the daemon's log file uses the local zone of the machine. The manual's own example is:
CRON_TZ=Japan
5 0 * * * $HOME/bin/daily.job >> $HOME/tmp/out 2>&1
A few other details that explain odd behavior:
- Cron runs commands with
/bin/shunless you setSHELLin the crontab.HOMEcan also be overridden;LOGNAMEcan't. - Environment lines look like
name = value. Comments must be on their own line. A#after a command is treated as part of the command. - A bare
%in the command is turned into a newline, and everything after the first one is sent to the command as standard input. Escape it with a backslash:date +\%s. - System crontabs (
/etc/crontaband files in/etc/cron.d/) need a username between the schedule and the command:
MAILTO=root
* * * * * root touch /tmp/file
Test any schedule with a runnable matcher#
This script parses a five-field expression (or a nickname) and prints the next few run times from a fixed start date. It follows the rules from the manual: ranges, lists, steps, names, Sunday as 0 or 7, and the either-or rule for the two day fields. The start date is fixed so the output is reproducible. Edit the list and try your own expressions.
from datetime import datetime, timedelta
MONTHS = "jan feb mar apr may jun jul aug sep oct nov dec".split()
DAYS = "sun mon tue wed thu fri sat".split()
NICK = {"@yearly": "0 0 1 1 *", "@annually": "0 0 1 1 *",
"@monthly": "0 0 1 * *", "@weekly": "0 0 * * 0",
"@daily": "0 0 * * *", "@hourly": "0 * * * *"}
def value(tok, names, base):
tok = tok.lower()
return names.index(tok) + base if tok in names else int(tok)
def field(text, lo, hi, names=(), base=0):
out = set()
for part in text.split(","):
rng, _, step = part.partition("/")
stepped = bool(step)
step = int(step) if step else 1
if rng == "*":
a, b = lo, hi
elif "-" in rng:
x, y = rng.split("-")
a, b = value(x, names, base), value(y, names, base)
else:
a = value(rng, names, base)
b = hi if stepped else a
if a > b or a < lo or b > hi:
raise ValueError("bad field: " + text)
out.update(range(a, b + 1, step))
return out
def parse(expr):
parts = NICK.get(expr, expr).split()
if len(parts) != 5:
raise ValueError("need 5 fields: " + expr)
return {
"min": field(parts[0], 0, 59),
"hour": field(parts[1], 0, 23),
"dom": field(parts[2], 1, 31),
"mon": field(parts[3], 1, 12, MONTHS, 1),
"dow": {v % 7 for v in field(parts[4], 0, 7, DAYS, 0)},
"dom_star": parts[2].startswith("*"),
"dow_star": parts[4].startswith("*"),
}
def day_ok(p, day):
dom_ok = day.day in p["dom"]
dow_ok = day.isoweekday() % 7 in p["dow"]
if p["dom_star"] or p["dow_star"]:
return dom_ok and dow_ok
return dom_ok or dow_ok # both restricted: either may match
def runs(expr, start, count=5):
p = parse(expr)
day = start.replace(hour=0, minute=0, second=0, microsecond=0)
found = []
for _ in range(800):
if day.month in p["mon"] and day_ok(p, day):
for h in sorted(p["hour"]):
for m in sorted(p["min"]):
t = day.replace(hour=h, minute=m)
if t > start:
found.append(t)
if len(found) == count:
return found
day += timedelta(days=1)
return found
start = datetime(2026, 10, 14, 12, 0)
print("start:", start.strftime("%a %Y-%m-%d %H:%M"))
for expr in ["*/5 * * * *", "*/35 * * * *", "0 9 * * 1-5",
"0 0 28-31 * *", "30 4 1,15 * 5", "@monthly", "0 0 31 2 *"]:
times = runs(expr, start)
shown = ", ".join(t.strftime("%a %m-%d %H:%M") for t in times)
print(expr.ljust(15), shown or "never")
The expressions in the list are chosen to show the gotchas in the next section. Compare what */35 and 30 4 1,15 * 5 actually produce with what you'd guess from reading them.
Common mistakes and gotchas#
Steps restart inside each field. */35 in the minute field doesn't mean every 35 minutes. It means minutes 0 and 35 of every hour, so the gap alternates between 35 and 25 minutes. Likewise */23 in the hours field fires at hour 0 and hour 23 of each day. For true intervals, the manual suggests running every minute and testing the clock in the command:
* * * * * if [ $(expr \( $(date +%s) / 60 \) % 58) = 0 ]; then echo this runs every 58 minutes; fi
Two restricted day fields mean OR, not AND. If neither the day-of-month nor day-of-week field is *, the job runs when either matches. 30 4 1,15 * 5 runs at 04:30 on the 1st and 15th, plus every Friday. If you wanted "Fridays that fall on the 1st or 15th", cron can't say that; check the date inside the script.
Daylight saving time skips and repeats jobs. Times that don't exist during the spring change never match, so a job scheduled in that gap doesn't run. Times that happen twice in autumn cause matching jobs to run twice. If a job must run exactly once, schedule it outside the changeover hour or make it idempotent.
An unescaped % truncates your command. date +%F in a crontab breaks, because everything after the % becomes standard input. Write \%.
Missing trailing newline. Each crontab entry must end in a newline. If the last line ends at end-of-file, cron treats the crontab as at least partly broken and writes a warning to syslog.
Trailing comments. */5 * * * * run.sh # every five passes the comment to the shell as part of the command. Put comments on their own line.
Wrong file format. A line in /etc/crontab or /etc/cron.d/ without a username will try to run the first word of your command as the user. A line in a personal crontab with a username will try to run the username as the command.
You can also check syntax before installing a crontab with the -T option; see crontab(1) for details.
Quick reference checklist#
- Five fields, in order: minute, hour, day of month, month, day of week.
- "Every 5 minutes" is
*/5 * * * *. Steps only work within one field, so 35, 58 and similar numbers won't give even gaps. - Weekdays:
1-5ormon-fri. Sunday is0or7. - No "last day of month". Use
28-31and guard in the script. - Both day fields restricted means OR.
- Escape
%as\%. Keep comments on their own lines. - Set
CRON_TZif the schedule is in a specific zone, and remember the log file uses the machine's local time. - Redirect output or set
MAILTOso a noisy job doesn't flood a mailbox. - Don't schedule anything important in the daylight saving changeover hour.
- Run the expression through the matcher above before you install it.
For the full list of variables and edge cases, the crontab(5) page on man7.org is short and worth reading once.
Quick answers
What is the cron expression for every 5 minutes?
Use */5 * * * *. It runs at minutes 0, 5, 10 and so on through 55 of every hour. A full crontab line is */5 * * * * /path/to/command.
Can cron run a job on the last day of the month?
Not directly, because the day-of-month field has no "last" value. Schedule the job for days 28 to 31 with 0 0 28-31 * * and have the script check whether tomorrow is the 1st before doing any work.
How do I set a timezone in a crontab?
Add a CRON_TZ line to the crontab, such as CRON_TZ=Japan, and write your schedule times in that zone. The daemon's log file still uses the local time zone of the machine where it runs.
Why doesn't */35 * * * * run every 35 minutes?
Steps are evaluated within a single field, so it fires at minutes 0 and 35 of each hour. For an interval that doesn't divide 60 evenly, run the job every minute and test the elapsed time in the command, as the crontab(5) notes describe.
Did this solve it?
One tap. No account.
Sorry about that. What was missing?
from 0 readers
Saved you an hour? Send a tip.
USDC or ETH on Base. Keeps the notes free and the examples running.

0x9dFD2Ee86182f92464b50809C6702B999a0Cd54f
On a computer? Scan the code with your phone wallet, or copy the address.
USDC or ETH on Base works best. Other tokens on Base, Ethereum or Arbitrum also reach this address. Tips are optional and unlock nothing; every guide stays free.
Discussion
Start the discussion
Next note