Right Now
Automation 7 min read

Why we replaced the cron box with Durable Functions

Nearly every automation engagement we take on begins at the same place: a virtual machine, a scheduled task, and a Python script that has been quietly load-bearing for three years. It cost almost nothing to build. Replacing it is one of the highest-return pieces of work available to most organisations, and this is why.

The cron box

The pattern is familiar. Someone needed a nightly job. A virtual machine already existed, so the script went there, with a schedule and an email at the end. It worked. Over the following two years it grew four more responsibilities, a configuration file, and a dependency on a path that only exists on that machine.

The script is usually fine. What is missing is everything around it: any record of what ran, any way to re-run one failed item out of nine thousand, and any signal at all when it does not run. The failure mode is not a crash. It is a silence that nobody notices for a week.

What actually breaks

Across the migrations we have done, the same five problems come up:

  1. Partial completion. The job died at item 4,000 of 9,000. Nobody knows which 4,000 succeeded, so the safe option is to run the whole thing again and hope it is idempotent. It rarely is.
  2. Invisible failure. The alerting is an email on success. No email is indistinguishable from an email nobody read.
  3. Untracked state. Progress lives in memory, so the process cannot be resumed, only restarted.
  4. Unreproducible environment. The dependencies were installed by hand in 2023. Rebuilding the box is an afternoon of archaeology.
  5. Single owner. One person knows the running order, and their leave calendar is now a business risk.

The useful reframing

The interesting state is not inside the script. It is the orchestration around it: which items were attempted, which succeeded, what happens to the ones that did not, and who is told. That state deserves to be durable, and most cron scripts keep it nowhere at all.

Replayable orchestration

Durable Functions (Azure's orchestration layer over Functions) makes that state the platform's responsibility. An orchestrator function describes the workflow; each yield is a checkpoint written to storage. If the host restarts, the orchestration is replayed from history and resumes at the last checkpoint rather than the beginning.

@app.orchestration_trigger(context_name="ctx")
def reconcile(ctx):
    batch = yield ctx.call_activity("fetch_ledger")

    # Fan out: each item is checkpointed independently.
    results = yield ctx.task_all([
        ctx.call_activity("match_entry", row) for row in batch
    ])

    unmatched = [r for r in results if not r.matched]
    if unmatched:
        yield ctx.call_activity("queue_for_review", unmatched)

    return {"matched": len(results) - len(unmatched),
            "needs_human": len(unmatched)}

Three properties come almost free from this shape, and they are the three the cron box lacked:

  • Per-item recovery. The fan-out means one failing entry is one failing activity, retried on its own schedule, not a reason to re-run the batch.
  • An audit trail by construction. The orchestration history is the record of what happened, queryable after the fact without anyone having added logging for it.
  • An explicit human path. queue_for_review is not an error handler. It is a designed outcome: the system knows the difference between "failed" and "not confident", and only the second one involves a person.

There is a real constraint in exchange. Orchestrator code must be deterministic, because it is replayed: no clock reads, no random values, no direct input/output. Anything non-deterministic belongs in an activity. This catches every team once, and the error message is not gentle.

What it costs you

On consumption pricing, the workflows we build for this kind of job usually land in single-digit dollars per month, commonly less than the always-on virtual machine they replaced, which was billed twenty-four hours a day to do twenty minutes of work.

The genuine cost is conceptual. Somebody on your team needs to understand replay semantics and idempotency, and that is a real learning curve rather than a switch to flip. We budget for the handover session accordingly.

When cron is still right

We are not against scheduled scripts. If a job is short, has a single step, is safe to run twice, and someone is genuinely watching, then a scheduled task is the correct amount of engineering. Adding an orchestration framework to that would be a cost with no return.

The question we ask instead is simple: if this job silently stopped running tonight, when would you find out, and how? If the answer involves a customer, the cron box has already outgrown itself.


Questions or disagreement are welcome: info@rdrightnow.com. We would rather be corrected than quoted.

More notes

Applied AI9 min read

Scoring people fairly: notes from a ranking system we shipped

Read article
Cloud8 min read

A cost-aware Azure landing zone

Read article

Get started

Working on something similar?

We publish because writing it down is how we find the gaps. Tell us where ours are.