Skip to content

Cron schedule

Cron every day at midnight

The cron expression for every day at midnight is 0 0 * * *: minute 0 of hour 0, every day. @daily and @midnight are shorthand for the same line.

Every day at midnight
Expression0 0 * * *
Macro@daily
Runs365 times in 2027
systemd OnCalendardaily

How it works

Midnight is hour 0, minute 0. There is no hour 24 in cron; 0 24 * * * is rejected. The remaining three fields are *, so the job runs every day of every month regardless of the weekday.

crontab(5) defines both @daily and @midnight as 0 0 * * *, and Kubernetes and Spring accept them. systemd’s own daily keyword means *-*-* 00:00:00, the same time.

The real question is whose midnight. A crontab uses the server’s zone, while GitHub Actions (without timezone), Vercel and Cloudflare use UTC. UTC midnight is 21:00 the previous evening in São Paulo and 19:00 or 20:00 in New York.

Field-by-field breakdown
FieldValueMeaning
Minute0minute 0 (on the hour)
Hour0hour 0 (12 AM)
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 0 * * * /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 * * *'
      # 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 * * *"
  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 * * *" }
  ]
}
  • 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 * * *"]
  • 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 * * *', 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 every day at midnight

[Timer]
OnCalendar=daily
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 'daily', 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 * * *", zone = "UTC")
public void runJob() {
    // ...
}

// Quartz CronTrigger:
CronScheduleBuilder.cronSchedule("0 0 0 * * ?")
  • 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

Midnight is the busiest minute of the day

Log rotation, backups, reports and everyone else’s @daily jobs start at 00:00. If yours doesn’t have to be exact, 17 0 * * * or 0 2 * * * avoids the pile-up on shared databases and APIs.

“Yesterday” and “today” at 00:00

A job that processes “today’s” data at 00:00 sees an empty day that just started. Nightly jobs usually want the previous date, e.g. date -d yesterday +\%F (note the escaped % in a crontab line).

Machines that sleep miss the run

cron doesn’t catch up on a laptop or VM that was off at midnight. anacron or a systemd timer with Persistent=true runs the missed job at the next boot.

Frequently asked questions

What is the cron expression for every day at midnight?
0 0 * * *, meaning minute 0 of hour 0 on every day. @daily and @midnight are equivalent.
Is midnight 0 0 or 0 24 in cron?
0 0. The hour field runs from 0 to 23, so 24 is invalid.
How do I run a GitHub Actions workflow at midnight in my time zone?
Add timezone next to cron, e.g. cron: '0 0 * * *' with timezone: 'America/New_York'. Without it, the schedule is UTC.
How do I run a job every day at 23:59 instead?
59 23 * * * runs one minute before midnight, which keeps the run on the same calendar date it processes.

Last reviewed by Arielton Oberek.