durable execution · a fit guide
Considering Temporal? Read this first.
Temporal, Dapr Workflow, and Azure Durable Functions are serious systems, built by people who understand failure. If you need what only they do, use them; this page will tell you when that is. It answers the other question: you have outgrown a job runner, you looked at durable execution, and the machinery feels out of proportion to the problem in front of you. That instinct deserves a straight answer, not a sales page.
The shared model
One model, and its two costs.
All three are replay engines. Recovery does not resume your code where it stopped: it re-executes your orchestrator from the beginning, deterministically, against the event history recorded so far. The model is powerful, and it bills you twice.
- Cost one · a contract on your code
- The orchestrator must be deterministic, because replay must reproduce the exact decisions the first execution made. No wall clock, no random, no ordinary IO inside it; every nondeterministic thing is routed through the framework as an activity, a durable timer, or a framework-supplied value. And because recovery replays old histories through current code, deploying while work is in flight is a versioning problem with framework rules of its own.
- Cost two · a second system
- Temporal runs as a server cluster or a cloud service. Dapr Workflow requires the Dapr runtime and its sidecar. Durable Functions is the exception on this axis: with a bring-your-own backend the engine runs inside your process, and what remains is the storage provider and the replay contract itself. Where the second system exists, it is real infrastructure next to your application: deployed, secured, versioned, and paid for on its own schedule.
Neither cost is a flaw. Both follow directly from making replayed history the source of truth, and for some problems that trade is right. The question is whether it is right for yours.
The other way
Record outcomes. Never replay.
Acta persists recorded outcomes at explicit boundaries instead of re-executing history. A handler is an ordinary re-entrant .NET method: when a worker dies mid-job, another worker re-enters it, and everything already recorded is returned instead of re-run.
- Durable steps.
RunStepAsyncrecords a step's outcome as a SQL row; re-entry returns the stored result and the body does not run again. - Durable waits.
SleepAsyncandWaitSignalAsyncpark a job as durable state: no worker thread waits, and the job resumes on any peer when the timer fires or the signal arrives. - Fan-out with lineage.
MapAsyncspawns child jobs with recorded parentage, so a thousand-item batch stays traceable and the parent can wait on results. - No determinism contract. Wall clock, random, and IO are fine anywhere in handler code, because nothing is ever replayed against a history. There is no orchestrator/activity boundary to police.
- No second system. Infrastructure is a NuGet package plus the Postgres, SQL Server, or SQLite you already run. No server, no sidecar, no control plane. For Temporal and Dapr this is structural, not a maturity gap: an orchestration server can add features forever, but it cannot become a library inside your process using the database you already back up. Embedded and replay-free is the architecture Acta chose, the way replay is the architecture all three chose.
- State is rows. Jobs, attempts, leases, steps, signals, and events are SQL rows with curated views. When something is stuck, you
SELECTthe evidence.
The code, side by side
A determinism contract, next to a re-entrant handler.
// The shape every replay engine shares; names vary by engine.
// Recovery re-executes this method from the top against recorded
// history, so it must be deterministic: no wall clock, no random,
// no ordinary IO on this side of the boundary.
public async Task RunOrchestration(IOrchestrationContext ctx)
{
var artifact = await ctx.RunActivity<Artifact>(
"build-artifacts", input.ReleaseId);
var approved = await ctx.WaitExternalEvent<bool>(
"release-approval");
if (approved)
{
await ctx.RunActivity("publish", artifact);
}
}
// BuildArtifacts and Publish live elsewhere, registered as
// activities: ordinary code is legal only on that side.
public sealed record PublishRelease(string ReleaseId);
public sealed class ReleaseJobs
{
[Job("publish-release")]
public async Task Handle(PublishRelease input, JobContext ctx, CancellationToken ct)
{
// A named durable step: its outcome is recorded in SQL. Kill the worker after
// it completes and re-entry returns the stored result; the body does not run again.
var artifact = await ctx.RunStepAsync("build-artifacts",
token => BuildArtifactsAsync(input.ReleaseId, token));
// Suspends the job durably: no worker thread waits. Approve it in an hour or in
// three days, from code, the CLI, or the dashboard; it resumes on any peer worker.
var approved = await ctx.WaitSignalAsync<bool>("release-approval", ct);
if (approved)
{
await ctx.RunStepAsync("publish", token => PublishAsync(artifact, token));
}
}
}
The left panel is deliberately generic; the boundary is what every engine shares, whatever it names the methods. On the right there is no boundary: BuildArtifactsAsync is ordinary code called in place, and durability comes from the recorded step outcome, not from re-executing the method. A worker kill between any two lines loses no completed work.
A fair split
When you genuinely need a replay engine.
Some requirements are exactly what these systems were built for. If one of these is yours, choose them: Acta does not compete there, and says so on its own homepage (no deterministic replay, no BPMN, no hosted control plane).
- Polyglot workers
- Orchestrations coordinating workers written in Go, Java, Python, or TypeScript alongside .NET. Acta is .NET only.
- Replay as a guarantee
- Deterministic re-execution itself is the requirement: an auditable guarantee that recovery reproduces the exact decision path, not just the recorded outcomes.
- Very long histories at scale
- Orchestrations spanning months and many deploys, with framework-managed versioning of in-flight work as the code underneath it changes.
- One orchestrator, many teams
- An organization standardizing on a single orchestration platform across teams and languages, with the operating investment that decision assumes.
- Multi-region failover
- Orchestration state that must fail over across regions under the platform's own replication, beyond what your database's HA story provides.
Stated plainly
What Acta gives up.
- Exactly-once side effects: which nobody has. Acta steps are at-least-once, and so are the replay engines' activities: a crash between an external call and its recording re-runs the call on every one of these systems, because no engine can make the outside world transactional. Replay makes the orchestrator's decisions exactly-once, not your HTTP request. So external calls need idempotency or reconciliation there and here alike; Acta adds
AtMostOnce()for the steps where duplication is worse than an explicit ambiguity. At-most-once steps → - .NET only. Handlers, workers, and the tooling are .NET. There are no SDKs for other languages and none planned.
- One database is the durability boundary. Acta is exactly as durable and as available as the Postgres, SQL Server, or SQLite you point it at. That is the point of the design, and also its limit.
- Production miles. The replay engines have a decade of them; Acta is young. Its evidence is accumulated in the open instead: ~620 conformance tests run identically against real Postgres, SQL Server, and SQLite, and committed certification seals. The latest pair: a million jobs on Postgres and again on SQL Server, dozens of worker processes with one killed every five seconds, 155 and 189 workers reclaimed dead, 9,363 and 11,703 orphaned attempts, and 1,000,000 of 1,000,000 jobs succeeded on each with every SQL check green, on unmodified production defaults. The seals →
The fit
Most jobs do not need an orchestration platform.
Use Acta when a background job stops being a task and starts being application state: durable steps, long waits, signals, children with lineage, and operators who need to ask what happened. If that is your problem, it does not require a replay engine, a determinism contract, or a second system. It requires the work recorded where you can see it, in the database you already run, so you can kill the worker, keep the work. And that recovery path is exercised, not promised: certification runs kill real worker processes mid-job and assert the recovery with SQL checks anyone can re-run.
Also: how Acta compares·from Hangfire·from Quartz·from TickerQ