Durable Execution Engines for AI Agents in Production: Comparing Temporal, Inngest, Hatchet, and Restate Architecture, State Persistence, Event Replay, and Latency Overheads
Building production AI agents requires solving a fundamental systems failure: language model reasoning loops are inherently long-running, non-deterministic, and prone to transient failures. When an autonomous agent executes a 20-step chain involving web scraping, document parsing, API queries, code generation, and human approval gates, a single network timeout or rate limit at step 19 should not abort the execution or restart the loop from scratch.
Restarting an agent from the beginning wastes inference budget, duplicates external side effects such as database mutations or outbound emails, and degrades user experience. Traditional background queues like Celery or BullMQ require manual checkpointing, custom state machines, and ad-hoc idempotency keys to recover gracefully.
Durable execution has emerged as the architectural standard for fault-tolerant agent runtimes. By persisting execution state at every step boundary, durable execution engines guarantee that agent workflows survive process crashes, node restarts, network partitions, and multi-day human-in-the-loop pauses without losing state or duplicating tool executions.
This analysis evaluates four leading durable execution platforms used in production AI systems: Temporal, Inngest, Hatchet, and Restate. We compare their underlying state machines, event replay mechanisms, human-in-the-loop primitives, throughput limits, and operational footprints.

The Four Architectural Paradigms
Durable execution engines achieve fault tolerance through distinct execution models, ranging from event-sourced deterministic code replay to lightweight invocation journaling and transactional DAG queues.
1. Temporal: Event Sourcing and Deterministic Replay
Temporal coordinates durable state via an event history log maintained by a distributed cluster. In Temporal, code is split into two distinct abstractions:
- Workflows: Orchestration functions that must execute deterministically. Workflows define the control flow, state transitions, timers, and child invocations.
- Activities: Non-deterministic, idempotent functions that perform side effects, such as querying an LLM API, calling external tools, or reading from a database.
When a worker running a Temporal Workflow crashes, a new worker reconstructs the exact workflow state by re-executing the workflow function from the beginning. Whenever the workflow reaches an Activity that already completed, the Temporal SDK intercepts the call, fetches the activity result from the persisted event history, and returns it immediately without re-executing the network call.
For AI agents, Temporal provides battle-tested durability. However, it imposes strict constraints:
- Workflow code must remain completely deterministic (no non-deterministic random generators, system clocks, or direct network calls inside the workflow function).
- Workflow execution history is capped at 50,000 events or 50 MB. Long-running reasoning loops must proactively call
workflow.continue_as_new()to archive historical events and reset the active event log.
2. Inngest: Serverless Step-Level Memoization
Inngest decouples workflow orchestration from persistent worker processes. Instead of maintaining long-lived TCP connections, Inngest communicates with application code over standard HTTP endpoints or WebSockets, making it natively compatible with serverless environments such as AWS Lambda, Vercel, and Cloudflare Workers.
Inngest uses a step-memoization execution model:
- Developers author functions composed of explicit step primitives:
step.run(),step.sleep(),step.waitForEvent(), andstep.invoke(). - When a function executes, Inngest triggers the handler over HTTP. When the handler completes a
step.run()block, it returns the step output to the Inngest execution engine. - Inngest records the output and calls the function handler again for the subsequent step, injecting the memoized results of previous steps into the execution context.
This design eliminates the need for deterministic replay sandboxing. Developers write standard async TypeScript or Python code. The trade-off is network latency: each step boundary incurs an HTTP roundtrip to the Inngest orchestration layer, introducing 15 ms to 50 ms of dispatch overhead per step.
3. Hatchet: High-Throughput Postgres-Backed Task Queues and DAGs
Hatchet is an open-source orchestration engine built in Go and backed directly by PostgreSQL. Rather than enforcing a pure event-sourced replay engine, Hatchet combines transactional task queues, directed acyclic graphs (DAGs), and durable task primitives.
Key architectural characteristics include:
- Zero Heavy Infrastructure: Hatchet uses PostgreSQL for queue persistence, state storage, and inter-process signaling via
LISTEN/NOTIFY, avoiding the operational complexity of distributed Cassandra or Kafka clusters. - High Concurrency and Rate Limiting: Hatchet provides first-class support for tenant-level concurrency limits, worker slot allocations, and fair queuing algorithms, preventing a single high-volume tenant from monopolizing worker pools.
- Unlimited Fan-Out: Unlike Temporal, Hatchet imposes no hard limits on spawned child tasks or workflow event histories, making it well-suited for massive parallel subagent swarms and multi-document map-reduce workflows.
- Streaming Event Architecture: Hatchet workers can push real-time execution events and streaming LLM token chunks back to the central engine, allowing clients to monitor agent reasoning steps live.
4. Restate: Virtual Objects and Journaled Suspension
Restate delivers durable execution through a lightweight distributed runtime written in Rust. Distributed as a single self-contained binary or managed cloud service, Restate acts as an intelligent, durable reverse proxy in front of HTTP or gRPC microservices.
Restate models agent state using two core abstractions:
- Virtual Objects: Stateful, actor-like entities keyed by a unique identifier (such as
user_idorsession_id). Restate guarantees single-threaded, concurrency-controlled access to a Virtual Object's state. All read and write operations to the object's key-value store are automatically committed with transactional consistency. - Durable Handlers and Awakeables: Inside a handler, side effects and tool invocations are wrapped in
ctx.run(). When awaiting external signals or human approvals, Restate creates anawakeable(a durable callback promise) and suspends the handler.
When a handler is suspended, Restate flushes the execution journal to its embedded storage engine (RocksDB backed by S3 or blob storage) and frees all worker compute resources. When the external event resolves the awakeable, Restate resumes the handler on any available worker from the exact line where it paused.
Architectural Comparison Across Production Dimensions
Primary Execution Model
- Temporal: Event-sourced deterministic replay with strict workflow sandboxing.
- Inngest: Step-level memoization and function re-entry over HTTP/WebSockets.
- Hatchet: Transactional task queues and DAG state machines in PostgreSQL.
- Restate: Invocation journaling and durable suspension via an actor-like reverse proxy.
Core Storage Backend and Ops Footprint
- Temporal: Heavy multi-service cluster (Frontend, Matching, History, Worker) backed by Cassandra, PostgreSQL, or MySQL.
- Inngest: Managed Cloud or self-hosted serverless orchestration gateway backed by PostgreSQL.
- Hatchet: Lightweight single Go engine binary backed directly by PostgreSQL.
- Restate: Single self-contained Rust binary with embedded RocksDB, backed by S3 or local disk.
Determinism and Sandboxing Constraints
- Temporal: Strict workflow determinism required. System time, random numbers, and network calls must be wrapped in Activities.
- Inngest: No replay constraints. Standard async execution flow with explicit
step.run()boundaries. - Hatchet: Standard async execution flow. Durable tasks compose steps, DAGs, and child workers.
- Restate: Standard async execution flow. Side effects and non-deterministic operations wrapped in
ctx.run().
History Limits and Scalability
- Temporal: Hard ceiling of 50,000 events or 50 MB per workflow execution before requiring
continue_as_new. - Inngest: Tier-based step and payload limits per execution.
- Hatchet: Unlimited child task spawning and DAG fan-out without history capping.
- Restate: Scalable invocation journals managed per virtual object and pruned upon completion.
Human-in-the-Loop Primitives
- Temporal: Workflow Signals, Updates, and durable Timers.
- Inngest:
step.waitForEvent()andstep.sleep()primitives. - Hatchet:
ctx.wait_for_event()and composite sleep conditions. - Restate:
ctx.awakeable()one-shot callback promises and durable signals.
Step Dispatch Latency Overhead
- Temporal: 5 ms to 20 ms per activity dispatch via cluster gRPC.
- Inngest: 15 ms to 50 ms per step boundary due to HTTP webhook invocations.
- Hatchet: 2 ms to 8 ms per task dispatch via PostgreSQL queue indexing.
- Restate: 1 ms to 5 ms per step dispatch via Rust gRPC proxying.
Implementing the ReAct Loop with Durable Execution
In an agentic ReAct (Reason + Act) loop, the agent repeatedly queries a language model, evaluates whether tool calls are required, executes the specified tools, and appends the observations back to its prompt context until a termination condition is reached.
The code examples below illustrate how the ReAct loop is structured across Temporal and Restate to guarantee crash recovery without duplicating LLM API spend or tool mutations.
Temporal ReAct Implementation (Python)
In Temporal, the orchestration loop lives within a deterministic Workflow, while the model inference and tool executions are dispatched as Activities.
from datetime import timedelta
from temporalio import workflow, activity
from typing import List, Dict, Any
@workflow.defn
class DurableAgentWorkflow:
@workflow.run
async def run(self, system_prompt: str, user_query: str) -> str:
messages: List[Dict[str, Any]] = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query}
]
max_iterations = 20
iteration = 0
while iteration < max_iterations:
iteration += 1
# Step 1: Execute LLM inference as a durable activity
response = await workflow.execute_activity(
"call_llm_activity",
messages,
start_to_close_timeout=timedelta(minutes=2),
retry_policy=workflow.RetryPolicy(maximum_attempts=3)
)
messages.append({"role": "assistant", "content": response.get("content")})
# Check if agent generated final text or tool calls
tool_calls = response.get("tool_calls", [])
if not tool_calls:
return response.get("content", "")
# Step 2: Execute each tool call as an isolated activity
for tool in tool_calls:
observation = await workflow.execute_activity(
"execute_tool_activity",
tool,
start_to_close_timeout=timedelta(minutes=5),
retry_policy=workflow.RetryPolicy(maximum_attempts=5)
)
messages.append({
"role": "tool",
"tool_call_id": tool["id"],
"content": str(observation)
})
# Manage workflow history size for long-running sessions
if workflow.info().get_current_history_length() > 2000:
await workflow.continue_as_new(system_prompt, messages)
return "Iteration limit exceeded."If the node hosting the Temporal worker loses power during the execution of execute_tool_activity, a separate worker picks up the workflow, replays the message history, restores the exact state prior to the crash, and retries only the failed tool activity.
Restate Virtual Object Implementation (Python)
Restate allows developers to define stateful agents as Virtual Objects. Each agent session maintains its conversation history in durable storage, with built-in concurrency control preventing race conditions when handling multiple concurrent requests.
import restate
from restate import ObjectContext
from typing import Dict, Any
agent_service = restate.VirtualObject("AgentSession")
@agent_service.handler()
async def execute_turn(ctx: ObjectContext, user_message: str) -> str:
# Retrieve persisted message history from Restate's durable state
messages = await ctx.get("messages") or []
messages.append({"role": "user", "content": user_message})
finished = False
while not finished:
# Wrap LLM inference in ctx.run to journal the result
ai_response = await ctx.run(
"llm_inference",
lambda: call_llm(messages)
)
messages.append(ai_response)
tool_calls = ai_response.get("tool_calls", [])
if not tool_calls:
finished = True
break
for tool in tool_calls:
if tool["name"] == "request_human_approval":
# Create a durable promise and suspend compute
awakeable_id, promise = ctx.awakeable()
await ctx.run(
"notify_approver",
lambda: send_approval_request(awakeable_id, tool["args"])
)
# Workflow pauses here; 0 compute consumed while waiting
approval_result = await promise
messages.append({
"role": "tool",
"tool_call_id": tool["id"],
"content": approval_result
})
else:
tool_result = await ctx.run(
f"tool_{tool['name']}",
lambda: run_tool(tool["name"], tool["args"])
)
messages.append({
"role": "tool",
"tool_call_id": tool["id"],
"content": str(tool_result)
})
# Commit updated conversation back to durable storage
ctx.set("messages", messages)
return messages[-1].get("content", "")When Restate reaches the await promise expression, it flushes the execution journal and suspends the invocation. When a human operator submits an approval through an external API endpoint calling restate.resolve_awakeable(awakeable_id, payload), Restate loads the journal and resumes execution immediately.
Human-in-the-Loop and Suspension Economics
In production agent applications, critical actions like committing financial transactions, modifying production infrastructure, or sending customer communications require human authorization.
Traditional architectures implement approval gates using polling loops, background worker sleeps, or ephemeral Redis locks. These patterns introduce substantial operational risks:
- Compute Waste: Keeping an async worker thread alive for hours or days waiting for user interaction consumes memory and worker connections.
- Process Vulnerability: If the worker restarts during a deployment or node maintenance window, in-memory wait states are lost.
- Complex State Synchronization: Polling databases requires custom schemas to track pending approvals, token expiration, and callback routing.
Durable execution transforms human-in-the-loop workflows into zero-cost suspensions:
- Temporal Signals: The workflow enters a durable wait state via
await workflow.wait_condition(lambda: self.approved is not None). The workflow consumes zero CPU or memory while waiting. When an external webhook sends a Signal to the workflow ID, Temporal wakes the execution. - Inngest
waitForEvent: Developers declareawait step.waitForEvent('approval.response', match='data.approval_id', timeout='72h'). If the timeout expires before the event fires, the function handles the fallback path deterministically. - Restate Awakeables: A unique callback token is generated and attached to an external ticket (such as a Slack interactive message or web dashboard). The handler halts until the token is resolved via HTTP POST.
- Hatchet Conditions: Tasks pause on composite conditions combining timeout sleeps and event triggers using boolean logic (
sleep_condition | event_condition).
Token Streaming and Observability
A key engineering challenge in durable agent architectures is token streaming. While durable execution relies on discrete, completed step outputs for checkpointing, interactive chat applications require real-time token streaming to maintain low Time-to-First-Token (TTFT).
Engines address this trade-off using dual-path architectures:
- Ephemeral Streaming Channel: During LLM inference within an activity or task handler, tokens are streamed directly to the frontend via WebSockets or Server-Sent Events (SSE).
- Durable Final Payload: Once token generation finishes, the complete aggregated text and metadata are returned to the orchestration engine to be committed to the durable log.
Hatchet provides native support for worker-to-client event streaming, allowing handlers to push intermediate reasoning traces and token chunks through the Hatchet engine directly. In Restate, HTTP handlers can stream SSE responses to the client while simultaneously recording execution milestones in the durable journal.
Decision Framework: Selecting the Right Engine
Choosing a durable execution engine depends on existing infrastructure, scale requirements, and programming language preferences:
- Choose Temporal when:
- You are orchestrating mission-critical enterprise workflows spanning dozens of microservices.
- You require multi-region disaster recovery and deep observability into enterprise distributed transactions.
- You already operate Kubernetes clusters capable of supporting Temporal's multi-service architecture.
- Choose Inngest when:
- You are deploying agent workflows in serverless environments (Vercel, AWS Lambda, Cloudflare).
- Your application is heavily event-driven and integrates tightly with frontend frameworks like Next.js.
- You want immediate developer velocity without deploying or managing database clusters.
- Choose Hatchet when:
- Your AI workload requires high-throughput task queuing, complex multi-tenant rate limits, and fine-grained worker concurrency control.
- You need unlimited parallel subagent fan-outs without hitting workflow history limits.
- Your infrastructure is standardized on PostgreSQL and Go/Python services.
- Choose Restate when:
- You are building stateful, interactive agents that benefit from the Actor model and Virtual Objects.
- You require minimal operational overhead via a single lightweight binary or sidecar.
- You need sub-millisecond dispatch latency and fine-grained journaled suspension for human-in-the-loop agent workflows.
Sources
- Temporal: Durable Execution for AI Applications
- Inngest: Durable Execution for AI Agents in Production
- Hatchet: Durable Execution and Background Task Architecture
- Restate: Durable AI Loops and Fault Tolerance Across Frameworks
- Pydantic AI: Durable Execution Capabilities and Framework Integrations
- DBOS: Durable Execution for Crashproof AI Agents



