Durable Execution Architecture: Temporal, Restate and DBOS (2026)

Durable Execution Architecture: Temporal, Restate and DBOS (2026)

Durable Execution Architecture: Temporal, Restate and DBOS in 2026

Every backend team eventually writes the same fragile code. An order pipeline charges a card, reserves inventory, calls a shipping API, then emails the customer. Halfway through, the process is killed by a deploy, an OOM, or a spot-instance reclaim. Now the money left the account, the stock is half-reserved, and nobody shipped anything. The usual fix is a sprawl of status columns, retry loops, cron-driven reconciliation jobs, and hand-rolled saga compensations that nobody fully trusts. Durable execution is the architectural pattern that deletes almost all of that code by making a running function survive the death of the process it runs in.

This matters now because three production-grade engines – Temporal, Restate, and DBOS – have converged on the same core idea from very different operational starting points, and choosing between them is a real 2026 decision. This article explains the mechanism, then the trade-offs.

What this covers: the failure durable execution solves, the event-sourced replay mechanism that powers it, why workflow code must be deterministic, how Temporal, Restate, and DBOS differ architecturally, and the failure modes and costs that decide which one you should adopt.

Context and Background

Before durable execution had a name, the answer to “how does this multi-step process survive a crash?” was a state machine plus a database. You persisted a status enum on a row, wrote a worker that polled for rows in each state, and advanced them one transition at a time. It worked, but every workflow became a bespoke distributed-systems project: idempotent transitions, dead-letter handling, timeout sweeps, and compensation logic all written by hand and debugged in production.

The saga pattern formalized part of this – a sequence of local transactions, each with a compensating action to undo it if a later step fails. Sagas are correct in theory and miserable in practice, because the orchestration state (which step am I on, what have I already undone?) is exactly the thing that gets lost when a process dies mid-flight.

Durable execution flips the model. Instead of you persisting state, the engine persists the execution itself. Temporal, which popularized the term and grew out of Uber’s Cadence project, describes durable execution as the ability of application code to continue running “even when the underlying infrastructure fails,” including surviving crashes, restarts, and deploys without losing progress, per the Temporal Workflow Execution documentation. Restate, DBOS, AWS Step Functions, and Azure Durable Functions all now sit in this category. This piece is the cross-engine architecture treatment; if you want a hands-on single-tool build, see our Temporal durable workflow orchestration tutorial.

The Reference Architecture: Deterministic Code Plus a Replayable Log

A durable execution engine is, at its heart, two things bolted together: an append-only event history that records every meaningful thing a workflow does, and a runtime that replays that history to reconstruct the workflow’s in-memory state after any interruption. Your business logic runs as ordinary code; the engine intercepts its interactions with the outside world and turns them into durable events.

Durable execution reference architecture with deterministic workflow code, an append-only event history and worker replay

Figure 1: The core durable execution loop – a client starts a workflow, the engine records intent in an append-only event history, a worker runs deterministic workflow code, and every side effect flows back into the log.

The client asks the engine to start a workflow. The engine writes a “workflow started” event to the history and puts a task on a queue. A worker picks up the task, runs your workflow function, and each time that function does something durable – schedules an activity, starts a timer, waits for a signal – the worker sends a command back to the engine, which appends a corresponding event to the history. When the worker or the whole machine dies, none of that state is lost, because it lives in the log, not in the worker’s memory. A fresh worker reloads the history and rebuilds state.

The engine owns durability so your code does not have to. In a hand-rolled system, the durability boundary is wherever you remembered to write a row. In durable execution, the boundary is every interaction the runtime intercepts, uniformly, whether you thought about it or not. That uniformity is the whole value proposition.

Event history is event sourcing applied to control flow

The event history is a textbook application of event sourcing: the source of truth is an ordered, immutable sequence of events, and current state is a fold over that sequence rather than a mutable snapshot. Temporal’s Event History encyclopedia entry defines it as “a complete and durable log of everything that has happened” in a workflow’s lifecycle – WorkflowExecutionStarted, ActivityTaskScheduled, ActivityTaskCompleted, TimerStarted, TimerFired, and dozens of other event types.

The key insight is that you can throw away all in-memory state at any moment and recover it exactly by replaying the log. There is no separate “checkpoint” the way a game saves progress; the log is the checkpoint, growing one event at a time. This is why the pattern is sometimes called “event-sourced execution.”

Activities and steps are the durability boundary for side effects

Workflow code itself must be pure orchestration. Anything that touches the non-deterministic outside world – an HTTP call, a database write, reading the clock, generating a UUID – is wrapped in an activity (Temporal’s term) or a step (DBOS and Restate). The engine executes the activity, captures its result, and writes that result into the event history as a completed event.

On replay, the engine does not re-run the activity. It sees the recorded ActivityTaskCompleted event and simply hands the stored result straight back to your workflow code. The Temporal durable execution learning materials put it plainly: activities that already succeeded are skipped on replay, and the workflow “remembers” their results. This is the mechanism by which a card is charged exactly once even though the workflow function may run to that line a dozen times across a dozen replays.

The replay contract is what makes it all work

During replay the runtime re-executes your workflow function from the top and, for each command your code emits, checks it against the next event in the history. As long as your code produces the same sequence of commands it produced originally, replay slides forward silently until it reaches the point where new work begins. This equivalence between “the commands the code wants to issue” and “the events already in the log” is the contract the entire architecture rests on – and it is exactly why the code must be deterministic.

How Replay Reconstructs State After a Crash

The clearest way to understand durable execution is to watch a workflow die and come back. Consider our order pipeline: charge card, reserve stock, ship order. Suppose the worker crashes after the first two steps have completed but before shipping.

Sequence diagram showing a worker crash and event history replay reconstructing workflow state without re-charging

Figure 2: A crash-and-replay sequence. The worker completes two steps, recording each in the event history, then crashes. A new worker replays the history to rebuild state and resumes at the exact next step – no double charge.

Walk through Figure 2. The client starts the order workflow. A worker charges the card and records charge card done in the history, then reserves stock and records that too. Now the worker crashes – power loss, OOM, a rolling deploy, it does not matter. The engine notices the workflow task was never completed and redispatches it to a different worker.

The new worker has no memory of this order. It loads the event history and replays it. When the workflow code reaches the “charge card” line, the runtime sees a completed event already in the log and returns the stored result instead of calling the payment gateway again. Same for “reserve stock.” Only when the code reaches “ship order” – which has no corresponding event yet – does the engine actually schedule real work. The customer is charged once, the stock is reserved once, and the order ships once, despite the workflow function having executed those first two lines twice.

This is the difference between at-least-once delivery and effectively-exactly-once execution. The engine may run your workflow function many times, but each side effect is committed to the world exactly once because its result is memoized in the durable log. That property is what lets you delete your reconciliation cron jobs.

Timers and signals are durable inputs, not just outputs

The log records more than side-effect results. It also captures inputs that arrive over time – and this is what makes durable execution suitable for workflows that run for days or months, not just seconds.

A durable timer is the engine’s replacement for sleep(). When workflow code asks to wait an hour, the runtime records a TimerStarted event and the worker goes away entirely; no thread is blocked, no process is held open. When the deadline passes, the engine writes TimerFired and redispatches the workflow. On replay, the fired timer is just another event folded into state, so a workflow can “sleep” for thirty days across dozens of deploys and wake up correctly. This is how subscription renewals, escalation timeouts, and human-approval deadlines are modeled without a cron sweeper.

A signal is an external input delivered into a running workflow – an approval click, a cancellation, an updated address. The engine appends the signal to the history as an event, so it becomes a durable, replayable part of the execution. Because signals are ordered in the log alongside everything else, a workflow that blocks waiting for one resumes deterministically after a crash. Temporal adds queries (read-only state reads that never mutate history) and child workflows (composition with independent histories); Restate exposes the same shape through awakeables and durable promises. The architectural point is uniform: time and external events are first-class, journaled inputs, which is precisely why you must never read the wall clock or block on a raw socket yourself.

Why the code must be deterministic

Replay only works if the code takes the same path every time. If your workflow branches on if random() > 0.5 or if now() > deadline, the second run can take a different branch than the first, emit a different command than the log expects, and the whole reconstruction falls apart. Temporal calls this a non-determinism error, and replay “will be unable to continue” when the commands produced diverge from the recorded history, as the Event History docs describe.

So durable execution engines forbid raw non-determinism inside workflow code and provide deterministic substitutes. Here is the shape of the rule:

# WRONG - non-deterministic inside workflow code.
# On replay, time and randomness differ from the original run,
# so the workflow can take a different branch and break replay.
import time, random, requests

def order_workflow(order):
    if random.random() < 0.1:                 # non-deterministic branch
        flag_for_review(order)
    charged_at = time.time()                  # wall clock differs on replay
    resp = requests.post("/charge", json=order)  # un-journaled side effect
    return resp

# RIGHT - determinism is delegated to the engine.
# Randomness, time, and I/O all go through durable primitives
# whose results are recorded in the event history and replayed.
async def order_workflow(order):
    if await workflow.side_effect(sample_bernoulli):  # recorded, replay-safe
        await workflow.execute_activity(flag_for_review, order)
    charged_at = workflow.now()               # deterministic engine clock
    result = await workflow.execute_activity(charge_card, order)  # journaled
    return result

The pattern is identical across engines even when the API names differ: never read the clock, roll dice, sleep, or make a network call directly from orchestration code. Route every one of those through an engine primitive – an activity, a durable timer, a side-effect wrapper, or a durable RPC – so its outcome is written to the log and can be replayed byte-for-byte.

The Three Engines: Same Pattern, Different Skeletons

All three engines implement the reference architecture above. Where they diverge is the operational architecture – where the log lives, what you have to run, and how the worker connects to it. This is the axis that actually decides adoption.

Deployment topology comparison of Temporal external cluster, Restate single binary and DBOS Postgres library

Figure 3: Three deployment shapes for the same pattern. Temporal runs an external cluster with a dedicated history store; Restate is a single binary with an embedded log; DBOS is a library that stores state in your existing Postgres.

Temporal: an external cluster with a dedicated history service

Temporal is the heavyweight. You run a Temporal Service – a cluster of services including a history service, matching service, frontend, and worker service – backed by a persistence store (Cassandra, PostgreSQL, or MySQL). Your application code runs as workers that poll task queues from the cluster. The cluster owns the event history; workers are stateless and disposable.

To make replay cheap, Temporal workers keep a sticky cache: after a worker runs a workflow, the cluster prefers to route that workflow’s next task back to the same worker, which still has the reconstructed state warm in memory, avoiding a full replay from event zero. The worker performance docs describe tuning this sticky cache as a core scaling lever. This split – durable cluster plus sticky stateless workers – is what lets Temporal scale to enormous fleets and support SDKs in Go, Java, TypeScript, Python, .NET, PHP, and Ruby against one backend. The cost is that you (or Temporal Cloud) must operate that cluster.

Restate: a single binary with a built-in log and durable RPC

Restate takes the opposite stance: it is “a distributed architecture in a box,” a single self-contained binary that bundles the durable log, the invocation router, and the orchestration engine, per the Restate 1.5 announcement. It stores invocation journals in embedded RocksDB tables – no external database required to get started – and its internal durable log is called Bifrost.

Restate’s distinctive move is treating durable execution as durable, exactly-once RPC rather than only as long-running workflows. Because Restate sits on both the caller and callee side of every invocation, it can guarantee end-to-end idempotency across service-to-service calls transparently. It also ships virtual objects – actor-style, keyed, stateful entities with serialized access and durable state – which the Restate anatomy blog presents as first-class primitives alongside workflows. This makes Restate feel less like an orchestrator you submit jobs to and more like a durability layer woven through your service mesh.

DBOS: a library embedded in your process, durable on Postgres

DBOS is the lightest footprint of the three. There is no separate orchestrator to run at all. DBOS Transact is an open-source library you import into your application; you annotate functions as workflows and steps, point it at a Postgres database, and it checkpoints each step into Postgres tables as the workflow runs. The DBOS Transact page describes it as having “no separate service or orchestrator or any external dependencies except Postgres.”

Because the durability layer is just your database, DBOS can offer something the others cannot do as cleanly: a @DBOS.Transaction step runs its writes inside the same Postgres transaction that records the step’s completion. The business write and the durability record commit atomically, giving genuine transactional exactly-once for database steps rather than the memoize-the-result approximation. DBOS ships libraries for Python, Go, TypeScript, and Java. The trade-off is that your workflow durability inherits the scale ceiling and operational profile of a single Postgres, and recovery is tied to your app processes restarting and reclaiming their pending workflows.

Decision Matrix, Failure Modes, and Cost

Here is the comparison distilled. Treat “best fit” as a starting hypothesis, not a verdict; the right answer depends on your existing stack and team.

Dimension Temporal Restate DBOS
Ops model External multi-service cluster (or Temporal Cloud) Single self-contained binary In-process library, no orchestrator
State storage Cassandra / PostgreSQL / MySQL via history service Embedded RocksDB log (Bifrost) Your existing Postgres
Workers Stateless workers polling task queues, sticky cache App handlers behind the binary App process itself, self-recovering
Language SDKs Go, Java, TS, Python, .NET, PHP, Ruby TS, Java, Kotlin, Python, Go, Rust Python, Go, TS, Java
Distinctive primitive Signals, queries, child workflows, Continue-As-New Durable RPC + virtual objects Transactional exactly-once step in Postgres
Exactly-once story Effectively once via replay + activity memoization Exactly-once RPC across service boundaries Transactional exactly-once for DB steps
Best fit Large scale, polyglot fleets, complex long workflows Service meshes wanting durable calls + actors Teams already on Postgres wanting zero new infra

Decision flowchart for choosing between Temporal, Restate and DBOS based on infrastructure and scale needs

Figure 4: A first-pass decision path. Start from how much new infrastructure you can absorb, whether you already run Postgres, and whether you need large-scale polyglot fan-out or durable RPC and actors.

Failure mode: poison workflows

A poison workflow is one that deterministically crashes the worker every time it is replayed – a null-pointer on a specific input, an assertion that always fails. Because the engine faithfully redispatches the task after each crash, a poison workflow retries forever, pinning a worker slot and never making progress. All three engines mitigate this with retry backoff and, critically, the ability to reset or terminate a stuck execution, but the operational lesson is the same: monitor for workflow tasks that fail repeatedly, and build a runbook for resetting them to a known-good history point rather than letting them spin.

Failure mode: event history size limits

The event history grows without bound if you let it. Temporal enforces a hard limit of 51,200 events or 50 MB per workflow execution, warning at 10,240 events or 10 MB, per the Temporal system limits. A workflow that loops millions of times or schedules endless activities will hit that wall and be terminated. The prescribed fix is Continue-As-New: the workflow atomically completes its current run and starts a fresh execution with the same workflow ID and an empty history, carrying forward only the state it needs. Any long-lived or unbounded workflow must be designed around this from day one. Restate and DBOS avoid a single fixed cap but inherit the storage and read-amplification cost of ever-growing journals, so the same discipline applies: bound your histories.

Failure mode: non-determinism on deploy

The nastiest production failure is a replay-versus-deploy incompatibility. You change your workflow code – reorder two activities, add a branch, rename a step – and deploy. Now in-flight workflows started under the old code replay against new code that emits a different command sequence, and they throw non-determinism errors mid-flight. Temporal addresses this with Patching / GetVersion, which records a version marker in the history so old and new executions each take their correct branch, and with Worker Versioning, which pins old executions to old workers, per the Temporal versioning docs. The architectural takeaway: workflow code is not ordinary code. Any change to control flow is a schema migration on your in-flight executions, and it must be gated behind a versioning primitive.

Capacity and cost

The cost profiles differ sharply. Temporal’s dedicated cluster is real infrastructure – persistence nodes, service replicas, monitoring – or a per-action Temporal Cloud bill; you pay a fixed operational floor in exchange for near-limitless horizontal scale. Restate’s single binary collapses that floor dramatically for small and mid-size deployments, with distributed multi-node modes for scale. DBOS’s cost is essentially the marginal load your workflow tables add to a Postgres you already run and pay for – the cheapest entry point, bounded above by how far one Postgres can be pushed before you need sharding or a bigger box. For a system that must survive regional failure, layer this against your broader resilience design; see our multi-region active-active architecture guide for how durable state and geographic redundancy interact.

Trade-offs, Gotchas, and What Goes Wrong

Durable execution is not free reliability. Its biggest hidden cost is the determinism tax on your programming model. Whole categories of ordinary code become bugs inside workflow functions: reading System.currentTimeMillis(), spawning a thread, iterating a HashMap whose order is unspecified, calling a random number generator, or making a bare HTTP request. Teams new to the pattern routinely ship non-deterministic workflows that pass every test – because tests do not replay – and only fail after a real crash or deploy weeks later. Adopt the engine’s replay-test tooling in CI so a code change that breaks history compatibility fails the build, not production.

A second gotcha is debugging a fold, not a stack. When a workflow misbehaves, the bug may live in event number 8,000 of a history you must read like a ledger. This is genuinely harder than reading a stack trace, and it is why event-history inspection tooling is a first-class part of every engine. Budget time to learn it.

Third, exactly-once is a property of the log, not of the world. The engine guarantees a step’s recorded result is applied once. But if your activity calls an external API that itself is not idempotent, an at-least-once retry between “API succeeded” and “result written to history” can double-fire the external effect. The engine cannot fix a non-idempotent downstream; you still need idempotency keys on outbound calls. This is the single most common correctness bug in durable systems and it survives every engine choice.

Finally, beware treating durable execution as a queue. It is more expensive per operation than Kafka or SQS because every step is journaled and fsynced. For high-throughput, fire-and-forget event streaming, a log or queue is the right tool; durable execution earns its cost on stateful, multi-step, long-lived work. When you need durable state changes fanned out reliably, pairing it with a change data capture pipeline using Debezium is often a cleaner split of responsibilities than forcing everything through the orchestrator.

Practical Recommendations

Start by choosing the engine that matches your operational budget, not the one with the best benchmarks. If you already run Postgres and want to add durability to a service with zero new infrastructure, prototype with DBOS first – the barrier to entry is an import statement. If you are building a mesh of services that call each other and you want those calls to be crash-proof and idempotent end to end, Restate’s durable RPC and virtual objects will feel native. If you are operating a large, polyglot fleet with complex, genuinely long-running workflows and can afford to run or buy a cluster, Temporal’s maturity and scale are hard to beat.

Whichever you pick, treat these as non-negotiable:

  • Keep workflow code pure. No clock, no randomness, no I/O, no unordered iteration – route every side effect through an activity, step, or durable primitive.
  • Make every external call idempotent. Pass an idempotency key derived from the workflow ID so at-least-once retries do not double-fire.
  • Version every control-flow change. Gate reordered or added steps behind a patch or worker-version marker before deploying to in-flight executions.
  • Bound your histories. Design long or looping workflows around Continue-As-New (or the equivalent) from the start, not after you hit the limit.
  • Adopt replay tests in CI. A determinism regression must fail the build, because it will never fail a normal unit test.

For automated operational response when a durable workflow does get stuck, wire alerts on repeated task failures into your incident tooling; our AIOps incident response architecture piece covers how to route those signals without drowning on-call in noise.

Frequently Asked Questions

What problem does durable execution actually solve?

It makes a multi-step function survive the death of the process running it. Without it, a crash halfway through a workflow – a deploy, an OOM, a lost instance – leaves your system in a half-finished state that you must detect and repair with status columns, retry loops, and reconciliation jobs. Durable execution moves that responsibility into the engine: it records every step in an append-only log and replays it on recovery, so the workflow resumes exactly where it stopped without re-running completed side effects. You delete the bespoke recovery machinery.

How is durable execution different from a message queue?

A queue delivers messages; it does not remember where you are inside a long computation. If you build multi-step orchestration on a queue, you still hand-write all the state tracking, idempotency, and compensation logic. Durable execution persists the execution itself – the call stack, local variables, and progress – as an event-sourced log, so your code reads like a normal function while surviving crashes. Queues are cheaper per operation and better for high-throughput streaming; durable execution is for stateful, long-lived, multi-step work where losing progress is expensive.

Why must workflow code be deterministic?

Because recovery works by replaying the code against a recorded event history. The runtime re-runs your workflow function and checks that the commands it produces match the events already in the log. If your code reads the clock, rolls a random number, or branches on live external state, the replay can take a different path than the original run, diverge from the history, and fail. Engines forbid raw non-determinism inside workflow code and provide deterministic substitutes – engine clocks, side-effect wrappers, and activities whose results are journaled and replayed unchanged.

Do Temporal, Restate, and DBOS give true exactly-once execution?

They give effectively-exactly-once for your business logic, not blanket exactly-once against the outside world. The engine runs your workflow function as many times as replay requires but applies each step’s recorded result only once, because completed steps are memoized in the log. DBOS goes furthest for database steps: it commits the write and the durability record in one Postgres transaction. But none of them can make a non-idempotent external API safe on their own – an at-least-once retry can still double-fire a downstream call, so you must add idempotency keys to outbound requests regardless of engine.

When should I choose DBOS over Temporal or Restate?

Choose DBOS when your priority is minimal operational footprint and you already run Postgres. It is a library, not a cluster – you import it, annotate workflows and steps, point it at your database, and get durability with no new service to operate. That makes it ideal for teams adding reliability to an existing app without standing up infrastructure. Choose Temporal instead when you need very large scale, many language SDKs, and complex long-running workflows; choose Restate when you want durable, exactly-once RPC and actor-style virtual objects woven through a service mesh.

What happens to running workflows when I change the code?

They can break. In-flight executions started under the old code will replay against your new code, and if the new code emits a different command sequence – a reordered step, a new branch – the engine throws a non-determinism error mid-workflow. This is why every engine provides versioning. Temporal offers Patching / GetVersion, which records a version marker in the history so old and new executions each follow their correct path, and Worker Versioning, which pins old executions to old workers. Treat any change to workflow control flow as a schema migration on live executions and gate it behind a versioning primitive before deploying.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *