Skip to content

Cron schedule

Cron every hour

To run a cron job every hour, use 0 * * * *, or the macro @hourly. It fires at minute 0 of every hour, 24 times a day.

Every hour
Expression0 * * * *
Macro@hourly
Runs24 times a day
systemd OnCalendarhourly

How it works

The minute field is what makes this hourly. A fixed 0 means “only at minute 0”, and * in the hour field lets that match every hour. Any other fixed minute also runs hourly: 17 * * * * is every hour at :17.

@hourly is defined in crontab(5) as exactly 0 * * * *, and Kubernetes, Spring and most libraries accept it. GitHub’s docs say Actions doesn’t support the @ macros, and Vercel and Cloudflare document only the five fields, so write them out there.

Distributions pick an odd minute for their own hourly jobs to avoid the top-of-the-hour crowd: Debian’s /etc/crontab runs /etc/cron.hourly at minute 17, and Fedora’s /etc/cron.d/0hourly at minute 01. Copy the idea if your job hits shared infrastructure.

Field-by-field breakdown
FieldValueMeaning
Minute0minute 0 (on the hour)
Hour*every hour
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 * * * * /usr/local/bin/job.sh >> /var/log/job.log 2>&1

# Same, but skip a run while the previous one is still going:
0 * * * * flock -n /tmp/job.lock /usr/local/bin/job.sh
  • 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 * * * *'
  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.

Kubernetes CronJob

cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: scheduled-job
spec:
  schedule: "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 * * * *" }
  ]
}
  • 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 * * * *"]
  • 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 * * * *', 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 hour

[Timer]
OnCalendar=hourly

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

// Quartz CronTrigger:
CronScheduleBuilder.cronSchedule("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

* * * * * or * */1 * * * is every minute, not every hour

An asterisk in the minute field matches all 60 minutes. * */1 * * * still runs 60 times an hour. The minute must be a single number for an hourly job.

The top of the hour is the busiest minute

GitHub’s docs name the start of every hour as a high-load time when scheduled workflows get delayed or even dropped. Moving to 17 * * * * or similar costs nothing and usually helps.

DST shifts the hourly count

On the spring-forward day a local-time hourly job runs 23 times. On the fall-back day the clock shows 01:00 twice, and cronie, whose DST handling in cron(8) only covers fixed-time jobs, runs it both times. Schedule in UTC if you need exactly 24 runs a day.

Frequently asked questions

What is the cron expression for every hour?
0 * * * *, meaning minute 0 of every hour. The @hourly macro is identical where it’s supported.
Is @hourly the same as 0 * * * *?
Yes. crontab(5) defines @hourly as 0 * * * *, and Kubernetes documents the same equivalence.
How do I run a cron job every hour at 30 minutes past?
30 * * * * runs at 00:30, 01:30, 02:30 and so on.
How do I run every hour only between 8 AM and 6 PM?
0 8-18 * * * runs on the hour from 08:00 to 18:00, eleven times a day.

Last reviewed by Arielton Oberek.