Skip to content

Cron schedule

Cron every 3 days

0 0 */3 * * is the usual answer, but it runs on days 1, 4, 7, …, 28, 31 of each month, so the gap resets every month. For a strict 3-day interval, run daily and test the day count: [ $(( $(date +\%s) / 86400 \% 3 )) -eq 0 ] && job.sh.

Every 3 days
Expression0 0 */3 * *
Runs127 times in 2027
systemd OnCalendar*-*-01/3 00:00:00

How it works

A step in the day-of-month field counts from 1 within each month: */3 expands to 1, 4, 7, 10, 13, 16, 19, 22, 25, 28 and 31. Each month starts over at day 1, whatever happened at the end of the previous one.

That makes the spacing uneven at month boundaries. After the 31st of a 31-day month the next run is the 1st, one day later. After the 28th of a 30-day month the next is the 1st, three days later. February in a non-leap year goes from the 28th to March 1st in one day. Over 2027 the expression fires 127 times, not the 121 or 122 that a steady 3-day rhythm would give.

If “roughly every few days, on predictable calendar dates” is fine, */3 is simple and readable. If the interval matters (rotating credentials, billing cycles, rate-limited APIs), count days since the epoch instead, which cron can’t do natively but a one-line shell test can.

Field-by-field breakdown
FieldValueMeaning
Minute0minute 0 (on the hour)
Hour0hour 0 (12 AM)
Day of month*/3every 3 days, counted from the 1st: 1, 4, 7, …, 31
Month*every month
Day of week*every day of the week

Next runs

Scheduler clock

As on GitHub Actions without timezone, Vercel, Cloudflare and Kubernetes with timeZone "Etc/UTC".

Next run times, computed in your browser from the current time
RunYour timeUTC
1Calculating… 
2  
3  
4  
5  

Ready-to-paste versions

The same schedule for each scheduler, with what differs on each one.

crontab (Linux, macOS, BSD)

crontab -e
# Calendar days 1, 4, 7, ..., 28, 31 (resets every month):
0 0 */3 * * /usr/local/bin/job.sh

# A true 3-day interval: run daily, act when days-since-epoch % 3 == 0
0 0 * * * [ $(( $(date +\%s) / 86400 \% 3 )) -eq 0 ] && /usr/local/bin/job.sh
  • Inside a crontab line % must be written as \%, or cron turns it into a newline. The epoch version counts whole UTC days since 1970, so it never resets at month boundaries.

GitHub Actions

.github/workflows/scheduled.yml
on:
  schedule:
    - cron: '0 0 */3 * *'
      # timezone: 'America/New_York'  # optional; UTC when omitted
  workflow_dispatch: {}
  • Runs in UTC unless you set timezone, and only on the default branch. GitHub’s docs warn that scheduled runs can be delayed at busy times and that schedules in public repositories are disabled after 60 days without activity.
  • The start of every hour is GitHub’s busiest slot. If the exact minute doesn’t matter, move the 0 to something like 17 to get picked up sooner.
  • With a timezone that observes DST, a time skipped by the spring-forward change advances to the next valid time (the docs’ example: 2:30 AM becomes 3:00 AM).

Kubernetes CronJob

cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: scheduled-job
spec:
  schedule: "0 0 */3 * *"
  timeZone: "Etc/UTC"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: job
              image: busybox:1.36
              command: ["/bin/sh", "-c", "date; echo running"]
  • spec.timeZone is stable since Kubernetes 1.27; without it, the kube-controller-manager’s zone applies. concurrencyPolicy: Forbid skips a run while the previous Job is still active.

Vercel Cron Jobs

vercel.json
{
  "crons": [
    { "path": "/api/cron", "schedule": "0 0 */3 * *" }
  ]
}
  • Vercel always uses UTC, doesn’t accept names like MON or JAN, and won’t let you set both day of month and day of week.
  • This fits the Hobby plan, but Hobby precision is per hour: the invocation can land anywhere within the scheduled hour.

Cloudflare Workers

wrangler.toml
[triggers]
crons = ["0 0 */3 * *"]
  • Cron Triggers run on UTC. Changes can take up to 15 minutes to propagate across Cloudflare’s network.

node-cron (Node.js)

scheduler.js
import cron from 'node-cron';

// 6 fields: the first one (seconds) is optional
cron.schedule('0 0 0 */3 * *', async () => {
  await runJob();
}, { timezone: 'UTC', noOverlap: true });
  • The schedule lives inside the Node process: if it’s down, nothing runs, and three replicas run the job three times. noOverlap skips a tick while the previous run is still going.

systemd timer

job.timer + job.service
# /etc/systemd/system/job.timer
[Unit]
Description=Run job.service on days 1, 4, 7, ... of the month

[Timer]
OnCalendar=*-*-01/3 00:00:00
Persistent=true

[Install]
WantedBy=timers.target

# /etc/systemd/system/job.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/job.sh

# systemctl daemon-reload && systemctl enable --now job.timer
  • Check it with systemd-analyze calendar '*-*-01/3 00:00:00', which prints the normalized form and the next elapse. AccuracySec defaults to 1min, so the start can drift by up to a minute.
  • Persistent=true runs the job at boot if the machine was off at the scheduled time, which plain cron doesn’t do.
  • 01/3 in systemd has the same month reset as cron. For a strict 72-hour cadence use a monotonic timer instead: OnUnitActiveSec=3d plus OnBootSec=5min to start the cycle.

Spring @Scheduled and Quartz

Java
@Scheduled(cron = "0 0 0 */3 * *", zone = "UTC")
public void runJob() {
    // ...
}

// Quartz CronTrigger:
CronScheduleBuilder.cronSchedule("0 0 0 1/3 * ?")
  • In Spring, the first of the 6 fields is seconds; the rest follow standard cron (0 or 7 = Sunday).
  • In Quartz, one of the two day fields must be ?, and weekdays run 1 = Sunday to 7 = Saturday, which is why the example uses names.

Pitfalls

The month-boundary gap

January 31 and February 1 both run, back to back. April 28 to May 1 is three days. The same reset hits */2 (days 1, 3, …, 31, then 1 again) and any other day step.

Adding a weekday turns OR into AND, or not

In cronie a day-of-month field that starts with * counts as unrestricted, so 0 0 */3 * 1 means “every third day AND Monday”. Other implementations treat */3 as restricted and OR it with Monday. Avoid combining the two.

Hosted schedulers inherit the same behavior

GitHub Actions, Kubernetes, Vercel and Cloudflare all expand */3 per month. Only a monotonic timer (systemd OnUnitActiveSec=3d) or an epoch check in the job gives a real 72-hour period.

Frequently asked questions

What is the cron expression for every 3 days?
0 0 */3 * * runs at midnight on days 1, 4, 7, …, 28 and 31 of each month. The interval resets every month, so it’s not strictly every 3 days.
Why does my every-3-days cron job run two days in a row?
Because the step restarts on the 1st. In a 31-day month the job runs on the 31st and again on the 1st.
How do I run a cron job exactly every 3 days?
Run it daily and check the day count since the Unix epoch: [ $(( $(date +\%s) / 86400 \% 3 )) -eq 0 ] && job.sh. Or use a systemd timer with OnUnitActiveSec=3d.
Does every 2 days have the same problem?
Yes. */2 in day-of-month runs on odd days (1, 3, …, 29, 31), so a 31-day month runs on the 31st and the 1st.

Last reviewed by Arielton Oberek.