Dates / Time Debugging workflow

Why Cron Jobs Run at the Wrong Time

Debug scheduled jobs by checking cron field order, timezone assumptions, day-of-week semantics, DST, server location and scheduler-specific syntax.

Quick Answer

Cron jobs usually run at the wrong time because the expression is interpreted in a different timezone, the scheduler uses a different field count, day-of-week rules are misunderstood, or daylight saving time changes the local clock. Check the scheduler documentation and calculate the next runs explicitly.

Example Scenario

A cleanup job should run at 9 AM local time but fires in the middle of the night. Another job runs twice on a daylight-saving transition. A copied cron expression works in one platform and fails in another because one scheduler expects five fields while another includes seconds.

Step-by-Step Explanation

  1. Confirm whether the scheduler uses five, six or seven cron fields.
  2. Identify the timezone used by the scheduler, not just the server timezone.
  3. Calculate the next several run times before deploying.
  4. Check day-of-week and day-of-month interaction for the specific scheduler.
  5. Review daylight saving behavior for local-time schedules.
  6. Prefer UTC for operational jobs unless a business rule requires local time.

Field Count Changes Everything

Traditional cron uses five fields: minute, hour, day of month, month and day of week. Some schedulers add seconds at the beginning. Others add year at the end. Copying an expression between systems without checking field count can shift every value into the wrong position.

For example, 0 9 * * * means 9:00 in a five-field scheduler. In a six-field scheduler with seconds first, it may be invalid or interpreted differently depending on the platform. The expression is not portable until the scheduler format is confirmed.

Always label cron examples with their target system. A bare expression in a ticket is not enough context.

Timezone Is the Most Common Surprise

A scheduler may use UTC, server local time, project time, account time or an explicit timezone attached to the job. The machine running the code is not always the authority. Serverless platforms and managed job systems often default to UTC even when the team thinks in local time.

If a job is off by a fixed number of hours, timezone is more likely than cron syntax. Calculate the next run in UTC and in the intended local timezone. The mismatch usually becomes obvious.

For business workflows such as “send at 9 AM Asia/Shanghai,” store the timezone as part of the job configuration. For infrastructure workflows such as cleanup and metrics aggregation, UTC is usually simpler.

Day Fields Are Scheduler-Specific

The relationship between day-of-month and day-of-week can surprise teams. Some cron implementations treat them with OR behavior, while other schedulers document different rules. A job intended for the first Monday can run more often than expected if the expression is copied without checking semantics.

Names and numbers for weekdays also vary in readability. Sunday can be 0 or 7 in many systems, but not every platform accepts both. Using names like MON can be clearer when the scheduler supports them.

For complex business schedules, a calendar-aware scheduler or application-level rule may be safer than a clever cron expression.

DST Makes Local Time Messy

Daylight saving time can create missing or repeated local times. A job scheduled at 2:30 AM may not run on spring-forward day in some regions. A job scheduled during the repeated hour may run twice on fall-back day depending on the scheduler.

This is not a bug in cron so much as a property of local time. If the business requirement truly depends on local wall-clock time, document what should happen during DST transitions.

If the job does not need local time, use UTC and convert only for reports or human-facing output.

Observed Run Time Needs More Than One Log Line

A single log timestamp can mislead if the logging system displays local time while the scheduler uses UTC. Capture scheduled time, actual start time, timezone and job id together.

Also check queue delays. The cron trigger may fire correctly, but the worker may start late because the queue is backed up or concurrency is limited. That is a scheduling execution problem, not necessarily a cron expression problem.

Record the next few expected runs after every cron change. This gives operations and product teams a concrete schedule to review.

Scheduler Dashboards May Show Localized Time

Managed platforms sometimes display schedule times in the viewer’s browser timezone while executing the job in UTC. Two teammates can look at the same dashboard and describe different visible times if their local timezone settings differ.

When discussing a schedule, write the expression, scheduler timezone and next UTC run time together. UTC gives the team one shared reference point, while local display can be added for business stakeholders.

If a platform lets you configure timezone per job, verify that the setting is deployed with the job and not only selected in a dashboard preview.

What to Check Next

Use the Cron Expression Parser to calculate upcoming runs. Then compare those runs with the platform dashboard. If the parser and platform disagree, the platform likely uses different syntax or timezone assumptions.

For user-facing schedules, write tests around daylight saving transitions and month boundaries. For infrastructure schedules, document that the job uses UTC and avoid local-time language in runbooks.

When in doubt, replace a clever expression with a simple frequent trigger plus application-level checks. Clear code can be easier to review than an expression nobody wants to touch.

After deployment, observe at least the first real run and compare actual start time with the expected schedule. If the job is business-critical, add an alert for missed runs and duplicate runs rather than only alerting when the job code throws.

Include the cron expression in logs. Future investigators should not need dashboard access just to know which schedule produced a run.

For jobs that call APIs, log the scheduled trigger separately from the API result. Otherwise a failed HTTP request can be confused with a missed schedule. Knowing that the job triggered on time but the downstream API returned 429, 500 or invalid JSON sends debugging to the right layer. Keep both records linked by one job run id and environment. Include retry count too.

Code Examples

Five-field daily 09:00 cron
# minute hour day-of-month month day-of-week
0 9 * * *
Log schedule context together
console.log({
  job: 'daily-cleanup',
  schedulerTimezone: 'UTC',
  triggeredAt: new Date().toISOString(),
  expression: '0 9 * * *'
});
Guard business logic inside a frequent job
if (nowInZone('Asia/Shanghai').hour !== 9) {
  return;
}

await sendDailyDigest();

Common Mistakes

  • Copying a five-field expression into a scheduler that expects seconds.
  • Assuming the server timezone controls the scheduler.
  • Ignoring daylight saving behavior for local-time jobs.
  • Misunderstanding day-of-month and day-of-week interaction.
  • Blaming cron when queue delay caused late execution.

FAQ

Why is my cron job off by eight hours?

The scheduler probably uses UTC while you expected a local timezone, or the dashboard displays time differently.

Are all cron expressions portable?

No. Field count, names, special characters and timezone behavior vary by scheduler.

Should I schedule jobs in UTC?

For operational jobs, usually yes. Business wall-clock jobs may need an explicit local timezone.

Can daylight saving time skip a run?

Some schedulers skip or duplicate local-time runs during DST transitions. Check the platform behavior.