You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We want to discuss standardizing CRON schedules on UTC and removing (or deprecating) the per-job timezone parameter. The cron expression should always be interpreted as UTC, and we explicitly do not want to rewrite/offset the cron string itself to encode a timezone.
This came out of #38, which fixed a deserialization failure on starts_at/ends_at and surfaced that the timezone parameter isn't doing what it appears to.
Current state
The per-job timezone (cron_timezone) is effectively cosmetic today, and in a misleading way:
Firing is delegated to pg_cron.db::jobs::build_cron_command + cron.schedule(name, cron_expression, command) (crates/common/src/db/jobs.rs) hand the raw cron_expression to pg_cron. pg_cron evaluates it in its own cluster-global timezone (cron.timezone, default GMT) — it never sees the per-job cron_timezone.
timezone is used in exactly one place: the API handler's compute_next_cron (crates/api/src/handlers/jobs.rs), purely to populate cron_next_run_at at create/update time.
cron_next_run_at is write-once. The worker never recomputes it (no references to cron_next_run_at in crates/worker), so it's a stale snapshot after the first tick.
Net effect: a job with timezone: Asia/Kolkata, cron 0 9 * * *reportsnext_run_at as 09:00 IST but actually fires at 09:00 UTC (14:30 IST). The parameter promises local-time scheduling we don't deliver.
Proposed direction
Treat the cron expression as UTC always.
Do not rewrite the cron string to bake in an offset (e.g. turning 0 9 * * * into 30 3 * * *). String rewriting is brittle — it breaks across DST transitions, day/month rollovers, and field ranges/steps (0 9-17 * * MON-FRI), and it makes the stored expression no longer match what the user typed.
Use starts_at/ends_at as absolute UTC window bounds (this is already how cron_ends_at gates firing in pg_cron).
Deprecate the timezone parameter explicitly rather than silently: it currently sets an expectation (local-time, DST-aware recurrence) the system does not honor.
Trade-off to acknowledge
timezone and starts_at/ends_at are not substitutes for each other:
timezone would define what wall-clock the recurring cron fields mean and is inherently DST-aware for a recurring rule ("09:00 local every day" as the UTC offset shifts twice a year).
starts_at/ends_at are two absolute instants bounding when the schedule is active. You cannot reconstruct a DST-aware recurring rule from two instants.
So going UTC-only is a deliberate capability cut: we lose DST-correct local recurring schedules (e.g. "09:00 business hours in New York"). The decision here is that we'd rather have a simple, honest, consistent UTC model than carry a half-implemented timezone feature. Users who need local times convert to UTC at author time and accept fixed-offset behavior.
Scope of work (for discussion, not committing yet)
Decide: hard-remove timezone vs. deprecate-and-ignore (with a clear API warning/docs note).
Stop requiring timezone for CRON jobs in handlers::jobs::create (currently returns 400 when absent).
Recompute / stop persisting a misleading cron_next_run_at (compute in UTC, and ideally refresh it on tick, or drop it as a stored field).
Migration for existing jobs that carry a non-UTC cron_timezone (leave as metadata? backfill? document behavior change).
Update the dashboard job form (crates/dashboard/src/pages/workspace_detail.rs) and API docs/smithy model to reflect UTC-only semantics.
Alternative (rejected for now)
Make timezone authoritative — honor it in pg_cron (per-schedule timezone support / session TZ) and recompute next-runs DST-aware. Strictly more capable, but more moving parts and contrary to the "keep cron UTC, don't format the cron string" preference. Captured here so the trade-off is on record.
Summary
We want to discuss standardizing CRON schedules on UTC and removing (or deprecating) the per-job
timezoneparameter. The cron expression should always be interpreted as UTC, and we explicitly do not want to rewrite/offset the cron string itself to encode a timezone.This came out of #38, which fixed a deserialization failure on
starts_at/ends_atand surfaced that thetimezoneparameter isn't doing what it appears to.Current state
The per-job
timezone(cron_timezone) is effectively cosmetic today, and in a misleading way:db::jobs::build_cron_command+cron.schedule(name, cron_expression, command)(crates/common/src/db/jobs.rs) hand the rawcron_expressionto pg_cron. pg_cron evaluates it in its own cluster-global timezone (cron.timezone, default GMT) — it never sees the per-jobcron_timezone.timezoneis used in exactly one place: the API handler'scompute_next_cron(crates/api/src/handlers/jobs.rs), purely to populatecron_next_run_atat create/update time.cron_next_run_atis write-once. The worker never recomputes it (no references tocron_next_run_atincrates/worker), so it's a stale snapshot after the first tick.Net effect: a job with
timezone: Asia/Kolkata, cron0 9 * * *reportsnext_run_atas 09:00 IST but actually fires at 09:00 UTC (14:30 IST). The parameter promises local-time scheduling we don't deliver.Proposed direction
0 9 * * *into30 3 * * *). String rewriting is brittle — it breaks across DST transitions, day/month rollovers, and field ranges/steps (0 9-17 * * MON-FRI), and it makes the stored expression no longer match what the user typed.starts_at/ends_atas absolute UTC window bounds (this is already howcron_ends_atgates firing in pg_cron).timezoneparameter explicitly rather than silently: it currently sets an expectation (local-time, DST-aware recurrence) the system does not honor.Trade-off to acknowledge
timezoneandstarts_at/ends_atare not substitutes for each other:timezonewould define what wall-clock the recurring cron fields mean and is inherently DST-aware for a recurring rule ("09:00 local every day" as the UTC offset shifts twice a year).starts_at/ends_atare two absolute instants bounding when the schedule is active. You cannot reconstruct a DST-aware recurring rule from two instants.So going UTC-only is a deliberate capability cut: we lose DST-correct local recurring schedules (e.g. "09:00 business hours in New York"). The decision here is that we'd rather have a simple, honest, consistent UTC model than carry a half-implemented timezone feature. Users who need local times convert to UTC at author time and accept fixed-offset behavior.
Scope of work (for discussion, not committing yet)
timezonevs. deprecate-and-ignore (with a clear API warning/docs note).timezonefor CRON jobs inhandlers::jobs::create(currently returns400when absent).cron_next_run_at(compute in UTC, and ideally refresh it on tick, or drop it as a stored field).starts_at/ends_atas UTC instants consistently (already the case after fix(api): accept datetime-local timestamps on job create/update #38; confirm and document).cron_timezone(leave as metadata? backfill? document behavior change).crates/dashboard/src/pages/workspace_detail.rs) and API docs/smithy model to reflect UTC-only semantics.Alternative (rejected for now)
Make
timezoneauthoritative — honor it in pg_cron (per-schedule timezone support / session TZ) and recompute next-runs DST-aware. Strictly more capable, but more moving parts and contrary to the "keep cron UTC, don't format the cron string" preference. Captured here so the trade-off is on record.References
crates/common/src/db/jobs.rs—build_cron_command,register_pg_cron.crates/api/src/handlers/jobs.rs—compute_next_cron, CRON create/update path.crates/common/src/models/job.rs—CreateJob/UpdateJob(timezone,starts_at,ends_at).