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.
| Expression | 0 0 */3 * * |
|---|---|
| Runs | 127 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 | Value | Meaning |
|---|---|---|
| Minute | 0 | minute 0 (on the hour) |
| Hour | 0 | hour 0 (12 AM) |
| Day of month | */3 | every 3 days, counted from the 1st: 1, 4, 7, …, 31 |
| Month | * | every month |
| Day of week | * | every day of the week |
Next runs
As on GitHub Actions without timezone, Vercel, Cloudflare and Kubernetes with timeZone "Etc/UTC".
| Run | Your time | UTC |
|---|---|---|
| 1 | Calculating… | |
| 2 | ||
| 3 | ||
| 4 | ||
| 5 |
Ready-to-paste versions
The same schedule for each scheduler, with what differs on each one.
crontab (Linux, macOS, BSD)
# 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
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
0to something like17to get picked up sooner. - With a
timezonethat 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
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.timeZoneis stable since Kubernetes 1.27; without it, the kube-controller-manager’s zone applies.concurrencyPolicy: Forbidskips a run while the previous Job is still active.
Vercel Cron Jobs
{
"crons": [
{ "path": "/api/cron", "schedule": "0 0 */3 * *" }
]
}- Vercel always uses UTC, doesn’t accept names like
MONorJAN, 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
[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)
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.
noOverlapskips a tick while the previous run is still going.
systemd timer
# /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.AccuracySecdefaults to 1min, so the start can drift by up to a minute. Persistent=trueruns the job at boot if the machine was off at the scheduled time, which plain cron doesn’t do.01/3in systemd has the same month reset as cron. For a strict 72-hour cadence use a monotonic timer instead:OnUnitActiveSec=3dplusOnBootSec=5minto start the cycle.
Spring @Scheduled and Quartz
@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.