Half-finished orders, payments with no shipment and the retry queues built to clean them up cost a team its nights and its roadmap. Durable execution is the idea that takes most of that work off your engineers, and Temporal is the best-known framework built on it.
Durable execution means a function keeps its progress when the machine running it doesn't. If the process crashes halfway through an order, the code picks up at the line it had reached instead of starting over or leaving the order half-finished. Temporal does it by recording every step in an Event History before the code moves on, so after a crash another machine can replay that record and carry on from the same point.
Updated by the TechFabric editors in October 2026. John Bellaud's 2024 article has been rewritten around how durable execution works, and every limit and default below was checked against the Temporal, AWS and Microsoft documentation on 11 October 2026.
The failure durable execution removes
Picture an order flow that reserves stock, charges the card, creates the shipment and emails the customer. Written as an ordinary service, it has four places to die. A pod gets evicted after the charge and before the shipment. The payment API times out and you can't tell whether the charge landed. A deploy restarts the service while thirty orders are mid-flight.
The usual answer is a pile of infrastructure around the business logic. A status column in a database, a queue per step, a cron job that sweeps for orders stuck in "charged" for more than ten minutes, retry counters, and an on-call engineer who knows which rows to fix by hand. Every team builds a slightly different version, and every version has a gap somebody finds at 2am.
Durable execution moves that bookkeeping into the platform. You write the four steps as one function. The platform guarantees it either runs to the end or fails in a way you chose, however many times the processes underneath it restart.
How Temporal's framework works
Temporal splits an application into two kinds of code and two kinds of process.
- Workflows are the orchestration. They decide what happens next, and they must be deterministic, because Temporal re-runs them.
- Activities are the side effects: API calls, database writes, emails, model calls. They can fail, time out and be retried.
- The Temporal Service (self-hosted, open source under the MIT licence, or Temporal Cloud) stores each Workflow's Event History and hands out tasks.
- Workers are your processes. They poll a Task Queue, run Workflow and Activity code, and report results back.
When Workflow code calls an Activity, it doesn't call it directly. It emits a command, the Service records ActivityTaskScheduled, and some Worker picks the task up. The result comes back as ActivityTaskCompleted, with the return value stored in the history. If the Worker running the Workflow dies, another one receives the next Workflow task, runs the function again from the top, and at each Activity call finds the result already in the history. It skips the call, takes the recorded value and carries on. That's replay, and it's why a Workflow can sleep for thirty days on a durable timer and wake up on a different machine after three deploys.
The documentation says the code must "make the same Workflow API calls in the same sequence, given the same input." On replay, the commands your code emits are compared with the history, and a mismatch fails the Workflow with a non-determinism error.
Temporal fault tolerance, in specifics
Activities retry by default, forever. Per Temporal's retry policy docs, the default Activity policy is a 1 second initial interval, a backoff coefficient of 2.0, a maximum interval of 100 times the initial one (100 seconds), and unlimited attempts. Unlimited is the surprise. A bug that throws on every call will retry until somebody notices, so mark the errors that can never succeed (a declined card, a validation failure) as non-retryable.
Workflows don't retry by default. The docs are explicit: "Unlike Activities, Workflow Executions do not retry by default." A Workflow survives Worker crashes through replay. A Workflow whose own logic throws stays failed unless you gave it a retry policy.
Every Activity needs a timeout. You must set Start-To-Close or Schedule-To-Close, or the Activity is rejected. Schedule-To-Start defaults to infinity, which means an Activity on a Task Queue with no Workers waits patiently forever. Long Activities should heartbeat with a Heartbeat Timeout, so a hung call is detected in seconds instead of at the end of a one-hour Start-To-Close.
At-least-once, not exactly-once, for side effects. If a Worker crashes after the payment API accepted a charge but before the result was recorded, the Activity runs again. Temporal can't see inside your payment provider. Pass an idempotency key built from the Workflow ID and the step name, as the code below does, so the second attempt is a no-op on their side.
Here is the order flow in the TypeScript SDK, with those choices made explicit:
import { proxyActivities, sleep, workflowInfo } from '@temporalio/workflow';
import type * as activities from './activities';
const { reserveStock, chargeCard, createShipment, sendEmail, releaseStock } =
proxyActivities<typeof activities>({
startToCloseTimeout: '30 seconds',
retry: {
initialInterval: '1 second',
backoffCoefficient: 2,
maximumInterval: '1 minute',
nonRetryableErrorTypes: ['CardDeclined'],
},
});
export async function orderWorkflow(orderId: string): Promise<void> {
await reserveStock(orderId);
try {
await chargeCard(orderId, { idempotencyKey: `${workflowInfo().workflowId}-charge` });
} catch (err) {
await releaseStock(orderId); // compensate, then fail the order
throw err;
}
await createShipment(orderId);
await sleep('2 days'); // durable timer, survives restarts and deploys
await sendEmail(orderId, 'how-was-your-order');
}
There's no status column, no sweeper and no retry loop in that code. To watch it work locally, temporal server start-dev runs a development Service with the Web UI on port 8233, and temporal workflow show --workflow-id <id> prints the Event History, which is the clearest way to understand replay. Kill the Worker mid-run, restart it, and read the history it resumes from.
The limits nobody puts on the first slide
Durable execution isn't free. These four constraints are the ones that bite in production.
- History size. An Event History is capped at 51,200 events or 50 MB, with warnings from 10,240 events or 10 MB (Temporal Cloud limits). A Workflow that loops over a million records, or polls every minute for a year, will hit it. The fix is Continue-As-New, which closes the run and starts a fresh one with a clean history and whatever state you carry forward.
- Payload size. A single request tops out at 2 MB, and each gRPC message at 4 MB. Pass a storage key, not the file.
- Determinism. Reading the system clock, generating random numbers, reading an environment variable or calling a network service from Workflow code will eventually break a replay. The SDKs give you deterministic replacements (
workflow.Now(ctx)in Go,Workflow.currentTimeMillis()andWorkflow.randomUUID()in Java), and the TypeScript SDK swapsDateandMath.randomfor deterministic versions inside its Workflow sandbox. Anything that touches the outside world belongs in an Activity. - Changing code while Workflows are running. A Workflow started last month replays against today's code. Reorder two Activity calls and every in-flight run fails its next replay. Temporal's answers are Worker Versioning, which pins running Workflows to the build that started them, and patching (
patched()in TypeScript and Python,Workflow.getVersionin Java,GetVersionin Go). Decide which one you'll use before the first deploy, not after the first non-determinism error.
A fifth, smaller one: each Workflow can hold up to 2,000 incomplete Activities, Signals or Child Workflows at once, and Temporal recommends 500 or fewer. Fan out wider than that through Child Workflows.
Durable execution vs Temporal, and the alternatives
Durable execution is the pattern, and Temporal is one implementation of it. Several others exist, and the right one depends on where your code already runs.
| Option | Model | Worth knowing |
|---|---|---|
| Temporal | Workflows as code, replayed from history | Official SDKs for Go, Java, Python, TypeScript, .NET, Ruby, PHP and Rust; self-host or Temporal Cloud |
| AWS Step Functions | State machine defined in JSON (Amazon States Language) | Standard workflows run up to 1 year, history capped at 25,000 events, 256 KiB per payload |
| Azure Durable Functions / Durable Task SDKs | Orchestrators as code, replayed from history | Same determinism rules as Temporal (use context.CurrentUtcDateTime, not DateTime.Now); the in-process .NET model loses support on 10 November 2026 |
| A queue plus a status table | Hand-built | Fine for one or two steps; the sweeper and retry logic become the system you maintain |
Step Functions suits teams already deep in AWS whose flows are mostly calls to AWS services, and its JSON definitions are easy to audit. Durable Functions is the natural fit for an Azure Functions estate, and it shares Temporal's replay model closely enough that the constraints above transfer almost word for word. Temporal earns its extra moving part when flows are long-running, cross several clouds or vendors, or carry business logic too involved to express as a state machine diagram.
You don't need any of them for a request that finishes in a second and can safely be retried by the caller. Neither does a batch job that's cheap to rerun from scratch.
Where we've used it
Two published examples show the shape of work where it pays. For Athlinks, a pipeline that stitches more than 400 million athlete records from disparate sources runs as a multi-tiered serverless architecture on Temporal, with automatic retries and visibility into each stage. For Location Services, a long-running, legally sensitive repossession process was rebuilt on Temporal, so every step is recoverable and the history of what happened is there afterwards.
TechFabric is a Temporal partner, and our durable execution work starts with the design decisions above, especially versioning and idempotency, before the first Workflow ships.
What to do next
If you're weighing Temporal for a real system, three steps will tell you more than another overview.
- Pick the one flow that pages someone most often and list its steps, its side effects and which of them are safe to repeat.
- Run it under
temporal server start-dev, kill the Worker halfway, and read the history it resumes from. - Before production, settle the versioning approach, the non-retryable error types and the idempotency key for every Activity that touches money or a third party.
The rest of our Temporal series goes deeper on specific builds: how Temporal changed one senior developer's approach, an airline booking demo, durable RAG with Temporal and Chainlit, and multi-tenant AI infrastructure on Temporal.
Frequently asked questions
Does a Workflow that runs for months hit any limits?
It can, and the limits come from two directions. The Event History grows with every step, so a Workflow that polls on a short interval for months is heading for the 51,200-event cap, and the way out is Continue-As-New. The other limit is your own code. A Workflow that sleeps that long will be replayed against code that has been deployed many times since it started, so we'd settle the versioning approach before the first deploy.
If we're already on AWS, is Step Functions the simpler choice?
For flows that are mostly calls to AWS services, probably yes. Standard workflows cap history at 25,000 events against Temporal's 51,200, and limit payloads to 256 KiB against Temporal's 2 MB per request. A flow that carries a lot of state or runs long, or one that reaches outside AWS to other vendors, is where we'd look at Temporal.
Can I use the Event History to show what happened to an order afterwards?
Yes, that's what it is. temporal workflow show --workflow-id <id> prints it. Each Activity result is stored in the history, so you can see what every step returned, which is useful in a process where somebody may later ask why an order ended up where it did. It's the same property the Location Services repossession process relies on.