Skip to content

Cron schedule

Cron on the first day of the month

0 0 1 * * runs at 00:00 on the first day of every month. For the first Monday or first business day, standard cron needs a date check in the command; Spring and Quartz have MON#1 and 1W.

First day of the month
Expression0 0 1 * *
Runs12 times in 2027
systemd OnCalendar*-*-01 00:00:00

How it works

1 in the day-of-month field matches the 1st, and the month field * repeats it every month. It’s the same line as @monthly; this page is about the variations people usually need next.

The first business day depends on weekends: if the 1st is a Saturday, it’s the 3rd. Standard cron can’t express that. Quartz and Spring support 1W (“the weekday nearest the 1st, without leaving the month”), so 0 0 0 1W * ? in Quartz. In plain cron, run on days 1 to 3 and let the script decide.

The first Monday is the classic trap: it looks like 0 0 1-7 * 1 and isn’t. Each tool handles it differently, so the pitfalls below spell out what to write where.

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 on the 1st of each month

[Timer]
OnCalendar=*-*-01 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 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.

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

0 0 1-7 * 1 runs 10 or 11 times a month

With both day fields restricted, Vixie cron and cronie run the job when either matches: every day from the 1st to the 7th, plus every Monday. Write 0 0 1-7 * * and test the weekday in the command: [ "$(date +\%u)" = 1 ] && job.sh.

node-cron and systemd use AND, not OR

node-cron combines day of month and day of week with AND, so 0 0 1-7 * 1 really is the first Monday there. systemd works the same way: Mon *-*-01..07 00:00:00. The same string means different things on different schedulers.

Quartz won’t accept both day fields

Quartz requires ? in one of the day fields, so it rejects the OR form outright and gives you MON#1 instead: 0 0 0 ? * MON#1. Spring accepts MON#1 too.

Frequently asked questions

What is the cron expression for the first day of the month?
0 0 1 * * runs at midnight on the 1st of every month. @monthly is equivalent.
How do I run a cron job on the first Monday of the month?
In crontab: 0 0 1-7 * * with [ "$(date +\%u)" = 1 ] && job.sh. In Spring or Quartz: MON#1 in the day-of-week field. In systemd: OnCalendar=Mon *-*-01..07 00:00:00.
How do I run on the first business day of the month?
Quartz and Spring support 1W in the day-of-month field. In plain cron, run on days 1-3 and have the script check whether today is the first weekday of the month.
Does cron run on the 1st if the server was off at midnight?
No. Plain cron doesn’t catch up. systemd timers with Persistent=true and anacron do.

Last reviewed by Arielton Oberek.