A cron expression that runs every 30 minutes:
*/30 * * * *
This fires at :00 and :30 of every hour. Equivalent to:
0,30 * * * *
48 fires per day. The page above generates the schedule and others; this companion explains the half-hour cadence and how it differs from neighboring schedules.
How */30 works
The / is the step operator. */30 in the minute field means “every 30 minutes from minute 0 of the hour.” So minute 0 fires, minute 30 fires, and that’s it for the hour — minute 60 is minute 0 of the next hour.
*/30 = 0, 30
Some engineers prefer the explicit form 0,30 * * * * for clarity; the schedules are identical.
When 30-minute cadence is right
- Sync jobs that pull moderately fresh data. A 15-minute cadence is overkill for most external data; an hourly cadence is too stale. Half-hour splits the difference.
- Scheduled cache invalidation. Twice an hour is enough for content that doesn’t update on a per-minute basis but does change.
- Heartbeat-style health checks. A check that’s too costly to run every 5 minutes but worth running multiple times an hour.
- Quotation/price refreshes in non-trading systems where intra-day freshness is useful but per-minute is overkill.
When 30-minute cadence is wrong
- The job takes longer than 30 minutes. Overruns into the next window. Either run less frequently or guarantee no overlap (
flock, in-job state check). - Real-time freshness matters. 30-minute staleness is meaningful in a UX. Either move to a streaming/event model or run more frequently.
- The job is heavy and idle most of the time. If 99% of
*/30runs do nothing because there’s no work to do, switch to event-driven scheduling.
Half-hour staggering
Like every other multi-of-an-hour schedule, */30 lines up across services. If many internal jobs use it, they all fire at :00 and :30. To spread load:
0,30 * * * * ← exactly on the half-hour
5,35 * * * * ← offset by 5 minutes
10,40 * * * * ← offset by 10
15,45 * * * * ← lines up with quarter-hour (also valid for "every 30")
All four are 30-minute cadences with different alignment. Pick one based on what other jobs and external systems are already doing.
Variants
*/30 * * * * ← :00 and :30 every hour, every day
0,30 * * * 1-5 ← weekdays only
*/30 9-17 * * * ← only during 9 AM–5 PM
0,30 0,12 * * * ← :00 and :30 of midnight and noon — 4 fires/day, not 48
The fourth example is the common mistake — combining steps with discrete hour values produces sparser schedules than the writer expects. Always read back the schedule with the cron parser before deploying.
Comparison with neighboring cadences
| Cadence | Expression | Fires per day | Typical use |
|---|---|---|---|
| Every minute | * * * * * | 1440 | Real-time monitoring, fast retry |
| Every 5 minutes | */5 * * * * | 288 | High-frequency sync |
| Every 15 minutes | */15 * * * * | 96 | Quarter-hourly metrics |
| Every 30 minutes | */30 * * * * | 48 | This page — moderate freshness |
| Hourly | 0 * * * * | 24 | Standard refresh cadence |
| Daily | 0 0 * * * | 1 | Off-peak batch jobs |
The 5-minute and 30-minute cadences are the two most common picks for “more-than-hourly but not real-time” jobs.
Common pitfalls
- Drift due to long-running jobs. A
*/30job that sometimes takes 25 minutes has only 5 minutes of slack. Sometimes it’ll overrun. Addflockor schedule less frequently. - Off-by-one minute alignment.
0 30 * * *(with a space) is “minute 0, hour 30” — and hour 30 doesn’t exist, so the daemon will reject it. Always check the field separation. - Assuming
*/30runs exactly 30 minutes apart. If the system is overloaded and cron fires are queued, the actual interval can be longer. cron is best-effort, not real-time. - Mixing UTC and local time. A
*/30schedule in local time has subtle DST edge cases (the spring-forward hour is missing two fires; the fall-back hour has an extra two). UTC is cleaner if precision matters.
For neighboring schedules, see cron every 15 minutes and cron every hour. For the broader cron syntax, see cron expression generator.