Cron schedule
Cron every quarter
The cron expression for every quarter is 0 0 1 1,4,7,10 *: 00:00 on January 1, April 1, July 1 and October 1. 0 0 1 */3 * expands to the same four months.
| Expression | 0 0 1 1,4,7,10 * |
|---|---|
| Runs | 4 times in 2027 |
| systemd OnCalendar | quarterly |
How it works
Quarters are a month-field job. 1,4,7,10 names the first month of each calendar quarter, and day 1 with 0 0 makes it midnight on the first day. */3 in the month field counts from 1 in steps of 3, which lands on the same months.
systemd has a keyword for it: quarterly normalizes to *-01,04,07,10-01 00:00:00. crontab and Kubernetes have no @quarterly macro, so you write the fields.
A quarter-start run is when the previous quarter has closed, which suits financial summaries, compliance exports and license reviews. The job should report on the quarter that just ended.
| Field | Value | Meaning |
|---|---|---|
| Minute | 0 | minute 0 (on the hour) |
| Hour | 0 | hour 0 (12 AM) |
| Day of month | 1 | day 1 |
| Month | 1,4,7,10 | January, April, July and October |
| 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)
# m h dom mon dow command
0 0 1 1,4,7,10 * /usr/local/bin/job.sh >> /var/log/job.log 2>&1- cron uses the server’s time zone. On cronie (Fedora, RHEL), a
CRON_TZ=Europe/Londonline above the entry changes the zone for the entries below it.
GitHub Actions
on:
schedule:
- cron: '0 0 1 1,4,7,10 *'
# 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 1 1,4,7,10 *"
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 1 1,4,7,10 *" }
]
}- 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 1 1,4,7,10 *"]- 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 1 1,4,7,10 *', 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 quarterly
[Timer]
OnCalendar=quarterly
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 'quarterly', 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.
Spring @Scheduled and Quartz
@Scheduled(cron = "0 0 0 1 1,4,7,10 *", zone = "UTC")
public void runJob() {
// ...
}
// Quartz CronTrigger:
CronScheduleBuilder.cronSchedule("0 0 0 1 1,4,7,10 ?")- 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
Fiscal quarters don’t always start in January
If the fiscal year starts in April, quarters start in April, July, October and January: the same list. If it starts in February, you need 0 0 1 2,5,8,11 *, and */3 can’t express that without a range like 2-12/3.
Quarter end is not quarter start
Running on the last day of March, June, September and December needs the last-day workaround limited to those months: 0 0 28-31 3,6,9,12 * plus a check that tomorrow is the 1st. In Quartz, 0 0 0 L 3,6,9,12 ?.
Four runs a year are easy to lose
A quarterly job that silently fails costs three months. Alert on failure and keep a way to trigger it by hand, like GitHub’s workflow_dispatch or kubectl create job --from=cronjob/….
Frequently asked questions
- What is the cron expression for every quarter?
- 0 0 1 1,4,7,10 * runs at midnight on the first day of January, April, July and October. 0 0 1 */3 * is equivalent.
- Is there a @quarterly macro in cron?
- Not in crontab(5) or Kubernetes. systemd has OnCalendar=quarterly.
- How do I run at the end of each quarter?
- Schedule 0 0 28-31 3,6,9,12 * with a check that tomorrow is the 1st, or use L in Quartz, Spring, node-cron or Cloudflare.
- How do I test a quarterly cron job?
- Run the command manually, and verify the schedule with a next-run calculator such as the table on this page or systemd-analyze calendar quarterly.
Last reviewed by Arielton Oberek.