Cron schedule
Cron every 2 hours
The cron expression for every 2 hours is 0 */2 * * *. It runs at minute 0 of every even hour (00:00, 02:00, 04:00, …, 22:00), 12 times a day.
| Expression | 0 */2 * * * |
|---|---|
| Runs | 12 times a day |
| systemd OnCalendar | *-*-* 0/2:00:00 |
How it works
Two fields do the work. */2 in the hour field selects 0, 2, 4, …, 22, and the 0 in the minute field pins each run to the top of that hour. The long form is 0 0,2,4,6,8,10,12,14,16,18,20,22 * * *.
For the odd hours instead, give the step a starting point with a range: 0 1-23/2 * * * runs at 01:00, 03:00, …, 23:00. The same trick moves the minute too, for example 30 1-23/2 * * *.
Since 2 divides 24, the spacing is a steady 2 hours, including the jump from 22:00 to 00:00. That stops being true for steps like 5 or 7; see the every-6-hours page.
| Field | Value | Meaning |
|---|---|---|
| Minute | 0 | minute 0 (on the hour) |
| Hour | */2 | every 2 hours: 0, 2, 4, …, 22 |
| Day of month | * | every day of the month |
| Month | * | every month |
| Day of week | * | every day of the week |
Next runs
As on GitHub Actions without timezone, Vercel, Cloudflare and Kubernetes with timeZone "Etc/UTC".
| Run | Your time | UTC |
|---|---|---|
| 1 | Calculating… | |
| 2 | ||
| 3 | ||
| 4 | ||
| 5 |
Ready-to-paste versions
The same schedule for each scheduler, with what differs on each one.
crontab (Linux, macOS, BSD)
# m h dom mon dow command
0 */2 * * * /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/Londonline above the entry changes the zone for the entries below it.
GitHub Actions
on:
schedule:
- cron: '0 */2 * * *'
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
0to something like17to get picked up sooner.
Kubernetes CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: scheduled-job
spec:
schedule: "0 */2 * * *"
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.timeZoneis stable since Kubernetes 1.27; without it, the kube-controller-manager’s zone applies.concurrencyPolicy: Forbidskips a run while the previous Job is still active.
Vercel Cron Jobs
{
"crons": [
{ "path": "/api/cron", "schedule": "0 */2 * * *" }
]
}- Vercel always uses UTC, doesn’t accept names like
MONorJAN, 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
[triggers]
crons = ["0 */2 * * *"]- Cron Triggers run on UTC. Changes can take up to 15 minutes to propagate across Cloudflare’s network.
node-cron (Node.js)
import cron from 'node-cron';
// 6 fields: the first one (seconds) is optional
cron.schedule('0 0 */2 * * *', 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.
noOverlapskips a tick while the previous run is still going.
systemd timer
# /etc/systemd/system/job.timer
[Unit]
Description=Run job.service every 2 hours
[Timer]
OnCalendar=*-*-* 0/2: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 '*-*-* 0/2:00:00', which prints the normalized form and the next elapse.AccuracySecdefaults to 1min, so the start can drift by up to a minute.
Spring @Scheduled and Quartz
@Scheduled(cron = "0 0 */2 * * *", zone = "UTC")
public void runJob() {
// ...
}
// Quartz CronTrigger:
CronScheduleBuilder.cronSchedule("0 0 0/2 * * ?")- 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
* */2 * * * runs every minute of every other hour
With * in the minute field, each even hour matches for all 60 minutes: 720 runs a day. The minute has to be fixed, usually 0.
Even hours in UTC may be odd hours locally
GitHub, Vercel and Cloudflare evaluate cron in UTC. In a UTC-3 or UTC+5:30 zone the runs land on odd hours or half hours. That matters if the job is expected “at 2, 4, 6” local time.
Two-hour jobs that run long
A job that sometimes takes over 2 hours will overlap the next one. Kubernetes concurrencyPolicy: Forbid or flock -n in crontab skips the late run instead of doubling the load.
Frequently asked questions
- What is the cron expression for every 2 hours?
- 0 */2 * * *. It runs at 00:00, 02:00, 04:00 and every even hour up to 22:00.
- How do I run a cron job every 2 hours on odd hours?
- 0 1-23/2 * * * runs at 01:00, 03:00, …, 23:00.
- How do I run every 2 hours between 8 AM and 8 PM?
- 0 8-20/2 * * * runs at 08:00, 10:00, 12:00, 14:00, 16:00, 18:00 and 20:00.
- Is 0 */2 * * * the same as 0 0-23/2 * * *?
- Yes. A step after * covers the whole field, which for hours is 0-23.
Last reviewed by Arielton Oberek.