CoreMesh

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#

text
┌──────── 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-b is an inclusive range. 8-11 in the hours field means 8, 9, 10 and 11.
  • a,b,c is a list. Lists can contain ranges, like 0-4,8-12.
  • /n is 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:

text
*/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:

text
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/sh unless you set SHELL in the crontab. HOME can also be overridden; LOGNAME can'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/crontab and files in /etc/cron.d/) need a username between the schedule and the command:
text
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.

python
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:

text
* * * * * 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-5 or mon-fri. Sunday is 0 or 7.
  • No "last day of month". Use 28-31 and guard in the script.
  • Both day fields restricted means OR.
  • Escape % as \%. Keep comments on their own lines.
  • Set CRON_TZ if the schedule is in a specific zone, and remember the log file uses the machine's local time.
  • Redirect output or set MAILTO so 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.

Saved you an hour? Send a tip.

USDC or ETH on Base. Keeps the notes free and the examples running.

Base
QR code with the CoreMesh tip address on Base
0x9dFD2Ee86182f92464b50809C6702B999a0Cd54f
Open in wallet · 5 USDC

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

`code` and ``` blocks work

No account needed. Comments are read before they show up, usually the same day. Your email is never asked for.

Indra Wijaya

Builds and runs AI agents, bots and content pipelines, and writes down what works and what broke.

About · Email · RSS

Keep reading

New notes, no spam.

One post at a time, only when there's something worth reading. Follow in any feed reader.

Subscribe via RSS