Diagrid
Back to Blog

What's New in Dapr 1.19: Faster Workflows, New State APIs, and a Simpler Control Plane

Dapr 1.19 makes workflows faster with stateful worker streams and an opt-in fast path that cuts median step latency from 553 ms to 153 ms, lets the Scheduler take over actor placement, and adds alpha Binary Store, search, vector, and MCP server APIs.

Marc Duiker

Marc Duiker

Developer Advocate

October 22, 202617 min read

Dapr 1.19 is available today, and it makes workflows faster, the control plane simpler, and adds new state APIs! A workflow worker now receives only the new history events on each step, a new opt-in fast path cuts median workflow step latency from 553 ms to 153 ms, and the Scheduler can take over actor placement so you run one fewer control-plane service. This release also adds a Binary Store building block for large files, alpha search and vector APIs, and a Dapr MCP server that exposes your sidecar to AI agents, and the .NET, Java, Go, and Python SDKs gain list, history, and rerun workflow management APIs. These changes matter most for long-running AI agents built on Dapr Workflow, which run for hours and fan out into dozens of parallel tasks.

Why it matters

A durable workflow records every step in its history. When a process crashes or a pod restarts, the workflow replays that history and continues where it stopped. That is what lets an AI agent run all afternoon and split into 50 parallel tasks without losing its place. The catch is that the history keeps growing while the workflow runs, and the sidecar sent all of it to the worker on every step, so a workflow that had run for hours paid more per step in latency, memory use, and network traffic.

The control plane had a different kind of overhead. The Placement and Scheduler services both tracked cluster membership and both pushed state to sidecars, so you ran two stateful services and two Raft clusters for work that overlapped. The split also sat on the hot path. Actor reminders and workflow jobs are stored in the Scheduler, but only Placement knew which sidecar hosts the actor they target, so each firing needed a second lookup before it reached the right host. Workflows depend on this path on every step because each wake-up and activity is a Scheduler job.

The same agents also need to work with data that doesn't fit in a state store. They keep large files, search documents, and look up context by similarity to feed a model, and each of those used to mean a different SDK and query language for each storage, search engine, or vector database. The new Binary Store, search, and vector APIs give you one API for each job, and you swap the backing service by changing a Dapr component, as you already do for state and pub/sub. The Dapr MCP server goes the other direction: it lets an agent or an IDE assistant call your sidecar's building blocks as tools, without you writing the glue.

What's new in Dapr 1.19

Workflow

Stateful worker stream: send only what's new

In 1.19, the workflow worker keeps the history it already holds for each instance, and the sidecar sends only the events that are new since the worker last saw that instance. The data sent per step no longer depends on how long the workflow has been running: an agent that has been working for hours and fans out into 50 tasks sends roughly the same small payload on step 500 as on step 5.

The worker still replays the full history locally, so the deterministic replay model doesn't change; only the transfer shrinks. After a reconnect, the sidecar sends the full history once. If the worker has evicted an instance from its cache, it fetches the full history from the sidecar. Either case adds latency but doesn't change the result, and the worker then continues with deltas. Read more in the stateful workflow client docs.

The Python, Java, .NET, and Go SDK workers turn this on by default (Go from SDK v1.16.0), so you get it by upgrading the SDK. Because the worker now keeps each instance's history in memory, you can turn it off if worker memory is limited: set disable_stateful_history=True on WorkflowRuntime in Python, call WorkflowRuntimeBuilder.withStatefulHistoryDisabled(true) in Java, or set WorkflowRuntimeOptions.DisableStatefulHistory in .NET. Workers that don't use it keep receiving the full history, so existing apps work as before.

WorkflowsFastPath: a faster route through the engine (preview)

Every workflow wake-up in Dapr Workflow goes through a durable Scheduler reminder for each event. That makes recovery reliable, but it adds a round trip to the Scheduler on every step. With the new WorkflowsFastPath preview feature, a wake-up runs on the host that armed it. A repeating janitor reminder for each instance keeps the work durable, so a host crash doesn't lose it.

In engine benchmarks, the fast path reduced median workflow step latency from 553 ms to 153 ms and increased fan-out throughput.

The feature is off by default in 1.19. To try it, enable it in your Dapr Configuration:

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: appconfig
spec:
  features:
    - name: WorkflowsFastPath
      enabled: true

If you've configured the workflow concurrency limits from 1.18, the fast path stays off and the sidecar logs the reason. The fast path runs work items directly on the sidecar, so it would bypass the limits that the Scheduler enforces.

Workflow management APIs in the .NET, Java, Go, and Python SDKs

In 1.19, the .NET, Java, Go, and Python SDKs expose three workflow management operations that were previously available only through direct gRPC calls:

  • List workflow instance IDs, with pagination.
  • Get the full execution history of a workflow instance.
  • Rerun a failed workflow instance from a specific event in its history.

You can use them to find stuck or failed instances, inspect what happened, and repair them without querying the state store. See how to manage workflows in the Dapr docs. During local development, the Dapr Dev Dashboard gives you a visual view of the same workflow state and history.

Cross-app operations, access policy, and detached workflows

A workflow can now start a detached workflow: a fully independent, top-level instance with no link back to the parent. The parent gets the new instance ID right away and continues without waiting, and the detached workflow's result or failure doesn't affect the parent. Here's what it looks like in the .NET SDK:

public sealed class ParentWorkflow : Workflow<string, string>
{
    public override async Task<string> RunAsync(WorkflowContext context, string orderId)
    {
        // Starts a fire-and-forget workflow and returns its instance ID.
        string instanceId = await context.ScheduleNewDetachedWorkflowAsync(
            nameof(AuditWorkflow),
            input: orderId);
        return instanceId;
    }
}

Other workflow changes in 1.19:

  • Cross-app workflow operations now let you start, get, raise an event on, pause, resume, terminate, purge, and rerun a workflow owned by another app in the same namespace. Listing instance IDs and reading history stay local, and rerun is available through the SDK workflow clients only. This suits an API or operations service that manages workflows owned by another app without registering them itself. If no workflow worker is connected for the target app, these operations now fail at once with a clear error instead of retrying for 20 seconds. The target app's WorkflowAccessPolicy decides which operations each caller may perform.
  • WorkflowAccessPolicy has a new requires block that allows an operation only when specific events appear in the caller's propagated workflow history.
  • Scheduling a workflow through an SDK workflow client with the ID of an active (not completed) instance fails with the gRPC AlreadyExists status code (in the sidecar since 1.18.2), so you can detect the conflict without parsing the error message. That makes starting a workflow with a fixed instance ID an idempotent call. Java throws a WorkflowInstanceAlreadyExistsException. The runtime workflow API (HTTP and gRPC StartWorkflowBeta1) keeps its error response.

A simpler control plane with actor placement in the Scheduler

Dapr 1.19 adds an opt-in mode in which the Scheduler also serves actor placement, so the standalone Placement service isn't deployed. You run one fewer stateful service and one fewer Raft cluster.

The change also affects how actors behave at runtime:

  • Actor reminders and workflow jobs are routed faster, because the Scheduler knows which sidecar owns each actor and sends each reminder or job directly to it, instead of looking up the owner in a separate service first. Workflow wake-ups and activities travel this route, so every workflow step benefits.
  • When a host joins or leaves, Dapr locks and updates only the affected actor types instead of pausing the placement table for the whole namespace. Actor types that the change doesn't touch keep serving traffic.
  • Placement uses rendezvous hashing instead of a consistent-hash ring. Rendezvous hashing balances actors evenly without tuning and moves the smallest possible set of actors when hosts change, so more of the cluster keeps running undisturbed during a rolling restart or scale event.

To turn the mode on, set the Helm value once every sidecar in the cluster runs 1.19:

helm upgrade dapr dapr/dapr --namespace dapr-system \
  --reuse-values \
  --set global.scheduler.placement.enabled=true

Before you enable it:

  • Upgrade every sidecar to 1.19 or later. Once no Placement service remains, an older sidecar's actor and workflow APIs stall until you upgrade its pod.
  • Keep global.scheduler.enabled, global.actors.enabled, and global.actors.serviceName=placement set, because the chart doesn't render without them.
  • Pass the value on every later helm upgrade, because Helm resets values you don't pass.
  • Expect actors to be deactivated and reactivated once on their new hosts each time you switch, in either direction. Actor state isn't lost.

You can switch the mode on or off without restarting sidecars. In self-hosted mode, run dapr init --scheduler-placement to start the Scheduler with placement and skip the dapr_placement container. Placement served by the Scheduler stays opt-in in 1.19, and the standalone Placement service remains the default.

Binary Store building block (alpha)

The new Binary Store building block stores files through one API, for content that is too large for a state store. You create a binarystore.* component and call the alpha API over HTTP (/v1.0-alpha1/binarystore/{name}/{fileName}, using PUT or POST, GET, and DELETE) or over gRPC, which streams files in and out.

Here is a minimal setup with the in-memory component, which is meant for local development and doesn't keep files across sidecar restarts. Save this as components/binary-store.yaml:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: mystore
spec:
  type: binarystore.in-memory
  version: v1
  metadata:
    - name: maxSize
      value: "16Mi"

Then start a sidecar with dapr run --app-id binary-store-example --resources-path ./components --dapr-http-port 3500 and send the file as raw bytes:

# Create the file. POST returns 409 Conflict if it already exists.
curl -X POST http://localhost:3500/v1.0-alpha1/binarystore/mystore/report.pdf \
  --data-binary @./report.pdf
 
# Replace the file
curl -X PUT http://localhost:3500/v1.0-alpha1/binarystore/mystore/report.pdf \
  --data-binary @./updated-report.pdf
 
# Download the file
curl http://localhost:3500/v1.0-alpha1/binarystore/mystore/report.pdf -o downloaded-report.pdf
 
# Delete the file
curl -X DELETE http://localhost:3500/v1.0-alpha1/binarystore/mystore/report.pdf

Successful uploads and deletes return 204 No Content, and a missing file returns 404 Not Found. In .NET, the DaprBinaryStoreClient does the same with SetBinaryFileAsync("mystore", "report.pdf", content). Pass overwrite: true to replace an existing file.

Components cover Amazon S3 and S3-compatible stores, Azure Blob Storage, Azure Data Lake Storage, Google Cloud Storage, and OCI Object Storage, plus an in-memory component for tests. Only the .NET SDK supports Binary Store in 1.19 (see the .NET SDK docs). From other languages, call the HTTP or gRPC API directly. Because the API is alpha, expect it to change.

Search and vector APIs (alpha)

Two new alpha building blocks cover search and similarity queries. The search API indexes JSON documents in full-text search engines and queries them through one API, with a portable filter language, sorting, highlighting, pagination, and an optional provider-native query for engine-specific features. The vector API stores pre-embedded vectors in collections with fixed dimensions and a distance metric (cosine, dot product, or Euclidean), then returns the closest records to a query vector or to an existing record, with metadata filters, score thresholds, and batch queries.

Both APIs use keyed upserts, so retrying a request is safe, and a write can return as soon as the engine accepts it or wait until it completes. They expect index-ready data: text extraction, chunking, and embedding generation stay in your application. For retrieval-augmented generation, pair the vector API with the conversation API to send the retrieved context to an LLM.

Here is a search request that runs a full-text query for dapr, keeps only guides, sorts by price, and highlights matches in the title, followed by a vector request that finds the two Teslas closest to a query vector:

# Search the "articles" index of the "mysearch" component
curl -X POST http://localhost:3500/v1.0-alpha1/search/mysearch/indexes/articles/query \
  -H "Content-Type: application/json" \
  -d '{
        "query": {
          "text": "dapr",
          "filter": { "category": "guide" },
          "sort": [ { "field": "price", "order": "SORT_ORDER_ASC" } ],
          "highlightFields": [ "title" ],
          "topK": 5
        }
      }'
 
# Query the "cars" collection of the "myvectors" component
curl -X POST http://localhost:3500/v1.0-alpha1/vector/myvectors/collections/cars/query \
  -H "Content-Type: application/json" \
  -d '{
        "query": {
          "vector": { "values": [1, 0, 0, 0] },
          "topK": 2,
          "filter": { "make": "tesla" }
        }
      }'

The index and collection must exist and contain data first. Document content and record payload are bytes, so the HTTP API expects them base64-encoded when you write them. The search how-to and vector how-to walk through creating an index or collection and writing data.

In 1.19, Meilisearch is the only engine, through the search.meilisearch and vector.meilisearch components, and the vector component supports cosine similarity only. The SDKs don't support these APIs yet, so call the HTTP API (/v1.0-alpha1/search and /v1.0-alpha1/vector) or gRPC directly. See the search API reference and vector API reference.

Dapr MCP server (alpha)

The Dapr MCP server turns a sidecar's building blocks into Model Context Protocol tools, so any MCP client can call them: Claude Desktop, Cursor, Claude Code, VS Code, Dapr Agents, or your own agent. It reads the sidecar's metadata and registers tools for every building block that has a component loaded, including state, pub/sub, bindings, secrets, service invocation, actors, distributed lock, cryptography, and conversation. Tools follow component hot reloading, and each one carries MCP safety annotations.

The MCP server is a separate binary in the dapr/dapr-mcp-server repository with its own versions and release cycle, and it is alpha, so tool names and configuration can still change. It runs over stdio by default, or over streamable HTTP with --http.

Authentication is off until you set AUTH_MODE (oidc, spiffe, dapr-sentry, or hybrid). Turn it on whenever you serve over HTTP to anything other than localhost. The server can reach whatever its sidecar can reach, and it returns tool results to the model, including secret values, so scope its components and secrets to what the agent needs.

Faster and leaner under load

The nightly performance tests on master (8 runs from 23 September to 2 October 2026, medians), compared with the published v1.18.0 run on the same cluster, show:

  • Jobs scheduling: p99 latency is 33% lower (39.5 ms to 26.3 ms), and the test now sustains the full requested 1000 QPS (921 QPS on 1.18.0).
  • Actor reminder registration: p99 latency is 71% lower (148 ms to 43 ms), at 43% more registrations per second.
  • Workflows: throughput is 40% higher in TestWorkflowWithConstantVUs and 32% higher in TestSeriesWorkflowWithMaxVUs.

Service invocation, state, and pub/sub publish show no change beyond run-to-run noise. The workflow engine now uses event-driven work-item completion, which contributes to these numbers.

The Scheduler also keeps far less history. It now retains 10,000 etcd revisions instead of 1,000,000. Every revision keeps the payload of the job it wrote, and in one 15-hour chaos test the old default produced a 2.7 GB database and 3.4 GB of memory use for 19 MB of live jobs. If you set dapr_scheduler.etcdCompactionRetention yourself, your value stays.

Actors and runtime

You can now list and get the timers registered for an actor, over both gRPC and HTTP, and the HTTP API gains an endpoint to list an actor's reminders, matching what gRPC already offered. See the actor timers and reminders docs and the actors API reference:

GET http://localhost:3500/v1.0/actors/<actorType>/<actorId>/timers
GET http://localhost:3500/v1.0/actors/<actorType>/<actorId>/reminders

Timer ownership is also stricter. When an actor moves to another host, its in-memory timers on the old host now stop, where before they kept firing and the actor ran twice. A timer operation sent to a host that doesn't own the actor fails with ERR_ACTOR_TIMER_NOT_OWNED, which can surface during rebalancing.

More in 1.19

  • Kafka transactions: Kafka pub/sub gets atomic bulk publish, read_committed consumers, and an opt-in consume-transform-produce mode. All of it is off by default.
  • Logging: control-plane binaries and daprd accept multiple log outputs, file rotation, and a configurable timestamp format. See the logging docs.
  • Security: four fixes for service invocation, the Scheduler, and workflows, which also shipped in 1.18.6. Three of them change behavior, so review them in the release notes.

Before you upgrade

1.19 includes behavior changes that can affect an upgrade, mostly in the SDKs. The ones to check first:

  • Helm users: apply the CRDs first. helm upgrade doesn't update CRDs. Until you apply the 1.19 Configuration and WorkflowAccessPolicy CRDs, the API server drops the new requires field, so a rule that uses it applies without that gate. The upgrade steps cover this.
  • Scheduler RBAC. The Scheduler now needs get, list, and watch on pods, whether or not you enable Scheduler placement. The Helm chart applies this. If you manage RBAC yourself, add it.
  • Scheduler restarts. The Scheduler now retries in the process instead of exiting when its embedded store is briefly unavailable, so alerts on Scheduler pod restarts no longer see that condition. If it can't bind its port, it still exits and Kubernetes restarts it.
  • Service invocation hardening. Methods that contain encoded slashes or dots (%2F, %5C, %2E) now fail, and the actor invoke API rejects reserved dapr.internal.* actor types.
  • Metrics endpoint. It answers only GET and HEAD. Prometheus scrapes are unaffected.
  • Python SDK. The extension packages are no longer published separately. Remove packages such as dapr-ext-workflow and dapr-ext-fastapi, then install the matching extras, for example pip install "dapr[fastapi,workflow]".
  • Java SDK. Spring integration requires Spring Boot 4.0.
  • Rollback. Don't roll back from 1.19 directly to 1.17.6 or earlier. Go to 1.18.x, or through 1.17.7 first, because older Sentry versions can't read the Ed25519 trust bundle.

Read the breaking changes and deprecations page and the release notes for the full list.

How to get started with Dapr 1.19

Locally with the Dapr CLI

To upgrade Dapr on your local machine, reinitialize it with the latest runtime:

  1. Uninstall Dapr: dapr uninstall --all
  2. Install Dapr: dapr init
  3. Verify the installation: dapr -v

To install the new Dapr CLI, follow the steps in the self-hosted upgrade guide.

On Kubernetes with the Dapr Ops Dashboard

The Dapr Ops Dashboard from Diagrid manages Dapr upgrades on your Kubernetes clusters and typically supports new Dapr releases within 24 hours of their public release. Follow the Dapr Ops Dashboard upgrade guide to move your clusters to 1.19. If you upgrade with Helm instead, apply the CRDs first, as described above. Once every sidecar runs 1.19, you can enable Scheduler-based actor placement.

Summary

Dapr 1.19 keeps the cost of a workflow step flat no matter how long the workflow has been running, gives you an opt-in fast path that cuts median step latency from 553 ms to 153 ms, and lets you run actor placement from the Scheduler so your control plane has one fewer stateful service. It also adds an alpha Binary Store for large files, alpha search and vector APIs, and an alpha Dapr MCP server for AI agents. The fast path and Scheduler-based placement are opt-in, so you can adopt them on your own timeline. To upgrade on your local machine, follow the self-hosted upgrade guide. On Kubernetes, you can upgrade Dapr with the Dapr Ops Dashboard. Use the Dapr Dev Dashboard to inspect your workflows locally, and if you're new to Dapr Workflow or agents, try the free courses at Dapr University.

This post is not a complete list of features and changes in 1.19. Read the official Dapr release notes for the full list of features, fixes, breaking changes, and upgrade guidance.