Cron tools › Convert
Cron time zone converter
Your job should run at 09:00 in Berlin but the scheduler runs in UTC. This rewrites the cron line for the scheduler's zone, splits it into one version per daylight-saving period with the dates to switch, and proves each version by comparing real instants: the original in its zone against the rewrite in the other.
Rewritten schedule
Try one
How to convert a cron schedule to another time zone
- Type the schedule as you mean it —
0 9 * * 1-5for 09:00 on weekdays — and pick the zone it is meant in. - Pick the zone the scheduler actually uses (UTC for GitHub Actions without
timezone:, EventBridge rules, Vercel and Cloudflare Workers). - Choose the period to cover: the next 12 months or a calendar year. Clock changes inside it split the result into sets.
- Use each set during the dates printed above it, and swap the crontab lines on the change-over dates.
- Read the check under each set: every run in those dates was compared as an instant with the original schedule in its own zone.
Why one line is not enough
Berlin is UTC+01:00 in winter and UTC+02:00 in summer. 09:00 in Berlin is therefore 08:00 UTC from late October to late March and 07:00 UTC in between — no single UTC cron line is right all year. The converter finds every moment either zone changes its offset within the period, rewrites the schedule once per distinct offset, and prints the date ranges each version is valid for. Zones without daylight saving (Asia/Tokyo, Asia/Kolkata) need one line.
When the scheduler can hold a time zone itself, that is simpler than any rewrite:
| Scheduler | Time zone setting |
|---|---|
| cronie | CRON_TZ=Europe/Berlin above the lines |
| Debian cron | no per-user time zones; TZ only affects the commands — rewrite, or change the system zone |
| Kubernetes CronJob | .spec.timeZone (stable since v1.27) |
| GitHub Actions | timezone: next to cron: (since 2026-03-19); UTC otherwise |
| AWS | EventBridge rules: always UTC. EventBridge Scheduler: --schedule-expression-timezone |
| Google Cloud Scheduler | timeZone field, default UTC |
| GitLab pipeline schedules | per-schedule time zone |
| Vercel, Cloudflare Workers | always UTC |
When the date moves too
Shifting the hour can cross midnight, and then the day moves with it. The converter handles each case exactly:
- Day of week shifts one for one: 00:30 on Monday in Berlin is 23:30 on Sunday in UTC, so
1becomes0. - Day of month shifts too, but at a month boundary the result depends on the month. The 1st at 01:00 in Berlin
summer time is 23:00 UTC on the last day of the previous month — the 30th, 31st, 28th or 29th. Unix cron cannot name "the
last day", so that line runs on the 28th to the 31st and tests
date -d tomorrow; schedulers withL(Quartz, Spring, AWS) can say it directly. - The other way round can be exact without tricks: in Berlin winter time, 23:00 UTC on the 31st is 00:00 on the
1st of the months that follow a 31-day month, which cron writes as
0 0 1 1,2,4,6,8,9,11 *. - A few shapes cannot be rewritten at all — a line whose day fields combine with AND, or a date that lands differently in leap years. The page says so and points to the zone setting instead.
FAQ
Should I rewrite the schedule or set a time zone?
Set the zone whenever the scheduler supports it (table above): one line that follows the zone's own clock changes, and readable by the next person. Rewrite only for schedulers that are UTC-only or cannot take a zone, such as EventBridge rules, Vercel, Cloudflare Workers or Debian's cron.
What happens on the day the clocks change?
A time inside the skipped hour does not exist that day, and a time inside the repeated hour exists twice. cronie runs a skipped fixed-time job straight after the jump and avoids running it twice when the clock goes back (for time changes of less than three hours). A rewritten UTC line cannot copy that, so the check marks those one or two runs as clock-change differences instead of hiding them.
Why does my job on the 1st end up on the 31st?
Because the rewrite moved it across midnight. 00:30 on the 1st in a zone ahead of UTC is still the previous day in UTC — the last day of the previous month. The converter writes that as a guarded line (or a month list when the result is fixed) and checks it.
What does the "one line all year" option do?
For a schedule at one hour (or one block of hours), it runs at every hour the job could fall on in the scheduler's zone
and lets the command through only when TZ=… date +%H shows the intended hour in the original zone. It is shown
only when that check passes for every run in the period.