Developer guide
Cron Timezone: Why Your Job Runs at the Wrong Hour
“It ran at 3 AM and I asked for 9” is one of the most common cron bugs in the world — and it is almost never the expression. The five fields are numbers; the timezone is a separate setting that each platform treats differently. Here is the exact default and the exact fix for crontab, Kubernetes, GitLab, GitHub Actions, node-cron, APScheduler and n8n.
Written by Benjamin Rotshtein
Updated
What timezone does cron actually use?
The system crontab runs in the server's local time, which is UTC on most cloud VMs and containers. Managed platforms differ: Kubernetes evaluates in the controller's zone, while GitLab and GitHub Actions always use UTC. Each has a different setting — CRON_TZ, timeZone, or an env var — shown below.
- What timezone does cron run in?
- The system crontab runs in the server's local time — which on most cloud VMs and containers is UTC. You schedule in UTC unless you change it.
- How do I fix it on Kubernetes?
- Add
timeZone: 'Europe/Berlin'to the CronJob spec (k8s 1.27+). - How do I fix it in GitLab / GitHub Actions?
- Both evaluate the cron expression against UTC; set the TZ environment variable inside the job so your scripts compute the right local time.
Why the expression is not the problem
A line like 0 9 * * 1-5 is exactly “weekdays at 9:00.” The missing half is 9:00 in which timezone? The system daemon applies the server's wall clock. On a container built from a slim image, /etc/timezone is UTC and the TZ env var is unset, so “9 AM” fires at 9 AM UTC — 9 hours early for a user in India.
The platform-by-platform cheat sheet
| Platform | Default | How to set it |
|---|---|---|
| Linux / macOS crontab | Server local time (often UTC on VMs) | Prefix a line with CRON_TZ=Europe/Berlin on cronie; otherwise schedule in UTC or set TZ for the job |
| Kubernetes CronJob | UTC | timeZone: 'Europe/Berlin' field in the CronJob spec (k8s 1.27+) |
| GitLab CI schedules | UTC | variables: TZ: 'Asia/Kolkata' on the job, or shift the cron expression to UTC |
| GitHub Actions schedule | UTC | cron in UTC + env: TZ: 'Europe/Berlin' inside the workflow |
| node-cron | Server time | Cron.schedule(expr, fn, { timezone: 'Asia/Kolkata' }) |
| APScheduler (Python) | Server time | CronTrigger(..., timezone='Asia/Kolkata') or a TimezoneInfo |
| n8n Cron trigger | UTC | Pick the timezone from the dropdown on the Cron trigger node |
Kubernetes CronJob timezone
Since Kubernetes 1.27 the CronJob spec accepts a top-level timezone. A backup that must run at 2 AM in Europe/Berlin looks like:
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-backup
spec:
schedule: "0 2 * * *"
timeZone: "Europe/Berlin"
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: my-backup-image
restartPolicy: OnFailureOn clusters older than 1.27 the field is ignored, so pair it with TZ in the container env and compute the schedule in UTC instead.
GitLab CI and GitHub Actions
GitLab pipeline schedules and GitHub Actions schedule triggers both evaluate the cron expression against UTC. You have two options: write the expression in UTC (simplest), or keep the expression local and set the timezone inside the job so the script itself uses local time. For GitLab:
job:
variables:
TZ: "Asia/Kolkata"
script:
- date
- ./daily-report.shFor GitHub Actions, add TZ to the workflow env:
on:
schedule:
- cron: "0 9 * * 1-5" # 9:00 UTC
env:
TZ: "Europe/Berlin"node-cron, APScheduler and n8n
In-process schedulers give you a timezone argument, which is the cleanest fix because it travels with the code:
// node-cron
import cron from "node-cron";
cron.schedule("0 9 * * 1-5", runReport, { timezone: "Asia/Kolkata" });# APScheduler from apscheduler.triggers.cron import CronTrigger trigger = CronTrigger(day_of_week="mon-fri", hour=9, timezone="Asia/Kolkata")
In n8n, open the Cron trigger node and pick the timezone from the dropdown — it defaults to UTC and this is the most common reason n8n workflows fire at the wrong hour.
DST and the twice-a-year surprise
A fixed wall-clock time like “every day at 2:00 AM Europe/Berlin” behaves differently across a daylight-saving transition: it may skip, run twice, or land on a nonexistent hour. Cron implementations differ — some skip the missing hour, some run on it. For irreversible jobs (billing, deletions) prefer running in UTC, which has no DST at all. The DST and cron guide goes deeper into this trap.
Generate a valid expression
Build your cron expression in plain English — the Cron Generator produces the correct 5-field string and previews the next runs in your timezone, so “9:30 AM weekdays” never becomes a 3 AM surprise.
Frequently asked questions
What timezone does cron use?
The system crontab runs in the server's local timezone, which is often UTC on modern containers and cloud VMs. Cron only sets TZ for the job's child process if the daemon explicitly passes it (cronie supports a CRON_TZ line; most others run the job in the server's timezone).
How do I set a timezone in a Kubernetes CronJob?
Add a top-level timeZone field in the CronJob spec (stable since Kubernetes 1.27), for example timeZone: 'Europe/Berlin'. On older clusters, set TZ in the container env and use the Job command to compute the schedule, or run a small sidecar that compares local time.
How do I set the timezone in GitLab CI?
GitLab schedules are configured in UTC by default. Either set the TZ variable for the job (variables: TZ: 'Asia/Kolkata') so your script uses the right local time, or add/subtract hours in the cron expression to hit the intended UTC wall-clock.
How do I set the timezone in GitHub Actions?
Use cron: '30 9 * * 1-5' with the cron option and set the TZ environment variable in the workflow env block so scripts inside the job run in your timezone. The schedule itself is evaluated against UTC, so express the desired time in UTC.
Does node-cron or n8n support a timezone?
node-cron accepts a timezone option (Cron.schedule(expr, fn, { timezone: 'Asia/Kolkata' })). n8n Cron trigger has a timezone dropdown that applies to the schedule. Both default to UTC on servers configured that way.
Why does my cron job run 5.5 hours early or late?
That exact offset is the signature of a UTC vs IST (UTC+5:30) mismatch: the expression is evaluated in UTC while you expect Indian wall time. Set CRON_TZ or the platform timezone to Asia/Kolkata (or your IANA zone), or shift the hours in the expression to the UTC equivalent.