Skip to content

Cron schedule

Cron every 30 minutes

The cron expression for every 30 minutes is */30 * * * *. It runs on the hour and on the half hour, at minutes 0 and 30, 48 times a day.

Every 30 minutes
Expression*/30 * * * *
Runs48 times a day
systemd OnCalendar*:0/30

How it works

*/30 in the minute field expands to just two values, 0 and 30. The list form 0,30 * * * * says exactly the same thing and is arguably clearer, since there are only two entries.

Half-hourly is a common compromise for syncs and reports that should feel fresh without hammering an API. It’s also the example Vercel uses in its own docs of what the Hobby plan rejects: anything that runs more than once a day needs Pro or Enterprise there.

If you want half-hour spacing but not on the hour, list the minutes yourself: 15,45 * * * * runs at quarter past and quarter to.

Field-by-field breakdown
FieldValueMeaning
Minute*/30every 30 minutes: 0, 30
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
*/30 * * * * /usr/local/bin/job.sh >> /var/log/job.log 2>&1

# Same, but skip a run while the previous one is still going:
*/30 * * * * 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: '*/30 * * * *'
  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.

Kubernetes CronJob

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

[Timer]
OnCalendar=*:0/30

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

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

30 * * * * is once an hour, at half past

A single value runs once per hour: minute 30, 24 runs a day. People reach for it when they mean every 30 minutes. The step form is */30.

*/30 in the hour field is not “every 30 hours”

The hour field only goes to 23, so 0 */30 * * * matches hour 0 only: once a day at midnight. cron has no way to express intervals longer than the field; see the every-3-days page for the day-level version of this trap.

Half-hour time zones

India (UTC+5:30) and parts of Australia are offset by half an hour. A UTC schedule at :00 and :30 still lands on :30 and :00 locally, but a job that should run “on the hour, local time” in those zones needs minute 30 in UTC, not 0.

Frequently asked questions

What is the cron expression for every 30 minutes?
*/30 * * * *, which runs at minute 0 and minute 30 of every hour. 0,30 * * * * is equivalent.
How do I run a cron job every 30 minutes starting at a quarter past?
List the minutes: 15,45 * * * * runs at :15 and :45 of every hour.
Can a Vercel cron job run every 30 minutes on the Hobby plan?
No. Vercel’s docs use */30 * * * * as an example of an expression that fails to deploy on Hobby, which is limited to once a day.
How do I run every 30 minutes only at night?
Restrict the hours: */30 22-23,0-5 * * * runs every half hour from 22:00 to 05:30.

Last reviewed by Arielton Oberek.