Edit

Data persistence and serialization in Durable Functions for Azure Functions

The Durable Functions runtime automatically persists function parameters, return values, and other state to the task hub to provide reliable execution. However, the amount and frequency of data persisted to durable storage can impact application performance and storage transaction costs. Depending on the type of data your application stores, data retention and privacy policies may also need to be considered.

This article explains what data gets persisted, how to handle large payloads and sensitive data, and how to customize serialization for each supported language.

In this article:

Task hub contents

Task hubs store the current state of instances, and any pending messages:

  • Instance states store the current status and history of an instance. For orchestration instances, this state includes the runtime state, the orchestration history, inputs, outputs, and custom status. For entity instances, it includes the entity state.
  • Messages store function inputs or outputs, event payloads, and metadata that is used for internal purposes, like routing and end-to-end correlation.

Messages are deleted after being processed, but instance states persist unless they're explicitly deleted by the application or an operator. In particular, an orchestration history remains in storage even after the orchestration completes.

For an example of how states and messages represent the progress of an orchestration, see the task hub execution example.

Where and how states and messages are represented in storage depends on the storage provider. Use Durable Task Scheduler because it provides a managed backend for task hubs and handles the underlying state store for you. However, Azure Storage remains a solid option for existing workloads and for apps that want to manage their own storage resources.

Storage provider How state is stored Recommended use
Durable Task Scheduler Orchestration and entity state are stored in the managed scheduler backend behind a task hub resource. Preferred option for new Durable Functions apps and managed deployments.
Azure Storage State and messages are represented in queues, tables, and blobs in an Azure Storage account. Good fit for existing apps or deployments that already rely on Azure Storage.

Types of data that are serialized and persisted

The following list shows the different types of data that will be serialized and persisted when using features of Durable Functions:

  • All inputs and outputs of orchestrator, activity, and entity functions, including any IDs and unhandled exceptions
  • Orchestrator, activity, and entity function names
  • External event names and payloads
  • Custom orchestration status payloads
  • Orchestration termination messages
  • Durable timer payloads
  • Durable HTTP request and response URLs, headers, and payloads
  • Entity call and signal payloads
  • Entity state payloads

For guidance on managing payload size and protecting sensitive items in this list, see the following sections.

Keep Durable Functions inputs and outputs small

You can run into memory issues if you provide large inputs and outputs to and from Durable Functions APIs. Inputs and outputs are serialized into the orchestration history, which means that large payloads can, over time, greatly contribute to unbounded history growth. This growth risks causing memory exceptions during replay.

To mitigate the impact of large inputs and outputs, you can:

  • Delegate work to sub-orchestrators to load balance the history memory burden across multiple orchestrators, keeping the memory footprint of individual histories small.
  • Store large data in external storage (such as Azure Blob Storage) and pass lightweight identifiers that allow you to retrieve that data inside activity functions when needed.

For the Durable Task Scheduler, use large payload support to offload larger payloads to Azure Blob Storage. For new apps, this pattern is recommended when an orchestration must pass large payloads between durable operations. If you use the Azure Storage provider, you can still apply the claim-check pattern shown in the following section and pass lightweight references between operations.

Tip

The best practice for dealing with large data is to keep it in external storage and materialize that data only inside activities, when needed.

Pass references to large payloads

Choose the pattern that fits your storage provider.

Durable Task Scheduler

If you use Durable Task Scheduler, enable large payload support so the runtime writes larger payloads to Azure Blob Storage and sends a small reference through the scheduler. A typical configuration is shown in the scheduler docs:

{
  "version": "2.0",
  "extensions": {
    "durableTask": {
      "storageProvider": {
        "type": "azureManaged",
        "connectionStringName": "DTS_CONNECTION_STRING",
        "payloadStorageEnabled": true,
        "payloadStorageThresholdBytes": 262144
      },
      "hubName": "%TASKHUB_NAME%"
    }
  }
}
Azure Storage

With the Azure Storage provider, you can use the Claim Check pattern to keep orchestration history small while still processing large payloads. The orchestrator passes a lightweight reference that contains the blob container and blob name, and the activity reads or writes the payload from Azure Blob Storage as needed.

The following examples assume that you already have a blob in your storage account. Start the orchestration with a reference such as {"container":"large-payloads","blobName":"input/job-123.json"}. The activity creates the processed-payloads output container if necessary. The sample processing step copies the input bytes unchanged; replace it with your application logic.

Important

Never include storage credentials or a shared access signature (SAS) in the reference. The system persists the reference in orchestration history. These examples use an app setting named PAYLOAD_STORAGE_CONNECTION_STRING to keep the storage code concise. For production workloads, use Microsoft Entra ID to authorize access to blob data.

This example requires the Azure.Storage.Blobs NuGet package.

using System;
using System.Threading.Tasks;
using Azure.Storage.Blobs;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;

public record BlobReference(string Container, string BlobName);

public static class LargePayloadFunctions
{
    [Function("ProcessLargePayload")]
    public static async Task<BlobReference> RunOrchestrator(
        [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        BlobReference inputReference = context.GetInput<BlobReference>()
            ?? throw new InvalidOperationException("A blob reference is required.");

        return await context.CallActivityAsync<BlobReference>(
            nameof(ProcessLargePayloadActivity), inputReference);
    }

    [Function(nameof(ProcessLargePayloadActivity))]
    public static async Task<BlobReference> ProcessLargePayloadActivity(
        [ActivityTrigger] BlobReference inputReference)
    {
        string connectionString =
            Environment.GetEnvironmentVariable("PAYLOAD_STORAGE_CONNECTION_STRING")
            ?? throw new InvalidOperationException("Payload storage is not configured.");
        BlobServiceClient service = new BlobServiceClient(connectionString);

        BlobClient inputBlob = service
            .GetBlobContainerClient(inputReference.Container)
            .GetBlobClient(inputReference.BlobName);
        BinaryData inputData = (await inputBlob.DownloadContentAsync()).Value.Content;

        BlobContainerClient outputContainer =
            service.GetBlobContainerClient("processed-payloads");
        await outputContainer.CreateIfNotExistsAsync();

        string outputName = $"processed/{Guid.NewGuid():N}.json";
        await outputContainer.GetBlobClient(outputName)
            .UploadAsync(inputData, overwrite: true);

        return new BlobReference(outputContainer.Name, outputName);
    }
}

If parallel activities produce multiple large results, return a list of references and pass that list to a final aggregation activity. The aggregation activity should load and combine the payloads and then write one final output blob. Don't load or concatenate the large results in the orchestrator.

Work with sensitive data

Inputs and outputs (including exceptions) to and from Durable Functions APIs are durably persisted in your storage provider of choice. If those inputs, outputs, or exceptions contain sensitive data (such as secrets, connection strings, or personally identifiable information), anyone with read access to your storage provider's resources could obtain them.

To safely handle sensitive data, fetch that data within activity functions from either Azure Key Vault or environment variables, and never communicate that data directly to or from orchestrators or entities. This approach helps prevent sensitive data from leaking into your storage resources.

Similarly, write access to storage resources must be tightly controlled, as tampered data in storage could alter orchestration behavior. For more information about securing task hub storage, see Secure your task hub storage.

Tip

This guidance also applies to the CallHttp orchestrator API, which persists its request and response payloads in storage. If your target HTTP endpoints require authentication, implement the HTTP call inside an activity, or use the built-in managed identity support offered by CallHttp, which doesn't persist credentials to storage.

Note

Avoid logging data containing secrets as anyone with read access to your logs (for example in Application Insights) could obtain those secrets.

Encryption at rest

When using the Azure Storage provider, all data is automatically encrypted at rest. However, anyone with access to the storage account can read the data in its unencrypted form. If you need stronger protection for sensitive data, consider first encrypting the data using your own encryption keys so that the data is persisted in its pre-encrypted form.

Alternatively, .NET users have the option of implementing custom serialization providers that provide automatic encryption. An example of custom serialization with encryption can be found in this GitHub sample.

Note

If you decide to implement application-level encryption, be aware that orchestrations and entities can exist for indefinite amounts of time. This matters when it comes time to rotate your encryption keys because an orchestration or entities may run longer than your key rotation policy. If a key rotation happens, the key used to encrypt your data may no longer be available to decrypt it the next time your orchestration or entity executes. Custom encryption is therefore recommended only when orchestrations and entities are expected to run for relatively short periods of time.

Secure your task hub storage

The storage backend that hosts your task hub is a critical trust boundary. The Durable Task Framework trusts data it reads from storage during orchestration replay and message processing. Anyone with write access to the task hub storage can tamper with orchestration state, pending messages, or stored payloads. This may alter application behavior, trigger unintended actions, or achieve remote code execution within the context of your function app.

Important

Don't expose your task hub storage credentials or grant write access to untrusted parties. Write access to task hub storage can be used to alter application behavior, including triggering arbitrary code execution.

Shared responsibility

Securing the storage backend is your responsibility, the same as securing any database that stores application state or code. The Durable Task Framework doesn't perform integrity verification on stored data, so it relies on the storage layer's access controls to prevent unauthorized modifications.

Backend Security responsibility Guidance
Durable Task Scheduler Microsoft manages the underlying storage backend. You manage identities, task hub access, and app-level security. Preferred default for new Durable Functions apps.
Azure Storage and other BYO providers You manage the storage account or database and its security controls. Good fit for existing workloads or deployments that already rely on Azure Storage.

Note

Don't share a single task hub between untrusted tenants. A task hub doesn't enforce access boundaries between its users, so any tenant that can read or write to the task hub can affect all orchestrations and entities within it. Similarly, don't rely on separate task hubs within the same backend as a security boundary. While Durable Task Scheduler supports RBAC scoped to individual task hubs, network controls such as IP allow lists and private endpoints apply only at the scheduler level, so task hubs within a scheduler aren't a security isolation boundary. The same is true for BYO storage providers—any tenant with access to the storage account or database can reach all task hubs on that backend. When you need security isolation between tenants, provision separate infrastructure for each tenant: separate storage accounts or databases for BYO providers, or separate Durable Task Scheduler instances.

Storage hardening checklist

Apply the following best practices to protect your task hub storage:

  • Use identity-based connections for the backend you choose.

    • With Durable Task Scheduler, rely on managed identities and RBAC for the scheduler and task hubs.
    • With Azure Storage and other BYO providers, prefer managed identity over connection strings where possible.

    See Configure a managed identity for Durable Functions.

  • Apply least-privilege RBAC roles. Grant only the minimum permissions required. Avoid granting broad storage access to users or services that don't need it.

  • Restrict network access to your storage account or scheduler deployment by using private endpoints or service endpoints. This restriction helps prevent unauthorized network-level access to task hub data.

  • Monitor storage access by enabling Azure Monitor resource logs for your storage account, especially the StorageWrite log category. Route these logs to a destination outside of the monitored storage account, such as Log Analytics, so they can't be tampered with. See Storage logs.

  • Rotate credentials regularly if you use connection strings. Treat storage account keys with the same care as any other high-privilege credential.

  • Consider a managed storage backend. Durable Task Scheduler handles storage security automatically, including authentication, RBAC, and network isolation, while Azure Storage offers explicit storage control.

Customize serialization and deserialization

Serialization customization options vary by language. Select your language tab to see the available options.

.NET Isolated and System.Text.Json

Durable Functions running in the .NET Isolated worker process use the same object serializer configured globally for your Azure Functions app (see WorkerOptions). This serializer is System.Text.Json by default rather than Newtonsoft.Json. Any changes to WorkerOptions.Serializer transitively apply to Durable Functions.

For more information on the built-in support for JSON serialization in .NET, see the JSON serialization and deserialization in .NET overview documentation.

Next steps