Durable Execution: Why Python while Loops Are Not Production Ready

A while loop is a state machine whose state lives in RAM. One pod eviction and every completed step is gone. Production agentic workflows require exactly-once semantics — a guarantee that must be encoded in the architecture, not hoped for through retry logic.

A process cannot make a promise. It can perform a computation. The promise — that an operation will complete exactly once, that a commitment will survive a system failure mid-execution — requires something a process lacks: durable storage that persists independent of the process's lifetime.

This is not a reliability concern addressable with better retry logic. It is a structural property of the execution model. A while loop holds its execution state in the call stack. A call stack lives in RAM. RAM is ephemeral by design — which is why the event-driven operator pattern checkpoints state to durable storage before returning to idle. RAM is ephemeral by construction — the operating system makes no commitment to preserve it across process boundaries, and it keeps that non-commitment precisely.

TL;DR — Key Takeaways:
  • A while loop is a state machine whose state lives in RAM. One pod eviction, OOM kill, or rolling deploy and every completed step is gone — the workflow restarts from step zero.
  • Production workflows require exactly-once execution semantics for side-effectful steps. A retry loop implements at-most-once across process boundaries — incorrect for any operation that must not be duplicated.
  • Durable execution engines (Temporal, AWS Step Functions, Restate) persist a journal of completed steps before advancing. Recovery replays the journal — it does not re-execute the operations that produced those results.
  • The distinction between journal replay and re-execution is load-bearing: re-execution charges the payment again. Journal replay reads "step 2 completed — idempotency key charge-7d3f" from storage and fast-forwards. The charge does not recur.
  • Retrofitting durable execution after a naive loop fails in production costs 3–5× more — a concrete instance of the iron triangle binding than adopting it at the start. The failure mode compounds with task complexity and does not announce itself until it becomes an incident.

The Three Delivery Semantics

Distributed systems literature identifies three delivery guarantees for operations that cross failure boundaries. Every workflow implements one of them — explicitly or by default:

At-most-once: Fire and forget. If the process dies before confirmation, the operation may not have occurred. Acceptable for metrics emission or append-only logging where loss is tolerable and duplication is harmless.

At-least-once: Retry on failure until confirmed. The operation will eventually succeed, but may be applied multiple times. Safe only for idempotent operations where re-application produces no new effect — reading from a database, querying a status endpoint.

Exactly-once: The operation completes exactly once regardless of how many retries the infrastructure requires. This needs two components in combination: idempotency on the receiver (a deduplication key that makes re-delivery a no-op) plus durable state on the sender (a record of which operations already completed, survives process death).

A while loop with a retry decorator implements at-least-once semantics within a single process lifetime — and at-most-once across process restart boundaries. If the process dies mid-loop, the current iteration is lost. If the process restarts, the loop re-runs from step one. The combination produces neither reliable completion nor reliable non-duplication. For charging a payment, writing a financial record, or sending a notification that must not be duplicated, this is the wrong guarantee — delivered confidently and invisibly.

The Journal and the Replay Distinction

Durable execution implements exactly-once semantics via a journal: an append-only log of completed steps and their outputs. Before a workflow engine advances past a step, it writes that step's completion to durable storage. On recovery after any failure, the engine replays the journal — it reads each completed entry, reconstructs the workflow's position, and resumes from the first unresolved step. The side effects of completed steps do not recur.

This is the architectural distinction that matters. Re-execution re-runs the operation: the payment API call fires again, the database write executes again, the notification sends again. Journal replay reads the recorded result of the operation from storage and presents it to subsequent steps as input, without triggering the operation itself. The workflow advances. The side effect does not recur.

Failure topology — naive retry loop vs. durable execution under process death
Figure 1: Failure topology — naive retry loop vs. durable execution under process death

This pattern is not novel in systems design. It is event sourcing applied to workflow execution history. The insight — which underpins Apache Kafka's log-based architecture and the stream processing work Martin Kleppmann documents extensively in Designing Data-Intensive Applications — is that the log is the state. The canonical record of what happened, in order, from which any derived state can be reconstructed. Durable execution engines apply the same principle to agent workflows: the journal is the workflow state, and the running process is a stateless consumer of that journal. Replace the process; lose no progress.

The architectural history extends further. The Saga pattern — long-lived transactions decomposed into local transactions with compensating operations for rollback — was formalized by Hector Garcia-Molina and Kenneth Salem in 1987, addressing the same structural problem: how to provide failure-recovery guarantees for operations that span minutes rather than milliseconds without holding locks for the full duration. Temporal, AWS Step Functions, and Restate are implementations of this pattern extended to general-purpose workflow execution. A while loop with retry logic is not a Saga. It has no journal, no compensation logic, and no recovery protocol.

The Naive Pattern and the Durable Pattern

The structural difference between the two is not in the logic — and the failure modes agents actually exhibit make the distinction load-bearing. It is not in the logic — it is in where state lives between steps:

# Naive pattern — state lives in RAM, dies with the process
import asyncio

async def process_order(order_id: str):
    validated = await validate_order(order_id)       # step 1
    charged = await charge_payment(validated)         # step 2 — side effect
    await send_confirmation_email(charged)            # step 3 — side effect
    await update_inventory(charged)                   # step 4 — side effect
    return charged

    # Process dies after step 2. On restart:
    # - step 1: re-validates (harmless)
    # - step 2: charges the card AGAIN (incident)
    # - no record that step 2 already succeeded

And the same workflow with durable execution. The logic is identical. The storage contract is not:

# Durable pattern — state lives in a persistent journal
from temporalio import workflow, activity
from datetime import timedelta

@activity.defn
async def charge_payment(order: dict) -> dict:
    """Activity: side-effectful, retried with backoff, idempotent by key."""
    return await payment_api.charge(
        order["id"], order["amount"],
        idempotency_key=f"charge-{order['id']}"  # safe to retry
    )

@workflow.defn
class ProcessOrderWorkflow:
    @workflow.run
    async def run(self, order_id: str) -> dict:
        validated = await workflow.execute_activity(
            validate_order, order_id,
            start_to_close_timeout=timedelta(seconds=30),
        )
        charged = await workflow.execute_activity(
            charge_payment, validated,
            start_to_close_timeout=timedelta(seconds=60),
        )
        # Process dies here. On restart:
        # - engine reads journal: "step 1 done, step 2 done"
        # - resumes at step 3 — no re-charge, no re-validation
        await workflow.execute_activity(
            send_confirmation_email, charged,
            start_to_close_timeout=timedelta(seconds=30),
        )
        return charged

Temporal enforces a hard separation between workflows (deterministic, replayable — no I/O, no clocks, no randomness) and activities (side-effectful, idempotent, retried with backoff). This separation is the architecture's encoding of the principle: the journal records activity results, and workflow replay reconstructs state from those results without re-executing the activities. The underlying topology — deterministic routing, stochastic execution, independently checkpointed nodes — is the same structure that makes Probabilistic State Machines reliable at scale.

Execution Model State Lives In Survives Process Death? Re-executes Side Effects? Delivery Semantic
while loop + retry decorator RAM / call stack No Yes At-most-once (cross-process)
Task queue (Celery, RQ) Message broker Partial (task envelope only) Yes (per task restart) At-least-once (task-level)
Durable execution (Temporal, Step Functions, Restate) Persistent journal Yes No Exactly-once (with idempotent activities)

Task queues occupy an important middle position. Celery persists the task envelope — the input and the task identity — to the message broker. What it does not persist is the internal state of a multi-step task. If a Celery worker dies inside a complex task, that task re-runs from its beginning. For single-step idempotent tasks, this is acceptable. For agentic workflows with 5–15 sequential LLM calls and accumulated external API state, it isn't — the task envelope survives the process death; the progress within the task does not.

The Compound Failure Problem

Classical background jobs — resize an image, send a transactional notification — complete in under two seconds, touch one external system, and produce no state that re-execution would corrupt. If the job fails, restart it. The restart cost is two seconds of compute.

Agentic workflows invert every one of these properties. A production agent orchestrating a multi-step business process runs for minutes, calls 6–12 external APIs, writes to multiple databases, and makes decisions that depend on the accumulated results of prior steps. Each external service is an independent failure domain. Each database write is a side effect that re-execution would duplicate.

Agentic workflows invert every assumption that makes naive retry acceptable: they run longer, cross more failure domains, and produce side effects that re-execution duplicates rather than corrects. The operational cost of this mismatch is not linear — it compounds with task complexity.

The compound probability is the argument. A workflow touching 8 external services, each with 99.9% availability, has a per-run joint success probability of 0.999⁸ ≈ 99.2%. At 10,000 agent runs per day, that is roughly 80 runs per day that encounter a service-level failure. Each of those 80, without a durable journal, either corrupts state or requires manual remediation. With a journal, each resumes from the last recorded checkpoint, automatically.

Infrastructure failures compound further. Pod evictions, OOM kills, and rolling deploys in a Kubernetes cluster under continuous delivery are not exceptional events — they are scheduled. A workflow running for 20 minutes on a cluster with an active deployment pipeline is statistically likely to span at least one infrastructure event. The while loop's response to every such event is identical: restart from step zero.

The Operational Cost Argument Is Backward

The standard objection is setup overhead: Temporal requires a cluster, Step Functions charges per state transition ($0.025 per 1,000 transitions), Restate requires a sidecar. The implication is that durable execution is an advanced optimization — justified only after a system has proven it needs it in production.

The reasoning inverts under real operating conditions. The operational cost of not having durable execution is paid in engineering hours debugging partial state, in customer-facing incidents from duplicate side effects, and in the eventual rewrite when the naive implementation collapses under production load. That rewrite — measured once the failure modes surface — consistently runs 3–5× higher than adopting a durable execution primitive from the start.

Temporal's self-hosted cluster adds roughly 2–4 hours of initial setup and $150–300/month in infrastructure for a small deployment. Step Functions charges $0.025 per 1,000 transitions — a workflow with 20 state transitions running 10,000 times per day costs $5/day. These are not optimization-phase costs. They are table stakes for any system where task failure has a business consequence.

The engineering cost of retrofitting durable execution after a naive loop fails in production runs 3–5× higher than adopting it at the start. "We'll add it later" assumes the failure mode is recoverable. For double-charged payments and corrupted records, it often is not.

Where This Principle Does Not Apply

The Principle of Execution Durability applies when three conditions co-occur: the workflow has side effects that cannot be safely re-executed, the workflow runs long enough to be interrupted by infrastructure events, and failure has a meaningful business cost. Remove any one of these and a simpler model is defensible.

A while loop is entirely appropriate for: a local development harness, a pure-computation pipeline with no external writes, a task that completes in under 5 seconds against a single idempotent endpoint. The principle is not "never use loops" — it is that loops are not a substitute for a durability guarantee when one is structurally required.

Similarly: a single LLM call with no downstream side effects needs no durable execution. The boundary condition is side-effect multiplicity crossed with execution duration crossed with the cost of failure. Below that threshold, simplicity wins. Above it, the loop is a liability that has not yet announced itself.

The frameworks catching up to this — Temporal's workflow model, LangGraph's persistence layer, the durable task patterns in Semantic Kernel — are encoding the same principle in different syntaxes. What differs is the syntax. What is identical is the underlying storage contract: before advancing, persist. The question is not which framework. The question is whether the team understands why the guarantee is necessary — because a team that understands the principle reaches for the right tool instinctively, and a team that doesn't rebuilds the problem in every new framework they adopt.

Most agentic systems currently running in production are one pod eviction away from a state corruption event their operators haven't encountered yet. The loop is still running. The journal does not exist.

Subscribe to Arizen

Architecture notes for teams shipping AI systems into real use.
[email protected]
Subscribe