Replace Cron with Temporal for Reliable Data Jobs

Cron fails on retries, overlaps, and writes due to zero observability. Temporal workflows add retries (3s initial, 2x backoff, 8 max attempts), atomic writes, unique output files per run ID, SKIP overlap policy, and full execution history via UI—surviving crashes with state in Temporal.

Cron's Silent Failures Demand Better Orchestration

Cron provides one bit of feedback—exit zero or non-zero—leaving retries, overlaps, and data integrity to manual hacks. In a 15-line MLB stats fetch script run nightly at 2am, three failures emerge: (1) requests.raise_for_status() exits on 429 rate limits or timeouts without retry, causing stale data (e.g., 9 missed runs led to dropping a hot player); (2) fixed latest.json output creates races if runs overlap (slow fetch > schedule interval); (3) non-atomic write_text() corrupts files on mid-write crashes (OOM, signals). Patching with loops bloats code, loses state on crashes, and forces log spelunking for history. Outcome: unreliable data for decisions, no audit trail for "what ran at 3am Tuesday?"

Temporal eliminates this by separating orchestration (Workflows: deterministic, own when) from side effects (Activities: fetch/parse/write). State persists in Temporal's history, not process memory, ensuring completion despite reboots.

Workflows + Activities Deliver Crash-Proof Reliability

Define a StatsCollectionWorkflow that calls collect_stats activity with start_to_close_timeout=timedelta(minutes=10) and RetryPolicy(initial_interval=timedelta(seconds=3), backoff_coefficient=2.0, maximum_interval=timedelta(minutes=2), maximum_attempts=8). Retries survive worker crashes—e.g., die on attempt 3, resume at 4. Activity fetches MLB page (proxies optional via env vars for 429s/geo-blocks), extracts statsDatatable JSON via string search (needle='stats: {"statsDatatable"'), sanitizes HTML tags, picks current season row, and writes atomically: tmp file + replace() prevents partial JSON. Filename uses workflow_id__run_id.json (e.g., stats-manual-abc123__run456.json), enabling diffs across runs and eliminating races.

Sync activities (not async) suit blocking I/O like requests.get(timeout=60); they run in thread pools without blocking event loops. Workers scale horizontally, polling task_queue without touching scheduling.

Schedules and UI Provide Production-Grade Control

Schedule with cron_expressions=[cron], ScheduleOverlapPolicy.SKIP prevents overlaps—if a 12min run bleeds into a 15min schedule, next tick skips until free. Idempotent create/update: describe(), catch NOT_FOUND, then create_schedule or update. Local dev: temporal server start-dev, uv run temporal-cron-worker, uv run temporal-cron-schedule (default 15min cron).

UI at localhost:8233 shows timelines: inputs/outputs per attempt, retry details (e.g., 429 on #2, success #3), full event history (schedule, activity start/complete, results). Replaces stdout guessing with searchable audits—debug failures without logs.

Production: Use Temporal Cloud/self-host, add secrets/logging/metrics. Pairs with proxies (Bright Data) for flaky networks; Temporal owns retries/timeouts, proxy hardens paths. Pattern scales to work ingest jobs: same Workflow/Activity for more surface area.

Summarized by x-ai/grok-4.1-fast via openrouter

8363 input / 2075 output tokens in 37734ms

© 2026 Edge