all posts

The best durable execution platforms in 2026

Ajay Kumar··10 min read

Durable execution is one idea wearing a lot of vendor logos. Instead of holding a workflow's state in a variable inside a process, you record every step it takes to a log. When the process dies — a deploy, an OOM kill, a spot reclaim — another reads the log, fast-forwards to where the first stopped, and carries on. The call halfway through a three-day approval wait is still halfway through it.

I'm Ajay, I build PandaStack. Conflict of interest up front: we do not sell a durable execution engine and are not planning to. We run Firecracker microVMs, managed Postgres, apps and functions — so we host workers, and the sandbox risky steps run inside. That leaves me no reason to talk you into an engine. If you have already picked Temporal and want to know where to run the cluster, that is a separate post, linked below.

What durable execution actually buys you

The mechanism is event sourcing applied to control flow. Every externally-visible thing your workflow does — an activity call, a timer, a signal, a random number — is appended to a history. When a worker dies, a new one re-executes the function from the top, feeding recorded results back in rather than redoing work, until it hits the first step with no result. That is replay, and it is the whole product.

  • A crash stops being an incident. Only the uncompleted step is retried, which deletes the bug genre where a retry re-charges a card because the process died between charge and database write.
  • Waiting is free and unbounded. An await on a 30-day timer is a row and a timer, not a process in memory, and the worker that resumes it need not have existed when the workflow started.
  • Retries and timeouts become configuration. Backoff, maximum attempts and per-step deadlines stop being hand-rolled loops scattered through a service and become a policy on a step.

Here is the shape in Temporal's TypeScript SDK. Read it for the boundary between the two kinds of code — every engine below has some version of it.

// workflow.ts -- this function is REPLAYED from history after any crash.
// Anything non-deterministic in here is a bug that first appears during
// recovery, which is the worst possible moment for a bug to appear.
import * as wf from "@temporalio/workflow";
import type * as activities from "./activities";

const { chargeCard, sendReceipt } = wf.proxyActivities<typeof activities>({
  startToCloseTimeout: "2 minutes",
  retry: { maximumAttempts: 5, backoffCoefficient: 2 },
});

export const approve = wf.defineSignal<[]>("approve");

export async function checkout(orderId: string): Promise<string> {
  let approved = false;
  wf.setHandler(approve, () => { approved = true; });

  // NOT allowed here: Date.now(), Math.random(), fetch(), reading a mutable
  // global, spawning a thread. Each gives a different answer on replay, and
  // a different answer on replay means the history no longer matches.
  const startedAt = wf.now();   // recorded in history, replay-safe
  const idem = wf.uuid4();      // identical value on every replay

  // Side effects live in activities. Activities are allowed to be
  // non-deterministic, slow and flaky -- that is what they are for.
  await chargeCard({ orderId, idempotencyKey: idem });

  // A human step is just an await. The worker can be killed right here and
  // the workflow does not notice; it resumes from history somewhere else.
  const ok = await wf.condition(() => approved, "72h");
  if (!ok) return "expired";

  await sendReceipt({ orderId, startedAt });
  return "done";
}

The workflow function is a deterministic state machine; the activities are your real code doing real I/O. Temporal draws that line hardest, Restate and DBOS more softly, Inngest at a step boundary inside an HTTP handler. Where a vendor draws it is the most useful single thing to know about them.

What it costs, which the landing pages skip

Cost one: determinism constrains how you write code, and the type system will not enforce it. You can call Date.now() in a workflow, pass every test you write, and fail on a replay weeks later. The SDKs ship replay-safe substitutes and linters for obvious violations, but not for a shared mutable global, or a library memoising on wall-clock time three dependencies down.

Cost two, the big one: versioning. A running workflow's history was written by the code you deployed in March, and replay runs today's code against March's history. Reordering two activities, inserting a step or renaming one can invalidate every in-flight execution. Every engine has an answer — patch markers, worker pinning, side-by-side deployment — and all of them make you think about it at every deploy, forever.

The failure mode this produces is not an outage, it is paralysis. Somebody writes a workflow with a 90-day timer, and eight months later executions are still in flight and nobody wants to be the one who breaks them. Durable execution means your bug is also durable. Design workflow signatures like a public API, because that is what they are.

Cost three is operational, and it varies enormously across this list. A Temporal cluster is several services (frontend, history, matching, its own worker) plus a persistence store — Postgres, MySQL or Cassandra — plus usually Elasticsearch or OpenSearch for advanced visibility. That last is underestimated: standard visibility works without it, but the moment support asks to find all workflows for a customer, you are running a search cluster.

The 2026 framing: agent loops are workflows nobody called workflows

Look at what an agent loop is. It runs for minutes or hours, calling a model, then tools, then the model again, for a number of turns not known up front. Every call is flaky in ways status codes barely describe — rate limits, truncated streams, a hung tool, malformed JSON on attempt three of four. It waits for humans, and must survive a sleeping laptop.

That is not agent-specific infrastructure. It is a long-running, retry-heavy, human-in-the-loop workflow — what durable execution was built for before anyone was writing agents. The mapping is clean: the loop is the workflow, each model or tool call an activity with its own timeout and retry policy, approval a signal, the history the transcript. You get an audit log of every agent decision for free. Teams miss this because they reinvent a worse version first — a state machine in Redis, a status column, a cron job hunting for stuck runs.

One step behaves differently: code execution. When the model writes code and something runs it, the risk is not flakiness — it is that the code is adversarial, accidentally destructive, or merely wants to eat all the memory on the box. Running it inside the worker process, the one holding your durable state and credentials, is wrong however good your engine is. That step wants a machine with its own kernel that you can throw away.

# activities.py -- the tool-execution step of an agent loop.
# The workflow decides WHAT to run and remembers that it ran.
# This decides WHERE, and the answer is: not inside the worker.
from temporalio import activity
from pandastack import Sandbox


@activity.defn
async def run_agent_code(source: str) -> str:
    # A fresh Firecracker microVM with its own guest kernel, restored from
    # a snapshot rather than booted. The TTL means it expires on its own
    # if this activity is killed before destroy() ever runs.
    sbx = Sandbox.create(template="code-interpreter", ttl_seconds=300)
    try:
        sbx.filesystem.write("/work/step.py", source)
        r = sbx.exec("python /work/step.py")
        if r.exit_code != 0:
            # Raise and let the workflow's retry policy decide. stderr goes
            # back to the model as an observation on the next turn.
            raise RuntimeError(r.stderr[-4000:])
        return r.stdout
    finally:
        # The VM is disposable. The history is the durable part.
        sbx.destroy()
Note the ttl_seconds. An activity that provisions something must assume it will be killed between provisioning and cleanup, because sooner or later it will be. Anything created inside an activity should expire on its own, without your code being alive to tidy up — every resource, not just sandboxes.

The options, qualitatively

No prices and no benchmarks below, deliberately. Several of these products have changed pricing shape at least once, and any number here would be wrong within a quarter. Pricing and feature sets move fast; verify against their docs before you commit. What follows is shape, which changes far more slowly.

1. Temporal

The reference implementation, and what everything else is positioned against. Open source, mature, SDKs across Go, Java, TypeScript, Python, .NET and PHP, and a set of primitives — signals, queries, child workflows, continue-as-new, schedules — broader than anything else here. If your orchestration problem is complicated, Temporal already has a name for it.

The cost is that split architecture, plus workers — long-lived processes polling task queues. Temporal Cloud takes the cluster half and leaves the worker half with you, an unusually clean division: they operate the quorum-shaped part nobody enjoys, your logic stays on your infrastructure with your secrets. Pick it when workflows are complex, long-lived and central, and someone will own them.

2. Restate

The most interesting architectural rethink in the category. Restate ships as a single self-contained binary with its own embedded log — no separate database to provision, no search cluster, no quorum of service types to reason about. You run it, register your services, it invokes them.

The programming model is flatter too. Rather than a hard workflow-versus-activity split you write ordinary handlers, and the runtime journals their steps and calls to each other — durable RPC and durable promises as first-class things, with virtual objects giving keyed single-writer concurrency without building a lock. If Temporal's surface feels heavy, look here first. It is younger, so the ecosystem is correspondingly smaller.

3. Inngest

The one that fits serverless, for architectural rather than cosmetic reasons. Inngest inverts the connection: instead of workers polling a broker, the platform calls your HTTP endpoint one step at a time and memoises each result. Your function is an ordinary route handler, and each step returns quickly, so it survives request timeouts.

No poller to keep alive, no worker fleet, no capacity to plan — for a team already on serverless that removes the whole operational category. It is event-driven, with fan-out, concurrency limits, throttling and debounce as declarative config. The trade: execution is HTTP-invoked from someone else's control plane — a real dependency, a network hop per step, and a cost model that rewards coarse steps.

4. DBOS

The minimal-infrastructure answer, and the option most likely to be under-considered. DBOS is a library, not a cluster. You add it to your application, annotate the functions you want durable, and it checkpoints their state into your Postgres — the one you already run, in a schema beside your own tables. No orchestrator in the request path.

The consequences are pleasant. Durable state is transactional with your business data, removing a class of consistency question the others answer with idempotency keys. Local development is your app plus a Postgres, and recovery happens when your process restarts. The flip side: your Postgres is now on the orchestration critical path, language coverage is narrower, and there are no exotic primitives. Important but not baroque workflows — start here.

5. Hatchet

Postgres-backed task orchestration that grew up from the queue side rather than down from the workflow side, and the ergonomics show it: a durable task queue with DAG-shaped dependencies, retries, concurrency controls and fairness keys, plus a dashboard treating run observability as a product rather than a debug tool.

Open source and self-hostable with Postgres as the store, plus a managed cloud. The niche: a job queue that grew a dependency graph, where Celery or a hand-rolled table stopped being enough but a workflow engine feels like a costume. Workers hold a long-lived connection, so this is poller-shaped like Temporal, not HTTP-invoked like Inngest.

6. A Postgres job table with SKIP LOCKED

The baseline everything above should have to beat, and it wins more often than this category's marketing suggests. One table, one partial index, and the SKIP LOCKED clause that lets concurrent workers claim rows without blocking each other. Transactional with your data, inspectable with SQL, and no failure mode you have not already met.

-- The honest baseline: no cluster, no SDK, no replay semantics to learn.
create table jobs (
  id           bigserial primary key,
  kind         text        not null,
  payload      jsonb       not null,
  state        text        not null default 'ready',
  attempts     int         not null default 0,
  run_after    timestamptz not null default now(),
  locked_until timestamptz
);

create index jobs_claimable on jobs (run_after) where state = 'ready';

-- Claim one job atomically. Concurrent workers skip each other's locked
-- rows instead of queueing behind them; that clause is the whole trick.
update jobs set
  state        = 'running',
  attempts     = attempts + 1,
  locked_until = now() + interval '5 minutes'
where id = (
  select id from jobs
  where state = 'ready' and run_after <= now()
  order by run_after
  for update skip locked
  limit 1
)
returning id, kind, payload;

-- The sweeper: a worker died holding a lease, so put the job back with
-- exponential backoff. Everyone forgets to write this one, exactly once.
update jobs set
  state     = 'ready',
  run_after = now() + (interval '1 second' * power(2, attempts))
where state = 'running' and locked_until < now();

What you do not get is the thing you came shopping for. There is no replay, so a worker dying mid-job restarts it from the beginning and every handler must be idempotent by hand. Multi-step flows with waits become rows and a state column you maintain — fine at three steps, miserable at fifteen. The upgrade signal is that column hitting five-plus values and someone drawing it on a whiteboard.

Side by side

  • Temporal — Model: deterministic workflow code replayed from an event history; a server cluster plus a persistence store and usually a search store, with long-lived workers polling task queues. Ops: highest self-hosted; Cloud removes the cluster, never the workers. Best for: complex, long-lived orchestration someone will own.
  • Restate — Model: a single self-contained binary with an embedded log, and a flatter model built on durable RPC, durable promises and keyed virtual objects rather than a workflow/activity split. Ops: lightest of the true engines. Best for: durable execution without adopting a distributed system.
  • Inngest — Model: inverted; the platform HTTP-invokes your endpoint step by step and memoises each result, event-driven, with concurrency and throttling as configuration. Ops: nil, no pollers to keep alive. Best for: serverless-deployed teams. Model the step-shaped cost against a real workflow first.
  • DBOS — Model: a library rather than a cluster, checkpointing workflow state into your own Postgres transactionally alongside your business data, with no orchestrator in the request path. Ops: whatever your Postgres already costs. Best for: important-but-not-baroque workflows in a codebase with a Postgres.
  • Hatchet — Model: a Postgres-backed durable task queue with DAG dependencies, retries, fairness and concurrency keys, plus good run observability; workers hold a long-lived connection. Ops: moderate self-hosted, low on their cloud. Best for: a job queue that outgrew Celery but not into workflow-engine vocabulary.
  • Postgres + SKIP LOCKED — Model: one table, one index, hand-rolled retries and an expiry sweeper, no replay, so every handler must be idempotent yourself. Ops: none beyond the database you run already. Best for: fan-out work, three-step flows, anything where you cannot articulate why replay would help.
  • PandaStack — Model: not a durable execution engine and not trying to be; Firecracker microVMs for the workers and the sandboxed tool-execution step, plus managed PostgreSQL 16 as the store DBOS and Hatchet want. Ops: yours, but it is a Linux process. Best for: the halves the engines above leave to you.

When this is all overkill, which is often

If your workflow is three steps and a retry, you do not need an engine. You need a job table and an idempotent handler. Durable execution instead buys a new deployment topology, a determinism constraint, and a versioning problem you carry for years. Check which of these you need before buying all of them.

  • You need to wait days or weeks mid-process — for a human, a webhook, a settlement window. The strongest signal, because it is genuinely awkward to build any other way.
  • A crash mid-flow leaves data in a state your code cannot describe, and you have written compensating logic to clean it up. Durable execution deletes that logic rather than improving it.
  • You have more than a handful of steps with real branching, and the state column has stopped being self-explanatory.
  • You need an auditable record of what happened in what order, and reconstructing it from logs is somebody's afternoon.
  • You fan out to hundreds of parallel sub-tasks and need to join them back reliably, including when some fail.

Fewer than two and the honest answer is the Postgres table. Migrating later is fine — a well-factored handler becomes an activity almost verbatim — and the reverse, unwinding an engine after four teams built on it, is the direction that hurts.

The best reason to adopt durable execution is that you have already built a bad version of it by accident and it is straining. The worst is an architecture diagram drawn before the first customer.

Where we fit, and where we are the wrong tool

To restate the disclosure: PandaStack does not offer managed durable execution. No hosted Temporal, no Restate control plane, nothing competing with the six options above. If that is what you came for, pick one of them.

What we run is the half those products leave with you. A worker — Temporal, Hatchet, a DBOS-annotated app — is a long-lived process that must hold a connection open, poll a queue, and not die quietly. That is an awkward shape for platforms built around request-scoped execution with a hard timeout, and an ordinary one for a Firecracker microVM with a full Ubuntu userspace: deploy from git, it stays up, deploys are blue-green. Managed PostgreSQL 16, created in 30 to 90 seconds, pairs with DBOS and Hatchet.

Now the part where our headline feature is the wrong tool. PandaStack apps and sandboxes scale to zero — restoring a microVM from a snapshot has a p50 around 179ms and a p99 around 203ms, against roughly 3 seconds for a first cold boot. For bursty work that is excellent. For a durable execution worker it is actively wrong: a worker that sleeps stops polling its task queue, and a queue nobody polls is a workflow that has silently stopped progressing. No error, no alert, just tasks piling up. Run steady-state workers always-on, and pay for a process to sit there.

Where scale-to-zero is exactly right is the other half: the per-step sandbox. An activity running model-generated code, a customer's script or a migration rehearsal wants a fresh machine with its own kernel that lives for one step and is destroyed — forking one takes 400 to 750 milliseconds on the same host. That is the extent of our claim: the engine is somebody else's, the workers can live here, the dangerous step gets its own kernel.

Choosing, in about ten minutes

  1. Count the properties in the overkill section you actually need. Fewer than two, build the Postgres table and stop reading. This step saves more teams more money than the rest of this list combined.
  2. Decide whether your workers can be long-lived processes. If your deployment story is serverless and you want to keep it, Inngest's HTTP-invoked model fits without a new topology; everything else assumes a poller.
  3. Decide how much infrastructure you will own: a library on your existing Postgres, a single binary, a Postgres-backed service, or a multi-service cluster plus a search store. That spectrum is the axis this market really varies on.
  4. Write down your worst versioning scenario first — the longest-lived workflow you expect in flight, and what happens when you must change its shape mid-life. Read each candidate's versioning docs against that.
  5. Check language coverage against what your team actually writes, including whatever your ML people use. It eliminates candidates faster than anything else and gets checked last.
  6. Separate the tool-execution question from the orchestration one. Any step running code you did not write needs its own isolation boundary, and no engine here provides one.

The short version

Temporal if the workflows are complex, long-lived and central enough that someone will own them properly. Restate for the same guarantees with far less infrastructure and a younger ecosystem. Inngest if you are serverless and want to stay that way. DBOS if you trust a Postgres and would rather add a library than a cluster. Hatchet if what you have is a job queue that grew a dependency graph. And SKIP LOCKED if you cannot name two properties above that you need.

If you are building agents, take the framing seriously: your loop is a workflow, and treating it as one buys crash recovery, human-in-the-loop pauses and an audit trail you would otherwise build badly three times. Just remember durability cuts both ways. The engine will carry your workflow faithfully through any crash, any deploy, any datacentre event — including the one you shipped with a bug in it, still out there, patiently making progress, months after you fixed the code.

Frequently asked questions

What is durable execution, in plain terms?

It is a way of running code so that the program's progress survives the process running it. Instead of holding workflow state in memory, the runtime appends every externally-visible step — an activity call, a timer, a signal, a generated identifier — to an event history. If the process crashes, another one re-executes the function from the top, feeding recorded results back in for steps that already completed, and only performs real work at the first step with no recorded result. That mechanism is called replay. The practical effect is that a crash is not an incident, waiting for days or weeks costs nothing, and retries and timeouts become per-step configuration rather than hand-written loops.

Do I need Temporal, or is a Postgres job queue enough?

A Postgres table with SELECT ... FOR UPDATE SKIP LOCKED is enough for a surprising amount of work, and it is transactional with your data and inspectable with ordinary SQL. It stops being enough once you need multi-step flows with long waits between steps, crash recovery that resumes mid-flow rather than restarting a handler, or an auditable ordered record of what happened. The useful test is to count how many of those you actually need — fewer than two, build the table. The upgrade signal is when your state column has more than about five values and someone has started drawing it on a whiteboard. Migrating a well-factored handler into an activity later is straightforward; unwinding an unnecessary workflow engine is not.

Why is workflow versioning such a problem?

Because replay re-executes today's code against a history written by the code deployed when the workflow started. If you reorder two steps, insert one in the middle or rename an activity, today's code produces a different sequence of commands than the history records, and every in-flight execution of that workflow can break. Every engine offers an answer — patch or version markers, worker versioning and pinning, or running old and new code side by side — and all of them work, but all of them require the change to be handled deliberately at deploy time for as long as any old execution survives. Treat workflow signatures like a public API, and be conservative about long timers.

Should I use durable execution for AI agent loops?

Usually yes, if the loop runs longer than a couple of turns. An agent loop is long-running, repeatedly calls flaky external services, often needs to pause for human approval, and must survive the user closing their laptop — precisely the shape durable execution was designed for. The mapping is clean: the loop is the workflow, each model or tool call is an activity with its own timeout and retry policy, approval is a signal, and the event history doubles as an audit trail of every decision the agent made. The one step needing separate treatment is code execution, which wants an isolation boundary with its own kernel that the workflow engine does not provide.

Does PandaStack offer managed durable execution?

No. We do not run a workflow engine and are not planning to — for orchestration, pick one of the engines in this post. What we host is the two halves those products leave to you. Workers run as long-lived processes in Firecracker microVMs with a full Ubuntu userspace, deployed from git with blue-green rollouts, and managed PostgreSQL 16 is available as the store that DBOS and Hatchet both want. One important caveat: do not put a worker on our scale-to-zero path. A worker that sleeps stops polling its task queue, and workflows then stall silently with no error. Use always-on apps for workers, and disposable sandboxes for the tool-execution step inside an activity.

Keep reading

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.