Storage backends
Durable orchestrations need persistent state for the event history, timers, and orchestration metadata. A storage provider is the durable runtime's source of truth: it accepts deterministic decisions from the orchestrator, persists them, and feeds ready work back to the worker loop. The public repository includes ready-to-run examples for both SQLite and PostgreSQL, but you can also bring your own implementation.
Provider responsibilities
Every storage backend implements the IStateStore contract exposed by Asynkron.DurableFunctions. At a minimum it must:
- Persist
DurableFunctionStateinstances that contain the orchestration history, timers, and outstanding activities. - Return states that are due for execution via
GetReadyStatesAsyncso the runtime can resume orchestrators on schedule. - Support idempotent writes: orchestrator replays will retry the same
SaveStateAsyncpayload until it succeeds. - Delete terminal instances when
RemoveStateAsyncis called or when automatic cleanup is enabled.
Writing a custom provider
- Implement
IStateStore. Provide concrete implementations forSaveStateAsync,GetStateAsync,GetReadyStatesAsync, andRemoveStateAsync. The methods should use transactions so partial updates do not leak through during process crashes. - Register the provider. Expose it through dependency injection so
DurableFunctionRuntimecan resolve it. In ASP.NET Core apps that usually meansservices.AddSingleton<IStateStore, YourStore>();. - Handle serialization. The runtime serializes orchestration payloads to JSON. Store them using UTF-8 text or a binary column that preserves the JSON value verbatim.
- Optimize ready-state queries. Add indexes that support lookups by
ExecuteAfter,InstanceId, and status flags so polling remains efficient under load. - Add migrations. Ensure the schema exists at startup. The built-in providers run migrations when the store is constructed; mirror that behavior if your backend requires table creation.
Tip: start from the
SqliteStateStoreimplementation to understand the minimal contract before layering on concurrency features.
Supporting multi-host concurrency
When multiple workers share the same database you need optimistic concurrency in addition to the basic CRUD contract. Implement IConcurrentStateStore, which extends IStateStore with lease-aware operations, and host it with ConcurrentDurableFunctionRuntime.
Key responsibilities include:
- Lease acquisition via
TryClaimLeaseAsyncso only one host executes an instance at a time. - Lease renewal for long-running orchestrations with
RenewLeaseAsync. - Lease release on completion or failure through
ReleaseLeaseAsync. - Claimable state queries that ignore instances whose leases are still valid.
The concurrent stores add Version, LeaseOwner, and LeaseExpiresAt columns to the DurableFunctionStates table and keep them in sync with each operation. Conflicts are expected: always retry when TryClaimLeaseAsync reports a version mismatch.
Lease-based coordination
Leases guard against duplicate execution when multiple hosts poll the same backend. Each worker includes its unique host identifier when claiming a lease. The store atomically updates the row to include the owner, an expiry timestamp, and the next expected version. If the worker crashes or loses connectivity, the lease expires and another host can safely take over.
While an orchestration runs, the runtime renews the lease in the background to keep ownership alive.
When renewal fails due to a version conflict, the runtime stops processing the instance and lets another worker claim it after the expiry window.
SQLite
Use SQLite when you need a self-contained database. The example runs entirely in-memory, but the same connection string works with file-backed databases as well.
var connectionString = "Data Source=:memory:";
using var stateStore = new SqliteStateStore(connectionString, loggerFactory.CreateLogger<SqliteStateStore>());
var runtime = new DurableFunctionRuntime(stateStore, logger, loggerFactory: loggerFactory);
Recommendations:
- Enable
Cache=Shared;Journal Mode=WAL;when multiple worker processes share the same.dbfile. - Switch to
ConcurrentSqliteStateStorefor multi-host safety; it adds versioning columns and handles leases for you. - Back up the
.dbfile or use a network share if you need durability beyond the local disk.
PostgreSQL
PostgreSQL is the preferred option when you deploy to Kubernetes or run several orchestrator hosts. The guide ships with a Docker Compose file that bootstraps PostgreSQL 16 and pgAdmin.
docker-compose up -d
Connect with the default credentials:
- Host:
localhost - Port:
5432 - Database:
durablefunctions - Username:
durableuser - Password:
durablepass
Wire it into your host using the extension methods.
builder.Services.AddDurableFunctionsWithPostgreSQL(connectionString, options =>
{
options.PollingIntervalMs = 100;
options.MaxConcurrentWorkflows = 10;
});
When the app starts, the provider ensures the schema exists. Concurrency-aware stores add version, lease_owner, and lease_expires_at columns plus the indexes they require.
Schema hygiene
Both relational providers support automatic cleanup:
- Completed orchestrations are removed when
DurableOrchestrationOptions.AutoDeleteOnCompletionis enabled. - Use the HTTP management API
DELETE /runtime/orchestrations/{instanceId}to purge on demand. - Keep an eye on the
DurableFunctionStatestable size. If it grows beyond expectations, check for orchestrations that never complete or forget to release leases.