DD
DevDash
cronschedulingdevopskubernetesautomation

Cron Expressions in 2026: 7 Patterns That Ship in Prod

Quick Answer

Cron syntax has five fields: minute, hour, day of month, month, day of week. Quartz-style cron adds a seconds field at the start and a year field at the end, totaling six or seven. The 95% of real-world cron you will ever write fits into seven patterns: every N minutes, daily at a fixed time, weekdays only, monthly on a date, last day of the month, specific weekdays, and a time window on specific hours. Timezone and daylight saving time (DST) are where cron bites you. Server time zone defaults to UTC on cloud hosts. If your cron runs at 0 2 * and you want 2 AM local time in New York, either configure the scheduler's timezone or be prepared for the job to shift by an hour twice a year.

I have written cron expressions for about 12 years across Linux crontab, Kubernetes CronJob, GitHub Actions, Vercel Cron, and Temporal schedules. Almost every real outage I have traced to cron came from one of four mistakes: wrong timezone, forgetting that day-of-month and day-of-week are OR'd together, assuming cron guarantees exactly-once delivery (it does not), or running a long-running job under cron without a lock.

This post covers the seven patterns I actually use, the syntax differences across major schedulers in 2026, the DST traps, and when you should reach past cron for something like Temporal or Kubernetes.

The five-field syntax, one more time

*    *    *    *    *
|    |    |    |    |
|    |    |    |    +--- Day of week   (0-7, 0 and 7 both = Sunday)
|    |    |    +-------- Month         (1-12 or JAN-DEC)
|    |    +------------- Day of month  (1-31)
|    +------------------ Hour          (0-23)
+----------------------- Minute        (0-59)

Operators you get for free:

  • * every value
  • , list (e.g. 1,15,30)
  • - range (e.g. 9-17)
  • / step (e.g. */15 every 15)

Some schedulers add non-standard operators. Quartz has L (last), W (nearest weekday), # (nth occurrence of a weekday in a month). Linux crontab does not.

The 7 patterns that cover 95% of real work

#Use caseExpressionNotes
1Every 15 minutes/15 *Runs at :00, :15, :30, :45
2Daily at 02:3030 2 *Timezone matters, see below
3Every weekday at 09:000 9 1-5Mon-Fri
4First of the month at 06:000 6 1 Good for monthly reports
5Last day of the month0 23 28-31 + guardSee trick below
6Every Tuesday and Thursday at 14:000 14 2,4
7Every hour 09:00-17:00 on weekdays0 9-17 1-5Office-hours jobs

Pattern 5: the last-day-of-month trick

Standard cron cannot say "last day of the month" directly because months vary in length. The two common fixes:

Option A, if your scheduler supports Quartz: use L. Quartz expression for last day at 23:00 is 0 0 23 L * ?.

Option B, pure POSIX cron: run every day from the 28th onward and guard with a date check inside your script.

# crontab entry
0 23 28-31 * * /usr/local/bin/month-end.sh

# month-end.sh
#!/bin/bash
tomorrow=$(date -d 'tomorrow' +%d)
if [ "$tomorrow" = "01" ]; then
  /usr/local/bin/run-month-end-job
fi

The script runs nightly on the 28th, 29th, 30th, and 31st (whichever exist in that month), checks whether tomorrow is the first, and only fires on the real last day.

Why day-of-month and day-of-week are OR'd

If you write 0 9 15 * 1, cron runs at 09:00 on every 15th of the month AND every Monday. Not the intersection. This is the second most common bug after timezone issues. If you need an intersection (e.g., "15th of the month only if it is a Monday"), you need to guard inside the script.

Daylight saving time is a minefield

A cron expression is a pattern matched against wall-clock time. Wall-clock time jumps forward 1 hour in spring and back 1 hour in fall. Concrete fallout:

  • In spring, a job scheduled for 30 2 * on the DST transition day is skipped because 02:30 never happens (clocks jump from 01:59:59 to 03:00:00).
  • In fall, the same job runs twice, once at the first 02:30 and once at the second 02:30 after clocks fall back.

Three ways out:

  1. Run the server in UTC (the default on AWS, GCP, Azure, Fly.io). Then cron times are local to UTC and DST does not exist. You lose intuitive wall-clock mapping but gain correctness.
  2. Schedule jobs for hours that DST does not touch (anything outside 02:00-03:00 in most US zones, anything outside 01:00-02:00 in most EU zones).
  3. Use a scheduler that supports TZ= directives (systemd timers, Kubernetes CronJob with timeZone, Vercel Cron). Then you declare the intended timezone and the scheduler handles the skip-or-double-run problem explicitly.

Kubernetes CronJob added spec.timeZone in v1.27 (stable). Set it:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-report
spec:
  schedule: "30 2 * * *"
  timeZone: "America/New_York"

Cron across the major schedulers in 2026

SchedulerFieldsTimezone supportSeconds?L/W/# ops
Linux crontab5CRON_TZ= envNoNo
systemd timern/aOnCalendar syntaxYesPartial
Kubernetes CronJob5spec.timeZoneNoNo
GitHub Actions5UTC onlyNoNo
Vercel Cron5UTC onlyNoNo
Cloudflare Workers Cron5UTC onlyNoNo
AWS EventBridge6 (year)UTC or namedNoPartial
Quartz / Spring6-7YesYesYes
Temporal SchedulesAnyYesYesN/A (interval API)

GitHub Actions, Vercel Cron, and Cloudflare Cron Triggers all run in UTC with no timezone override as of April 2026. That is not a bug, it is a design choice to keep scheduling deterministic across global infrastructure.

Cron does not guarantee exactly-once

Cron is a best-effort scheduler. If the node is down when the job should fire, most cron implementations do not catch up when the node comes back (anacron does, regular cron does not). If two replicas of your app have cron entries, both will try to run the job.

Three failure modes I have seen in production:

  1. A Kubernetes CronJob had two replicas due to a rolling update window. Both pods ran the nightly billing script. Customers got double-charged for a day.
  2. A GitHub Actions cron fired 6 minutes late because the runner pool was saturated. The job checked "was the previous run less than 4 hours ago?" and no-op'd itself, missing a scheduled send.
  3. A server's clock drifted 40 seconds. The cron at 0 0 * fired at 23:59:20 instead of 00:00, which the downstream system rejected as "too early."

Mitigations:

  • Use a distributed lock (Redis SET NX EX, Postgres advisory lock) if multiple replicas might fire the same job.
  • Make the job idempotent. Write results to a table keyed by (job_name, scheduled_date) and skip if a row already exists.
  • For critical business processes (invoicing, payouts, data exports) use a workflow engine like Temporal that tracks each scheduled run as an entity with retries, timeouts, and deduplication.

When to skip cron entirely

Cron is great for simple, periodic, standalone tasks. It is the wrong tool when:

  • The job takes longer than the interval between runs (pile-up).
  • The job depends on the success of a previous run.
  • The job needs to pause and resume on external events.
  • You need visibility into which runs succeeded, failed, or are still in flight.

For any of those, use a real workflow engine. Temporal, AWS Step Functions, and Vercel Workflow DevKit all offer cron-style schedules plus durable execution, retries, and history. The cost is a bit more setup. The benefit is you can actually answer "did last night's billing run finish?" without grepping logs.

I keep our /tools/cron-builder open when writing new expressions, and /tools/cron-expression when debugging an unfamiliar one. Paste in a pattern, see the next 10 execution times in your timezone, catch mistakes before you deploy.

Sanity checklist before you deploy a cron

  1. Does the scheduler run in UTC, or can you set a timezone? Pick one explicitly.
  2. Does the job tolerate running twice in a row? If not, add a lock.
  3. Does the job tolerate running one hour late? If not, use a stronger scheduler.
  4. Does the job log its start, end, and result somewhere you can query?
  5. Is there an alert if the job does not run for 24 hours?
  6. Is there a manual "run now" path for on-call?
  7. Has someone tested what happens when two runs overlap?

If you cannot answer yes to all seven, you are shipping a future incident.

FAQ

Q: Why does my cron run twice on a single day?

Most common cause: two replicas of your service both have cron entries. Check your deployment for duplicate schedules. Second most common: daylight saving transition in fall. Third: clock drift.

Q: How do I test a cron expression without waiting?

Use next() simulation. Most online tools (ours included) render the next 10 execution times. For server-side testing, libraries like croner (Node) and apscheduler (Python) have a next_fire_time() API.

Q: What timezone does GitHub Actions cron use?

UTC only. There is no timezone override. If you want 09:00 New York time, you have to rewrite your cron twice a year for DST, or accept that the job runs at 13:00 and 14:00 UTC depending on the season.

Q: Can I run cron every 30 seconds?

Standard Linux cron has 1-minute resolution. For sub-minute scheduling use systemd timers, a workflow engine, or just a sleep loop inside a supervised process.

Q: What happens if a cron job is still running when the next one fires?

It depends on the scheduler. Linux cron will start the second job in parallel. Kubernetes CronJob respects spec.concurrencyPolicy (Allow, Forbid, Replace). Vercel Cron has a 10-minute max duration and will not overlap.

Q: How do I catch up a missed cron run?

If the scheduler is cron itself, you cannot. The run is gone. anacron was designed for this (laptops that sleep through the run time) but does not replace cron on always-on servers. For important jobs, build your own catch-up logic: check what the last successful run was, and process everything newer.

The short version

Memorize the seven patterns. Declare your timezone. Add locks for distributed deployments. Log every run. Alert on silence. Move to a real workflow engine the moment cron stops being enough.

Build and preview expressions in /tools/cron-builder, decode an unfamiliar one in /tools/cron-expression, or read /blog/uuid-v4-vs-v7-time-ordered-database-design for how the jobs you schedule interact with your database keys.

References

  • POSIX crontab specification, IEEE Std 1003.1, pubs.opengroup.org, accessed 2026-04-15
  • Linux crontab(5) man page, man7.org, accessed 2026-04-15
  • Kubernetes CronJob documentation, kubernetes.io/docs, accessed 2026-04-15
  • Vercel Cron Jobs documentation, vercel.com/docs, accessed 2026-04-15
  • Quartz Scheduler cron trigger tutorial, quartz-scheduler.org, accessed 2026-04-15
  • Temporal Schedules documentation, docs.temporal.io, accessed 2026-04-15

Related Tools

Want API access + no ads? Pro coming soon.