Dapr Agents: A CNCF incubating project (Apache 2.0) that runs thousands of agents on a single core via the virtual actor model
Running thousands of agents on a single core via the virtual actor model — transparent distribution across machines for scale-out orchestration.
The Problem
Every AI agent framework shipping today makes the same implicit bet: that your agents will run in memory, all the time, on a single process. LangGraph keeps its state in a Python dict. CrewAI holds agent definitions in-memory. AutoGen spins up processes that stay resident. They all assume you have the RAM to keep 100 agents hot, the tolerance for losing state on crash, and the operational simplicity of a single-node deployment.
This assumption breaks in production for three reasons.
First, memory cost. A single agent with conversation history, tool definitions, and LLM context can consume 50-100 MB. Run 1,000 agents and you need 50-100 GB of RAM. Run 10,000 and you’re provisioning server fleets just to hold idle agents. Most agents spend 99% of their lifetime waiting for input — you’re paying for memory that does nothing.
Second, crash recovery. When a LangGraph process dies, its state dies with it. You can bolt on checkpointing (LangGraph’s Checkpoint API, AutoGen’s Redis-backed state), but it’s an afterthought, not a first-class guarantee. Every crash loses in-flight work.
Third, distribution. Scaling beyond one machine requires you to build your own routing, sharding, and discovery layer. Every framework punts this to the developer with a “deploy on Kubernetes” footnote and no actual distribution primitive.
| Dimension | In-Process Frameworks (LangGraph, CrewAI, AutoGen) | Dapr Agents |
|---|---|---|
| Agent lifecycle | Always resident in memory | Scale-to-zero via virtual actors |
| 1,000 agents memory | ~50-100 GB RAM | ~500 MB RAM (idle) |
| Crash recovery | Manual checkpointing | Automatic workflow replay |
| Distribution | Developer-built | Transparent via Dapr sidecar |
| State persistence | In-memory default | 30+ state store backends |
| LLM provider swap | Code change required | YAML config change |
| Observability | Manual instrumentation | Built-in OpenTelemetry |
| Security | Application-level | SPIFFE + mTLS sidecar |
Why this matters: The agent frameworks that dominate GitHub stars are designed for prototyping, not production. Dapr Agents is the first framework that starts from the assumption that your agents will crash, your traffic will spike, and your deployment will span multiple machines — and it builds those guarantees into the architecture from day one, not as optional add-ons.
The Investigation
The root cause of the production gap is architectural. Every popular agent framework was designed as a single-process Python library first, with distribution and durability bolted on later. The virtual actor model — pioneered by Microsoft’s Orleans and battle-tested in Azure’s infrastructure — solves all three problems at the architecture level, not the library level.
Finding 1: The virtual actor model eliminates the memory-idle tradeoff.
A virtual actor is a logical entity that exists whether or not its physical object is in memory. The runtime activates an actor on demand when a message arrives for it, and deactivates it after a configurable idle timeout. The actor’s state is persisted to a configurable store. The next message reactivates it from persisted state in milliseconds.
Dapr’s virtual actor implementation achieves ~3ms tp90 and ~6.2ms tp99 activation latency on commodity Kubernetes hardware (3-node cluster, 4 cores, 8 GB RAM per node). That means an agent that hasn’t been touched in hours spins up faster than a cache miss.
What this means: You can register 10,000 agents and pay for the memory of the 10-50 that are active at any moment. The other 9,950 consume zero RAM. Their state lives in Redis, PostgreSQL, or any of the 30+ supported state stores. This is not a theoretical optimization — it is the difference between provisioning 100 GB of RAM and provisioning 500 MB.
Finding 2: Durable execution requires event sourcing, not checkpointing.
Most frameworks implement crash recovery by serializing state at explicit checkpoint boundaries. If the process crashes between checkpoints, the work between the last checkpoint and the crash is lost. Dapr Workflows (which DurableAgent uses under the hood) use event sourcing: every workflow step is recorded as an event in the state store. On recovery, the workflow replays from the event log, skipping already-completed steps and re-executing only the step that was in-flight at crash time.
This is the difference between “we save state every N turns” and “every turn is saved atomically.” The Dapr Agents team benchmarked this at KubeCon EU 2026: a 50-step agent workflow recovers in under 2 seconds from a cold restart, regardless of which step was executing at crash time.
What this means: You do not design your agent workflows around checkpoint boundaries. You write the logic as a straight-line sequence of steps, and the runtime guarantees that each step executes exactly once (or at most once with idempotency). This is the same model that powers Azure Durable Functions, AWS Step Functions, and Temporal — applied to AI agents.
Finding 3: Transparent distribution requires a sidecar, not a library.
Every agent framework that claims “multi-node support” requires you to run a separate orchestration service (Redis, RabbitMQ, a custom gRPC server) and configure routing yourself. Dapr Agents inherits Dapr’s sidecar architecture: each agent process runs alongside a daprd sidecar that handles service discovery, pub/sub routing, state management, and mTLS. The sidecar is a standard binary — no custom infrastructure required.
The sidecar exposes the Dapr Conversation API, which abstracts LLM calls behind a uniform interface. Swap from OpenAI to Anthropic to a local Ollama model by changing one YAML file. No code changes. No redeploy.
What this means: Distribution is not a feature you enable — it is the default. Every Dapr Agent is reachable by any other Dapr Agent through the sidecar mesh. Scaling from 1 node to 100 nodes requires zero code changes. The sidecar handles routing, retries, and circuit breaking automatically.
The Solution
Dapr Agents is a Python framework (v1.0 GA, Apache 2.0) that wraps Dapr’s virtual actor runtime into an AI agent abstraction. You define agents as Python classes or declarative configs, and the runtime handles lifecycle, state, distribution, and durability.
┌─────────────────────────────────────────────┐
│ Dapr Control Plane │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Placement │ │ Sentry │ │ Operator │ │
│ │ Service │ │ (SPIFFE) │ │ (K8s CRD) │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
└─────────────────────────────────────────────┘
│
┌────────────────────────────┼────────────────────────────┐
│ │ │
┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐
│ Agent App │ │ Agent App │ │ Agent App │
│ (Python) │ │ (Python) │ │ (Python) │
│ │ │ │ │ │
│ DurableAgent│ │ DurableAgent│ │ DurableAgent│
│ (actor) │ │ (actor) │ │ (actor) │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ daprd │◄────mTLS───►│ daprd │◄────mTLS───►│ daprd │
│ (sidecar) │ │ (sidecar) │ │ (sidecar) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└────────────────────────────┼────────────────────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
┌─────┴─────┐ ┌──────┴──────┐ ┌─────┴─────┐
│ Redis │ │ Pub/Sub │ │ LLM API │
│ (State) │ │ (RabbitMQ) │ │ (OpenAI) │
└───────────┘ └─────────────┘ └───────────┘
Here’s what each piece does:
- DurableAgent: The agent class. Each instance is a virtual actor — stateful, thread-safe, and natively distributed. The runtime activates it on demand and deactivates it when idle.
- daprd sidecar: A sidecar process that runs alongside your agent. Handles service discovery, state management, pub/sub, mTLS, and LLM conversation proxying. Every agent communicates through its sidecar.
- Placement Service: The Dapr control plane component that maps actor IDs to physical pods. When you scale from 1 to 100 nodes, the placement service rebalances actors transparently.
- Sentry: SPIFFE-compatible identity service. Every agent gets a cryptographic identity. Every inter-agent call is authenticated and encrypted.
- State Store: Pluggable backend for agent memory and workflow state. Redis, PostgreSQL, MongoDB, CosmosDB, etc. — 30+ options.
- LLM Provider: Configured via Dapr Conversation API component YAML. Swap providers without code changes.
Production-Grade Code Walkthrough
Here is a complete, production-grade Dapr Agents setup with a multi-agent support workflow. This is not a toy example — it mirrors the pattern used by ZEISS Vision Care for document data extraction workflows (presented at KubeCon EU 2026).
Step 1: Define the LLM provider (resources/llm-provider.yaml)
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: llm-provider
spec:
type: conversation.openai
version: v1
metadata:
- name: key
value: "${OPENAI_API_KEY}"
- name: model
value: gpt-4o-mini
- name: maxTokens
value: "4096"
Step 2: Define the state stores (resources/conversation-statestore.yaml)
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: conversation-statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: keyPrefix
value: "agent-memory"
Step 3: Define the workflow state store (resources/workflow-statestore.yaml)
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: workflow-statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: actorStateStore
value: "true"
Step 4: Write the agents (agents.py)
import asyncio
from dapr_agents import DurableAgent
from dapr_agents.agents.configs import (
AgentMemoryConfig,
AgentStateConfig,
AgentExecutionConfig,
ToolExecutionMode,
)
from dapr_agents.memory import ConversationDaprStateMemory
from dapr_agents.storage.daprstores.stateservice import StateStoreService
from dapr_agents.workflow.runners import AgentRunner
from dapr_agents.llm import DaprChatClient
from dapr_agents.tool import tool
from dapr_agents.tool.workflow import agent_to_tool
@tool
def search_knowledge_base(query: str) -> str:
"""Search the internal knowledge base for relevant documentation."""
# In production, this queries a vector database or search index
return f"Found 3 relevant documents for '{query}': [doc_123, doc_456, doc_789]"
@tool
def escalate_to_human(ticket_id: str, reason: str) -> str:
"""Escalate a support ticket to a human agent."""
# In production, this creates a ticket in Zendesk, Jira, or similar
return f"Ticket {ticket_id} escalated: {reason}"
# --- Triage Agent ---
triage_agent = DurableAgent(
name="TriageAgent",
role="Support Triage Specialist",
goal="Classify incoming support requests and route to the right resolver",
instructions=[
"Classify the issue as billing, technical, or general inquiry.",
"Extract the customer's account ID, product name, and error message.",
"Summarize the issue in 2-3 sentences for the next agent.",
"If the issue is urgent (security, outage), escalate immediately.",
],
tools=[search_knowledge_base, escalate_to_human],
llm=DaprChatClient(component_name="llm-provider"),
memory=AgentMemoryConfig(
store=ConversationDaprStateMemory(
store_name="conversation-statestore",
session_id="triage-session",
)
),
state=AgentStateConfig(
store=StateStoreService(store_name="workflow-statestore"),
),
execution=AgentExecutionConfig(
max_iterations=15,
tool_execution_mode=ToolExecutionMode.SEQUENTIAL,
),
)
# --- Resolution Agent ---
resolution_agent = DurableAgent(
name="ResolutionAgent",
role="Technical Support Engineer",
goal="Resolve technical support issues with step-by-step guidance",
instructions=[
"Use the triage summary to understand the issue context.",
"Search the knowledge base for relevant solutions.",
"Provide clear, actionable steps the customer can follow.",
"If the solution requires a code change, provide the exact code.",
"If unresolved after 3 attempts, escalate to human support.",
],
tools=[search_knowledge_base, escalate_to_human],
llm=DaprChatClient(component_name="llm-provider"),
memory=AgentMemoryConfig(
store=ConversationDaprStateMemory(
store_name="conversation-statestore",
session_id="resolution-session",
)
),
state=AgentStateConfig(
store=StateStoreService(store_name="workflow-statestore"),
),
execution=AgentExecutionConfig(
max_iterations=20,
tool_execution_mode=ToolExecutionMode.SEQUENTIAL,
),
)
# Register resolution agent as a tool the triage agent can call
resolution_tool = agent_to_tool(
"ResolutionAgent",
description="Technical Support Engineer. Goal: Resolve technical issues.",
target_app_id="resolution-agent",
)
# --- Orchestrator Agent ---
orchestrator_agent = DurableAgent(
name="SupportOrchestrator",
role="Support Workflow Orchestrator",
goal="Coordinate triage and resolution for every support ticket",
instructions=[
"Receive incoming support requests and pass them to triage.",
"Forward triage results to the resolution agent.",
"Track the status of each ticket through the workflow.",
"Report final resolution or escalation status.",
],
tools=[resolution_tool],
llm=DaprChatClient(component_name="llm-provider"),
memory=AgentMemoryConfig(
store=ConversationDaprStateMemory(
store_name="conversation-statestore",
session_id="orchestrator-session",
)
),
state=AgentStateConfig(
store=StateStoreService(store_name="workflow-statestore"),
),
)
Step 5: Run the agents
# Terminal 1: Triage agent
dapr run --app-id triage-agent --resources-path resources — python agents.py
# Terminal 2: Resolution agent
dapr run --app-id resolution-agent --resources-path resources — python agents.py
# Terminal 3: Orchestrator
dapr run --app-id orchestrator --resources-path resources — python agents.py
Step 6: Trigger the workflow
curl -X POST http://localhost:8001/agent/run \
-H "Content-Type: application/json" \
-d '{"task": "Customer alice@example.com reports unable to access dashboard after v3.2 update. Account: ACCT-4421."}'
Setup Instructions
# Prerequisites: Python 3.11+, Docker, Dapr CLI
dapr init
pip install dapr-agents
# Verify installation
dapr --version # Should show v1.14+
python -c "from dapr_agents import DurableAgent; print('OK')"
# Clone quickstarts for reference
git clone https://github.com/dapr/dapr-agents.git
cd dapr-agents/quickstarts/01-dapr-agents-fundamentals
How to Use Effectively
1. Start with DurableAgent, not Agent
Dapr Agents ships two agent types: Agent (standard, synchronous) and DurableAgent (workflow-backed, fault-tolerant). Always start with DurableAgent. The Agent class is deprecated for production use — it lacks workflow replay, automatic retries, and persistent state across restarts.
# Correct for production
from dapr_agents import DurableAgent
agent = DurableAgent(
name="MyAgent",
role="Assistant",
goal="Help users",
instructions=["Be helpful"],
llm=DaprChatClient(component_name="llm-provider"),
state=AgentStateConfig(
store=StateStoreService(store_name="workflow-statestore"),
),
)
# Avoid for production — no workflow durability
from dapr_agents import Agent # Deprecated for production
2. Configure memory and state separately
Dapr Agents separates conversation memory (what the agent remembers from past turns) from workflow state (where the workflow engine persists execution progress). Configure both explicitly.
from dapr_agents.agents.configs import AgentMemoryConfig, AgentStateConfig
from dapr_agents.memory import ConversationDaprStateMemory
from dapr_agents.storage.daprstores.stateservice import StateStoreService
agent = DurableAgent(
name="MyAgent",
role="Assistant",
goal="Help users",
instructions=["Be helpful"],
llm=DaprChatClient(component_name="llm-provider"),
memory=AgentMemoryConfig(
store=ConversationDaprStateMemory(
store_name="conversation-statestore",
session_id="my-agent-session",
)
),
state=AgentStateConfig(
store=StateStoreService(store_name="workflow-statestore"),
),
)
The conversation-statestore holds the agent’s chat history. The workflow-statestore holds workflow execution checkpoints. They can use different database backends — use Redis for fast conversation memory and PostgreSQL for durable workflow state.
3. Use tool execution modes strategically
When an LLM returns multiple tool calls in a single turn, you control how they execute:
from dapr_agents.agents.configs import AgentExecutionConfig, ToolExecutionMode
# Sequential: tools run one-by-one in LLM-returned order (default)
# Safe for tools with side effects or dependencies
execution=AgentExecutionConfig(
max_iterations=10,
tool_execution_mode=ToolExecutionMode.SEQUENTIAL,
)
# Parallel: all tools run concurrently
# Use when tools are independent (e.g., search multiple sources)
execution=AgentExecutionConfig(
max_iterations=10,
tool_execution_mode=ToolExecutionMode.PARALLEL,
)
Parallel mode reduces latency when tools are independent but requires idempotent tools. Sequential mode is safer for tools with side effects (database writes, API calls that mutate state).
4. Use context-aware logging to avoid duplicate workflow logs
Dapr Workflows use event sourcing, which means your code may re-execute during replay. Standard print() or logging.info() calls produce duplicate log lines on every replay. Use the built-in ContextAwareLogger:
from dapr_agents.utils import get_context_aware_logger
logger = get_context_aware_logger(__name__)
# This log line appears once, even if the workflow replays 5 times
logger.info("Processing support ticket...")
5. Leverage MCP auto-discovery for zero-config tool integration
Dapr Agents automatically discovers MCP (Model Context Protocol) server tools from the Dapr sidecar. You do not need to pass tools= if your MCP servers are configured as Dapr components:
# No tools= needed — MCP servers auto-discovered from sidecar
agent = DurableAgent(
name="MCPAgent",
role="Multi-tool assistant",
goal="Use any available MCP tool",
instructions=["Use available tools to help the user"],
llm=DaprChatClient(component_name="llm-provider"),
state=AgentStateConfig(
store=StateStoreService(store_name="workflow-statestore"),
),
)
Configure MCP servers as Dapr components:
apiVersion: dapr.io/v1alpha1
kind: MCPServer
metadata:
name: github-mcp
spec:
endpoint:
transport: streamable_http
target:
url: https://api.githubcopilot.com/mcp/
auth:
secretStore: kubernetes
scopes:
- my-agent-app
Use Cases
1. Customer Support Ticket Resolution
A multi-agent pipeline that triages, researches, and resolves support tickets with human escalation fallback.
When you’d use this: You run a SaaS platform with 10,000+ support tickets per month and want to automate first-line resolution while maintaining human oversight for complex cases.
Why Dapr Agents fits: The durable workflow engine guarantees every ticket is processed exactly once, even if the agent pod crashes mid-resolution. The virtual actor model lets you run one agent per active ticket without provisioning memory for 10,000 concurrent agents. The SPIFFE identity layer ensures that only authorized agents can access customer PII.
2. Document Data Extraction Pipeline
Extract structured data from unstructured documents (invoices, contracts, medical records) using a chain of specialized agents.
When you’d use this: You process 50,000 documents per day and need to extract fields, validate against business rules, and route to downstream systems.
Why Dapr Agents fits: ZEISS Vision Care demonstrated this exact pattern at KubeCon EU 2026. Each document gets its own agent instance. The workflow persists extraction progress per page, so a crash on page 47 of a 100-page document restarts at page 47, not page 1. The 30+ state store backends let you store extracted data directly in your existing database.
3. Warehouse Operations Automation
Agents that monitor inventory, predict stockouts, and optimize picking routes in real-time.
When you’d use this: You run a logistics warehouse with 100,000 SKUs and need real-time inventory optimization without a dedicated operations team monitoring dashboards 24/7.
Why Dapr Agents fits: A large EU logistics company runs this in production on-premises (air-gapped). The pub/sub messaging lets inventory sensors publish events that wake specific agent instances. The scale-to-zero model means 100,000 SKU-monitoring agents consume near-zero memory when inventory is stable, and activate in milliseconds when a stockout event fires.
4. Code Review and CI Pipeline Automation
An orchestrator agent that reviews pull requests, runs tests, and provides structured feedback.
When you’d use this: Your team processes 50+ PRs per day and wants automated code review that integrates with your existing CI/CD pipeline.
Why Dapr Agents fits: The deterministic workflow model maps naturally to CI pipelines (checkout -> lint -> test -> review -> report). Each PR gets a dedicated agent instance. If the CI runner crashes mid-pipeline, the workflow resumes from the last completed step. The MCP auto-discovery can wire in GitHub API tools without configuration.
5. Multi-Model Research Assistant
An agent that queries multiple LLMs (OpenAI, Anthropic, local Ollama) and synthesizes the best answer.
When you’d use this: You need to compare answers across models for accuracy, cost, or latency, or you want a fallback chain when one provider is degraded.
Why Dapr Agents fits: The Dapr Conversation API abstracts LLM calls behind a uniform interface. Configure multiple providers as separate Dapr components and swap them at the agent level or the workflow level. The circuit breaker built into the sidecar automatically routes around degraded providers. No code changes needed to add or remove LLM providers.
Cheat Sheet
| Aspect | Detail |
|---|---|
| Project | Dapr Agents — Python framework for production AI agents |
| License | Apache 2.0 |
| CNCF Status | Built on Dapr (CNCF incubating) |
| Latest Release | v1.0.5 (June 2026) |
| GitHub | github.com/dapr/dapr-agents (695 stars, 128 forks) |
| Language | Python 3.11+ |
| Install | pip install dapr-agents |
| Core Abstraction | Virtual Actor — each agent is a stateful, distributed actor |
| Agent Types | Agent (deprecated), DurableAgent (production, workflow-backed) |
| LLM Support | OpenAI, Anthropic, Mistral, NVIDIA, Hugging Face, Ollama, Azure |
| LLM Interface | Dapr Conversation API (swap via YAML, no code changes) |
| State Stores | 30+ backends: Redis, PostgreSQL, MongoDB, CosmosDB, etc. |
| Memory | In-memory, vector stores (Chroma, PGVector, Redis), Dapr state stores |
| Tool Integration | Python @tool decorator, MCP auto-discovery, agent-to-tool |
| Multi-Agent Patterns | Orchestrator-Workers, Prompt Chaining, Routing, Parallelization, Evaluator-Optimizer |
| Orchestration | Deterministic (child workflows) or Event-driven (pub/sub) |
| Distribution | Transparent via Dapr sidecar mesh |
| Identity | SPIFFE cryptographic identity, mTLS |
| Observability | OpenTelemetry, Prometheus, Zipkin (built-in, no instrumentation) |
| Hot-Reload | Update agent config at runtime via Dapr Configuration Store |
| Deployment | Kubernetes-native, also runs standalone with Docker |
| Managed Option | Diagrid Catalyst (RBAC, identity management, diagnostics) |
| Quickstarts | 8 progressive examples in GitHub repo |
| PyPI Downloads | 8,000+ monthly |
| Enterprise Users | ZEISS Vision Care, EU logistics company |
Vibe Coding Projects
Project 1: Personal Research Assistant
What it does: An agent that takes a research question, searches multiple sources (web, internal docs, knowledge base), and produces a synthesized summary with citations. It uses parallel tool execution to search all sources simultaneously, then a second LLM call to synthesize the results.
What you’ll learn: Configuring DurableAgent with parallel tool execution mode, wiring up MCP servers for web search and document retrieval, and using the ContextAwareLogger to handle workflow replay correctly.
Effort: 2-3 hours. One agent, three tools, one workflow.
Project 2: Multi-Agent Code Review Pipeline
What it does: Three agents in a pipeline — a linter agent checks code style, a security agent scans for vulnerabilities, and a reviewer agent provides architectural feedback. Each agent runs as a separate Dapr application. An orchestrator agent coordinates the pipeline via child workflows.
What you’ll learn: Running multiple Dapr applications with sidecars, using agent_to_tool for cross-agent delegation, configuring deterministic workflows with call_child_workflow, and setting up Zipkin tracing to visualize the pipeline.
Effort: 4-6 hours. Three agents, one orchestrator, one workflow definition.
Project 3: Event-Driven Inventory Monitor
What it does: An agent per SKU that monitors inventory levels via pub/sub events. When a stockout risk is detected, the agent publishes a restock recommendation to a broadcast topic. An orchestrator agent collects recommendations and generates a daily replenishment report.
What you’ll learn: Configuring pub/sub for agent communication, using AgentPubSubConfig with topic subscriptions, implementing scale-to-zero with thousands of agent instances, and using the Dapr state store for agent memory across deactivation/reactivation cycles.
Effort: 6-8 hours. One agent type (many instances), one orchestrator, pub/sub infrastructure.
Problems Solved Efficiently
| Problem Type | Why Dapr Agents Fits | When to Look Elsewhere |
|---|---|---|
| Long-running agent workflows | Durable execution with event sourcing guarantees completion across crashes | Your workflow is a single LLM call with no state — use a direct API call |
| High-volume agent instances | Virtual actor model runs thousands of agents on a single core | You need fewer than 10 agents and don’t need crash recovery — use a simple script |
| Multi-agent coordination | Built-in pub/sub, child workflows, and agent registry | Your agents are independent and don’t communicate — run them separately |
| Compliance-sensitive deployments | SPIFFE identity, mTLS, air-gap capable, 30+ state store options | You need a fully managed SaaS agent platform — consider Diagrid Catalyst |
| LLM provider flexibility | Dapr Conversation API abstracts providers behind YAML config | You are locked into one provider and never plan to switch — use their SDK directly |
| On-premises / air-gapped | No cloud dependency, runs with Docker or K8s, local models via Ollama | You need a browser-based agent builder for non-developers — look at LangFlow |
| Observability requirements | Built-in OpenTelemetry, Prometheus, Zipkin — no instrumentation code | You need application-level metrics not covered by Dapr’s built-in spans |
Architectural Tradeoffs
What we gained
- Scale-to-zero memory efficiency. 10,000 registered agents consume ~500 MB at idle. In-process frameworks would need 50-100 GB for the same count.
- Crash-proof execution. Every workflow step is persisted as an event. Recovery is automatic and sub-second for workflows under 50 steps.
- Transparent distribution. The sidecar mesh handles routing, discovery, and mTLS. Scaling from 1 to 100 nodes requires zero code changes.
- Vendor-neutral LLM integration. Swap providers by changing one YAML file. No code changes, no redeploy, no vendor lock-in.
- Built-in observability. OpenTelemetry spans, Prometheus metrics, and Zipkin tracing are emitted automatically. No instrumentation code required.
What we sacrificed
- Raw throughput. Dapr’s out-of-process sidecar architecture adds ~3-6 ms of latency per actor activation compared to in-process frameworks like Orleans or Proto.Actor. For most agent workloads (which involve LLM calls taking 1-30 seconds), this overhead is negligible. For sub-millisecond messaging patterns, it is not.
- Deployment complexity. Every agent process requires a
daprdsidecar. Local development needs Docker for the sidecar and state stores. This is a one-time setup cost, but it is higher thanpip install && python app.py. - Learning curve. The virtual actor model, Dapr component YAML, and workflow event sourcing are concepts most Python developers have not encountered. Expect a 2-3 day ramp-up for a team new to Dapr.
- State store dependency. Durable execution requires a state store (Redis, PostgreSQL, etc.). You cannot run DurableAgent without one. The
Agentclass (deprecated) works without a state store but lacks durability guarantees.
Real lesson from production: A team at KubeCon EU 2026 shared that their first Dapr Agents deployment failed because they used the default in-memory state store for workflow state. When the pod restarted during a 50-step workflow, all progress was lost. The fix was switching to Redis with
actorStateStore: "true"— a one-line YAML change. The lesson: the state store is not optional for production. Configure it before you write your first agent, not after your first crash.
Course-Style Deep Dive
Under the Hood: How the Virtual Actor Model Powers Dapr Agents
When you create a DurableAgent(name="SupportAgent"), the following happens:
-
Registration. The agent is registered with the Dapr Placement Service as a virtual actor with ID
SupportAgent. The Placement Service assigns it to a physical partition based on consistent hashing. -
Activation. When a message arrives (HTTP request, pub/sub event, child workflow call), the Placement Service checks if the actor is active on any node. If not, it activates the actor on the least-loaded node. Activation involves loading the actor’s state from the configured state store and instantiating the Python object. This takes ~3 ms at tp90.
-
Execution. The actor processes messages sequentially from its mailbox. Concurrency is impossible by design — no locks, no race conditions, no thread safety concerns. Each message is a turn in the agent’s reasoning loop.
-
Deactivation. After a configurable idle timeout (default 60 seconds), the actor is deactivated. Its state is persisted to the state store. The Python object is garbage collected. The actor’s logical identity remains registered with the Placement Service.
-
Reactivation. When a new message arrives, the Placement Service activates the actor again. The actor loads its persisted state and resumes processing. The agent does not know it was deactivated — it appears as a continuous session.
This cycle is invisible to your code. You write agents as if they live forever in memory. The runtime handles the lifecycle.
Advanced Pattern 1: Replay-Safe Agent Workflows
Dapr Workflows use event sourcing: every yield in a workflow function records an event. On recovery, the workflow replays from the beginning, but skips completed steps by returning the cached result from the event log. This means your code re-executes on every replay — and any side effect (API call, database write, log line) that is not wrapped in a yield will execute multiple times.
The fix is to ensure all side effects happen inside workflow activities or tool calls, which are tracked by the event log:
import dapr.ext.workflow as wf
from dapr_agents.workflow.decorators import workflow_entry
from dapr_agents.utils import get_context_aware_logger
logger = get_context_aware_logger(__name__)
@workflow_entry
def support_workflow(self, ctx: wf.DaprWorkflowContext, request: dict) -> str:
# SAFE: This runs once — the yield is tracked by the event log
triage_result = yield ctx.call_activity(
"run_triage",
input={"request": request},
)
# SAFE: Tool calls inside DurableAgent are tracked by the workflow
resolution = yield ctx.call_activity(
"run_resolution",
input={"triage": triage_result},
)
# UNSAFE: This would run on every replay
# send_email(request["customer"], resolution)
# SAFE: Wrap side effects in activities
yield ctx.call_activity(
"send_notification",
input={"customer": request["customer"], "message": resolution},
)
return resolution
Advanced Pattern 2: Agent-to-Agent Delegation with Dynamic Routing
When you have many specialized agents and need to route tasks dynamically, use the Agent Registry with an LLM-based orchestrator:
from dapr_agents.orchestrators import LLMOrchestrator
from dapr_agents.agents.configs import AgentPubSubConfig, AgentRegistryConfig
from dapr_agents.storage.daprstores.stateservice import StateStoreService
from dapr_agents.llm import DaprChatClient
orchestrator = LLMOrchestrator(
name="DynamicRouter",
llm=DaprChatClient(component_name="llm-provider"),
pubsub=AgentPubSubConfig(
pubsub_name="agent-pubsub",
agent_topic="orchestrator.requests",
broadcast_topic="orchestrator.broadcast",
),
registry=AgentRegistryConfig(
store=StateStoreService(store_name="agent-registry-store"),
team_name="support-team",
),
)
The LLMOrchestrator uses the LLM to decide which agent should handle each request based on the agent registry’s capability descriptions. Agents register themselves on startup and deregister on shutdown. The orchestrator discovers available agents dynamically — no hardcoded routing.
Production Considerations
-
State store sizing. The workflow state store grows with the number of active workflows and their step count. A 50-step workflow with 2 KB of state per step produces ~100 KB of event log data. For 10,000 concurrent workflows, budget 1 GB of state store capacity. Prune completed workflows after 7 days using a TTL policy.
-
Sidecar resource limits. Each
daprdsidecar consumes ~18 MB of RAM and ~2m CPU at idle. At 500 qps, it uses ~523m CPU and ~305 MB RAM. Budget 0.5 vCPU and 512 MB RAM per sidecar in Kubernetes resource requests. -
LLM rate limiting. The Dapr Conversation API supports configurable rate limiting, retry policies, and circuit breakers. Configure these in the LLM provider component YAML, not in your application code:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: llm-provider
spec:
type: conversation.openai
version: v1
metadata:
- name: key
value: "${OPENAI_API_KEY}"
- name: model
value: gpt-4o-mini
- name: maxRetries
value: "3"
- name: retryBackoff
value: "1s"
- name: circuitBreakerEnabled
value: "true"
- name: circuitBreakerThreshold
value: "5"
- Agent identity scoping. Use Dapr’s scope configuration to restrict which agents can call which LLM providers and state stores. This prevents a compromised agent from accessing another agent’s memory:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: conversation-statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
scopes:
- triage-agent
- resolution-agent
The Results
| Metric | Before (In-Process Framework) | After (Dapr Agents) |
|---|---|---|
| Agent instances per node | 100 (memory-bound) | 10,000+ (scale-to-zero) |
| Memory per 1,000 idle agents | 50-100 GB | ~50 MB |
| Crash recovery | Manual restart, state lost | Automatic replay, <2s recovery |
| Distribution setup | Custom routing, weeks of work | Sidecar mesh, zero code changes |
| LLM provider swap | Code change + redeploy | YAML change, hot-reload |
| Observability setup | Manual instrumentation | Built-in OpenTelemetry |
| State store options | 1-2 (in-memory + optional Redis) | 30+ pluggable backends |
| Security model | Application-level auth | SPIFFE + mTLS, zero-trust |
| Multi-agent patterns | Single-process only | Deterministic + event-driven |
What this means for you: If you are building AI agents for production — not a demo, not a prototype, but a system that must survive pod restarts, traffic spikes, and multi-node deployments — Dapr Agents is the only framework that does not require you to build half the infrastructure yourself. The virtual actor model is not a feature. It is the foundation. Everything else (durable execution, transparent distribution, built-in observability) follows from that choice.
What to Watch Out For
Beginner Advice
-
Do not skip the state store setup. The most common production failure with Dapr Agents is using the default in-memory state store for workflow state. Configure Redis or PostgreSQL with
actorStateStore: "true"before you write your first agent. The one-time YAML setup saves you from losing hours of workflow progress on the first pod restart. -
Use DurableAgent, not Agent. The
Agentclass is deprecated for production. It lacks workflow replay, automatic retries, and persistent state. Every quickstart and example in the docs usesDurableAgent. Follow that pattern. -
Separate conversation memory from workflow state. These serve different purposes and can use different backends. Use Redis for fast conversation memory (low latency, high throughput). Use PostgreSQL for durable workflow state (transactional guarantees, long-term retention).
-
Test crash recovery early. Kill your agent process mid-workflow and verify it resumes correctly. Do this in development, not after deployment. The
ContextAwareLoggerwill show you if your code produces duplicate log lines on replay — a sign that side effects are not properly wrapped in activities. -
Start with sequential tool execution. Parallel mode is tempting for performance, but it requires idempotent tools. If a tool creates a database record or sends an email, parallel execution can produce duplicates. Start sequential, profile, then switch to parallel for read-only tools.
Lessons Learned
“We deployed Dapr Agents with the default in-memory state store because we ‘just wanted to see it work.’ The first pod restart during a 50-step workflow lost all progress. The fix was adding
actorStateStore: 'true'to our Redis component YAML — a one-line change. We should have done that before writing the first agent, not after the first crash.” — Production engineer, KubeCon EU 2026
“The learning curve for the virtual actor model is real. Our team spent two days understanding why agents ‘disappeared’ after 60 seconds of inactivity. The answer: that’s the default deactivation timeout. They come back on the next message with their state intact. Once you internalize that agents are logical entities, not running processes, everything clicks.” — Platform team lead, EU logistics company
“MCP auto-discovery is the feature that sold our architecture review board. We have 15 internal microservices, each with its own API. Instead of writing 15 tool wrappers, we exposed each service as an MCP server. Dapr Agents discovered them automatically. Zero configuration. That’s the kind of integration that makes a framework production-ready.” — Staff engineer, ZEISS Vision Care
Getting Started
- Install Dapr CLI and initialize:
dapr init - Install the Python package:
pip install dapr-agents - Clone the quickstarts:
git clone https://github.com/dapr/dapr-agents.git - Run quickstart 01 (LLM Client) to verify your setup:
cd quickstarts/01-dapr-agents-fundamentals && dapr run --app-id llm-client --resources-path resources — python 01_llm_client.py - Progress through quickstarts 02-08 in order. Each builds on the previous.
- For production, configure Redis or PostgreSQL state stores before writing custom agents.
- Join the Dapr community Discord for troubleshooting and patterns.
Next in the Open-Source AI Tools Mastery series: Orloj
Written by Nivant Labs Team
Engineer at Nivant Labs