Skip to content

Cron schedule

Cron twice a day

To run a cron job twice a day, list both hours in the hour field: 0 9,21 * * * runs at 09:00 and 21:00. Swap in any two hours, such as 0 0,12 * * * for midnight and noon.

Twice a day
Expression0 9,21 * * *
Runs2 times a day
systemd OnCalendar*-*-* 09,21:00:00

How it works

The comma builds a list, so 9,21 in the hour field means “hour 9 or hour 21”. With the minute fixed at 0, that’s two runs a day. The list can hold any hours in any order; cron sorts nothing and doesn’t care.

When the two times have different minutes, one line isn’t enough. 0,30 9,17 * * * doesn’t mean 09:00 and 17:30: every listed minute combines with every listed hour, giving 09:00, 09:30, 17:00 and 17:30. Use two lines, 0 9 * * * and 30 17 * * *.

If the two runs should be exactly 12 hours apart, 0 */12 * * * (midnight and noon) is the same thing written as a step; see the every-12-hours page.

Field-by-field breakdown
FieldValueMeaning
Minute0minute 0 (on the hour)
Hour9,21hours 9 and 21 (9 AM and 9 PM)
Day of month*every day of the month
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 9,21 * * * /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 9,21 * * *'
      # 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 9,21 * * *"
  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 9,21 * * *" }
  ]
}
  • 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 runs more than once a day, so it needs a Pro or Enterprise plan. Per Vercel’s docs, a Hobby deployment with an expression like this fails.

Cloudflare Workers

wrangler.toml
[triggers]
crons = ["0 9,21 * * *"]
  • 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 9,21 * * *', 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 at 09:00 and 21:00

[Timer]
OnCalendar=*-*-* 09,21:00:00

[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 '*-*-* 09,21: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.

Spring @Scheduled and Quartz

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

// Quartz CronTrigger:
CronScheduleBuilder.cronSchedule("0 0 9,21 * * ?")
  • 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 ?, so the day-of-week field gets it.

Pitfalls

A hyphen is a range, not a pair

0 9-21 * * * runs every hour from 09:00 to 21:00, 13 times a day. Two runs need a comma: 0 9,21 * * *.

Minutes and hours multiply

Every minute in the minute list runs at every hour in the hour list. 15,45 8,20 * * * is four runs (08:15, 08:45, 20:15, 20:45). Two different clock times with different minutes need two crontab lines, or two cron: entries on GitHub.

Vercel Hobby rejects it

Vercel’s Hobby plan allows at most one run a day; a twice-daily expression fails the deployment. Pro and Enterprise accept it.

Frequently asked questions

How do I run a cron job twice a day?
Put two hours in the hour field separated by a comma. 0 9,21 * * * runs at 09:00 and 21:00; 0 0,12 * * * runs at midnight and noon.
How do I run a cron job at 9:00 and 17:30?
Use two lines: 0 9 * * * and 30 17 * * *. One line would combine both minutes with both hours.
How do I run twice a day only on weekdays?
0 9,21 * * 1-5 runs at 09:00 and 21:00, Monday to Friday.
Can GitHub Actions run at two different times?
Yes. Either list the hours in one expression or add two cron: entries under on.schedule; github.event.schedule tells the workflow which one fired.

Last reviewed by Arielton Oberek.