Use IANA Time Zones, Never Offsets
The timezone field must contain an IANA database name such as America/New_York or Europe/London. These names encode daylight-saving transitions and historical changes. Fixed offsets and abbreviations such as +05:00 or EST produce incorrect firings twice a year when rules shift.
Kubernetes CronJobs accept a top-level timeZone field. GitHub Actions accepts an IANA string in the schedule.timezone property. Both evaluate the cron expression against the named zone rather than the controller's local clock. POSIX crontab implementations rely on the TZ environment variable or system zone, but the same IANA names remain the only portable choice.
- Confirm the zone name exists in the tz database before deployment.
- Test spring-forward and fall-back transitions with a short schedule that crosses the change.
- Never embed CRON_TZ or TZ inside the schedule string; Kubernetes rejects such syntax.
Define an Overlap Policy
An overlap occurs when a new scheduled time arrives while the previous run remains active. The overlap_policy field decides the outcome. The safest default for idempotent work is skip: the new occurrence is dropped, next_run_at advances, and a cron.skipped event is emitted.
Kubernetes offers Allow (default), Forbid, and Replace. Forbid behaves like skip. Replace terminates the running job and starts a fresh one. GitHub Actions relies on concurrency groups and cancel-in-progress to achieve similar control. Choose the policy that matches the job's side effects rather than relying on the platform default.
- Document why the chosen policy is safe for the specific task.
- Emit an event or log line on every skip so operators notice lost runs.
- Avoid Allow for long-running or resource-heavy jobs.
Track Execution History
System-managed fields give operators visibility without waiting for the next trigger. next_run_at shows the upcoming execution. last_run_at and run_count reveal whether triggers were missed. created_at supports auditing of schedule changes.
When the scheduler wakes after a pause or clock skew, it must fire at most one catch-up occurrence per job and advance next_run_at to the next future time. This prevents a flood of missed runs from overwhelming the system.
- Alert when last_run_at is older than next_run_at minus the expected interval.
- Store run history for at least the length of the longest schedule cycle.
- Include the cron expression and time zone in every log line for correlation.
Handle Missed Runs and Deadlines
Kubernetes CronJobs use startingDeadlineSeconds to decide how late a job may start before it is counted as missed. Values under ten seconds can prevent scheduling entirely. When the limit is exceeded, the controller skips the instance and continues with future occurrences.
If more than one hundred schedules are missed, the CronJob controller stops creating jobs and logs an error. GitHub Actions evaluates schedules on the default branch and skips runs during high load; the five-minute minimum interval remains the shortest supported cadence.
- Set startingDeadlineSeconds only when a late run would be useless or harmful.
- Monitor the too many missed start times error and investigate clock skew or leader pauses.
- Design every job to be idempotent so a single catch-up run produces the correct state.
Add Locks, Logs, and Monitoring
Application-level locks prevent duplicate work when the scheduler itself allows concurrency. Record every decision—start, skip, replace—in structured logs that include the scheduled time, actual start time, and outcome. These logs become the audit trail when investigating missed or duplicate runs.
Combine scheduler fields with external monitoring. A simple query that compares last_run_at against the expected interval catches silent failures faster than waiting for downstream data to look wrong.
- Use a distributed lock only when the overlap policy alone is insufficient.
- Expose metrics for skipped runs, active duration, and last successful completion.
- Test the entire loop after a simulated clock change or process pause.