Skip to content

Cron schedule

Cron every month

To run a cron job once a month, use @monthly or 0 0 1 * *, which fires at 00:00 on the 1st of every month. Any day from 1 to 28 also gives exactly 12 runs a year.

Every month
Expression0 0 1 * *
Macro@monthly
Runs12 times in 2027
systemd OnCalendarmonthly

How it works

The day-of-month field carries the schedule: 1 means the first, and * in the month field lets it match all twelve months. crontab(5) and Kubernetes define @monthly as exactly this, and systemd’s monthly keyword is the same time (*-*-01 00:00:00).

You can choose any day, but only 1 to 28 exist in every month. 0 0 15 * * runs mid-month, twelve times a year. Days 29, 30 and 31 silently skip the months that don’t have them.

For billing, invoicing and monthly reports, the 1st at 00:00 is when the previous month is complete. The job should compute “last month” rather than “this month”, which on the 1st has barely started.

Field-by-field breakdown
FieldValueMeaning
Minute0minute 0 (on the hour)
Hour0hour 0 (12 AM)
Day of month1day 1
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
# m h dom mon dow  command
0 0 1 * * /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/London line above the entry changes the zone for the entries below it.

GitHub Actions

.github/workflows/scheduled.yml
on:
  schedule:
    - cron: '0 0 1 * *'
      # 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 1 * *"
  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 1 * *" }
  ]
}
  • 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 1 * *"]
  • 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 1 * *', 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 monthly, on the 1st at midnight

[Timer]
OnCalendar=monthly
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 'monthly', 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.

Spring @Scheduled and Quartz

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

// Quartz CronTrigger:
CronScheduleBuilder.cronSchedule("0 0 0 1 * ?")
  • 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 31st skips five months

0 0 31 * * runs only in January, March, May, July, August, October and December: seven times a year. 0 0 30 * * skips February. If you mean the end of the month, see the last-day-of-month page.

Month-end work on the 1st

A report that runs on the 1st must query the previous month. Getting that wrong produces an empty report on the 1st and nobody notices until someone asks.

One missed run is a month of missing data

A monthly job that fails or doesn’t run leaves a gap for a whole month. Alert on failure, and on a machine that may be off, use systemd Persistent=true or anacron.

Frequently asked questions

What is the cron expression for once a month?
0 0 1 * *, or @monthly, runs at midnight on the 1st of every month.
How do I run a cron job on the 15th of every month?
0 0 15 * * runs at 00:00 on the 15th.
Why doesn’t my cron job run on the 31st every month?
Only seven months have 31 days, and cron skips days that don’t exist. Use a day from 1 to 28, or the last-day workaround.
What is @monthly in Spring?
Spring defines @monthly as 0 0 0 1 * *, the same time with a leading seconds field.

Last reviewed by Arielton Oberek.