# Your AI Agent Scheduler Needs a Clock-Skew Budget, Not Just Cron

> Source: <https://dev.to/zira125/your-ai-agent-scheduler-needs-a-clock-skew-budget-not-just-cron-mck>
> Published: 2026-08-19 03:55:58+00:00

A scheduler can be perfectly healthy and still run the wrong job at the wrong time.

The failure is usually not the cron expression. It is the boundary between wall-clock time, monotonic elapsed time, leases, retries, and a process that may pause or restart.

A reliable agent scheduler needs an explicit clock contract. Without one, a clock correction can make a job run twice, never run, or run after its authorization window has expired.

Use wall-clock time for human meaning and durable records:

Use a monotonic clock for elapsed-time decisions inside one process:

Use a database or provider sequence for ordering across processes:

A monotonic timestamp cannot be compared across hosts, and a wall-clock timestamp cannot safely measure a five-minute lease if NTP steps the clock backward. Store both kinds of evidence instead of pretending one timestamp answers every question.

Here is a deliberately boring record shape:

```
action: send_digest
run_id: 01J...
scheduled_at: 2026-08-19T08:00:00Z
not_before: 2026-08-19T08:00:00Z
expires_at: 2026-08-19T08:05:00Z
lease_owner: worker-7
lease_token: 1842
attempt: 1
state: READY
```

The important part is not the field names. It is the decision rule:

That last step matters after restarts. A clean restart is not proof that the previous effect did not happen. I covered the effect-side version of this problem in [restart-safe agent deduplication](https://dev.to/zira125/your-ai-agent-restarted-cleanly-why-did-it-run-the-job-twice-3p9d).

A clock-skew budget is the maximum uncertainty you will tolerate between the clock used to schedule a run and the clock used to authorize dispatch.

For example:

Do not silently turn the budget into a larger retry window. If the observed offset exceeds the budget, stop dispatching new work or move runs to CLOCK_UNCERTAIN. Existing in-flight work needs its own lease and effect policy.

A simple gate can look like this:

```
action = now_wall < run.not_before
expired = now_wall >= run.expires_at
clock_uncertain = abs(host_offset) > CLOCK_SKEW_BUDGET

if clock_uncertain:
    return CLOCK_UNCERTAIN
if action:
    return NOT_READY
if expired:
    return EXPIRED
return DISPATCHABLE
```

The ordering is intentional. A scheduler should not dispatch merely because a job is due if the host's clock is outside the authority's accepted uncertainty.

Test at least these cases:

| Fault | Unsafe symptom | Safer result |
|---|---|---|
| Clock jumps backward | due work appears early or leases live too long | use monotonic lease timers and hold new dispatch |
| Clock jumps forward | future work runs immediately or expires | reject dispatch outside the freshness window |
| NTP becomes unavailable | stale schedule decisions continue silently | enter CLOCK_UNCERTAIN with an alert |
| Worker pauses during a lease | two workers perform one effect | fencing token rejects the stale worker |
| Scheduler restarts at a boundary | a run is lost or duplicated | reload durable state and reconcile by run ID |
| DST or timezone conversion changes | local-time jobs shift unexpectedly | store UTC plus the original schedule zone |

A useful failure-injection test does not just mock now(). Pause a worker after it claims a lease, advance the authority clock, start a replacement worker, and then let the old worker attempt the effect. The expected result is a rejected stale token, not a second provider call.

Human schedules may be expressed as “every weekday at 09:00 Europe/Berlin.” Convert that schedule to an unambiguous UTC occurrence before creating the run record. Persist the timezone and the resolved occurrence together.

Do not let a browser session, email worker, or MCP tool reinterpret the local schedule. By the time work reaches an effect boundary, it should carry a concrete run ID, expiry, authority version, and idempotency key.

This also makes audits possible. When a user asks why a run happened at 08:00 UTC, you can distinguish:

Before running an always-on agent scheduler, verify:

If you need a managed always-on runtime for an OpenClaw or browser-based agent, [managed agent hosting on Ampere](https://ampere.sh/?utm_source=devto&utm_medium=article&utm_campaign=scheduler-clock-skew) is one option to evaluate. Hosting can keep a process running, but it does not define the clock contract, lease semantics, effect idempotency, or recovery policy. Those still belong in the application.

The practical lesson is simple: cron tells you when to try. A clock contract tells you whether the attempt is still authorized, current, and safe.

If this kind of control-plane detail is useful, follow for practical agent reliability patterns rather than model demos.
