State Persistence
All Flowtide streams require a persistent storage solution to function. It is responsible for persisting the data at checkpoint intervals to ensure the stream can continue to operate in case of a failure.
The recommended storage backend is the Reservoir storage system. It is designed for cloud-native deployments and provides a multi-tier architecture with an in-flight buffer, local disk cache, and persistent object storage (such as Azure Blob Storage). For details on the internal architecture, see Reservoir Storage System.
Legacy storage backends (FasterKV, temporary file cache, SQL Server) are still available but are not recommended for new deployments.
Reservoir Storage
Reservoir storage packs pages into large immutable files (default 64 MB), uses CRC32/CRC64 checksums for data integrity, and maintains a local disk cache for near-SSD read latency while the authoritative state lives in cloud object storage.
Azure Blob Storage
Store persistent state in Azure Blob Storage. This is the recommended configuration for production deployments.
Install the FlowtideDotNet.Storage.Azure NuGet package and use one of the AddAzureBlobStorage extension methods.
Connection string:
builder.Services.AddFlowtideStream("yourstream")
// ...
.AddStorage(s =>
{
s.AddAzureBlobStorage(
connectionString: "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net",
containerName: "flowtide-state",
directoryPath: "mystream",
localCacheDirectory: "/cache/flowtide"
);
});
Token credential (e.g. Managed Identity / DefaultAzureCredential):
s.AddAzureBlobStorage(
storageUri: new Uri("https://myaccount.blob.core.windows.net"),
containerName: "flowtide-state",
tokenCredential: new DefaultAzureCredential(),
directoryPath: "mystream"
);
SAS credential:
s.AddAzureBlobStorage(
storageUri: new Uri("https://myaccount.blob.core.windows.net"),
containerName: "flowtide-state",
sasCredential: new AzureSasCredential("sv=..."),
directoryPath: "mystream"
);
Shared key credential:
s.AddAzureBlobStorage(
storageUri: new Uri("https://myaccount.blob.core.windows.net"),
containerName: "flowtide-state",
sharedKeyCredential: new StorageSharedKeyCredential("myaccount", "key"),
directoryPath: "mystream"
);
Options delegate (full control):
For advanced scenarios — including supplying a custom BlobContainerClient via ClientFactory — use the options delegate overload:
s.AddAzureBlobStorage(opt =>
{
opt.ConnectionString = "...";
opt.ContainerName = "flowtide-state";
opt.DirectoryPath = "mystream";
opt.LocalCacheDirectory = "/cache/flowtide";
// Or provide a fully custom client:
// opt.ClientFactory = () => new BlobContainerClient(...);
});
Azure Blob Storage Options
| Property | Default | Description |
|---|---|---|
ConnectionString | — | Azure Storage connection string. |
ServiceUri | — | Blob service endpoint URI (used with credential options). |
ContainerName | — | Name of the blob container. |
DirectoryPath | null | Sub-directory within the container. |
LocalCacheDirectory | System temp folder | Local directory for the disk cache. |
TokenCredential | — | Azure token credential (e.g. DefaultAzureCredential). |
SasCredential | — | SAS credential for authentication. |
SharedKeyCredential | — | Shared key credential for authentication. |
ClientFactory | — | Factory function that returns a custom BlobContainerClient. Takes priority over all other authentication options. |
AddAzureBlobStorage returns an IReservoirBuilder for further tuning — see Reservoir Builder Options below.
Local File Storage
Store persistent state to a local directory. Useful for single-node deployments or when cloud storage is not needed.
builder.Services.AddFlowtideStream("yourstream")
// ...
.AddStorage(s =>
{
s.AddFileStorage("/persistence/flowtide");
});
AddFileStorage returns an IReservoirBuilder for further tuning — see Reservoir Builder Options below.
Temporary Development Storage
For development and testing, use temporary storage that is automatically cleaned up when the application exits.
builder.Services.AddFlowtideStream("yourstream")
// ...
.AddStorage(s =>
{
s.AddTemporaryStorage();
// Or specify a custom directory
// s.AddTemporaryStorage("/tmp/flowtide-dev");
});
Temporary storage is not suitable for production. All data is deleted when the application exits.
Reservoir Builder Options
All reservoir storage providers (AddAzureBlobStorage, AddFileStorage, AddTemporaryStorage) return an IReservoirBuilder that exposes shared tuning options:
builder.Services.AddFlowtideStream("yourstream")
// ...
.AddStorage(s =>
{
var reservoir = s.AddAzureBlobStorage(
connectionString: "...",
containerName: "flowtide-state"
);
reservoir.SetMaxDataFileSize(64 * 1024 * 1024);
reservoir.SetCacheSize(10L * 1024 * 1024 * 1024);
reservoir.SetSnapshotCheckpointInterval(20);
reservoir.OldStreamVersionsRetention(-1);
});
| Method | Default | Description |
|---|---|---|
SetMaxDataFileSize(int) | 64 MB | Maximum size, in bytes, of each data file uploaded to storage. Larger values reduce API call count at the cost of larger individual uploads. |
SetCacheSize(long) | 10 GB | Maximum total size, in bytes, of the local disk cache. LRU eviction kicks in at 80% capacity. |
SetSnapshotCheckpointInterval(int) | 20 | Number of incremental checkpoints between full snapshot checkpoints. Lower values speed up recovery at the cost of more writes. |
OldStreamVersionsRetention(int) | -1 (keep all) | Number of previous stream versions to retain. Only applies when versioning is explicitly configured on the stream builder via AddVersioningFromPlanHash(), AddVersioningFromString(), or AddVersioningFromAssembly(). Without versioning, the stream uses a single default version and this setting has no effect. The current version is always preserved; this setting controls how many previous versions are kept alongside it. A value of 0 deletes all old versions immediately, 1 keeps one previous version, and -1 retains all versions indefinitely. |
SetCache(IReservoirStorageProvider) | Provider-specific | Set a custom cache storage provider. |
DisableCache() | Cache enabled | Disable the local disk cache entirely. Not recommended for cloud storage providers. |
Using FlowtideBuilder (without Dependency Injection)
When configuring a stream directly with FlowtideBuilder, create a ReservoirPersistentStorage instance and pass it through StateManagerOptions:
using FlowtideDotNet.Storage.Persistence.Reservoir;
using FlowtideDotNet.Storage.Persistence.Reservoir.Internal;
using FlowtideDotNet.Storage.Persistence.Reservoir.LocalDisk;
var reservoirBuilder = new ReservoirBuilder();
reservoirBuilder.SetStorage(new LocalDiskProvider("/persistence/flowtide"));
// Optionally set cache, snapshot interval, max file size, etc.
// reservoirBuilder.SetSnapshotCheckpointInterval(10);
builder.WithStateOptions(new StateManagerOptions()
{
PersistentStorage = new ReservoirPersistentStorage(reservoirBuilder),
TemporaryStorageOptions = new FileCacheOptions()
{
DirectoryPath = "./temp"
}
});
FasterKV storage (Legacy)
FasterKV storage is a legacy option. Consider using Reservoir Storage for new deployments.
FasterKV is a persistent key value store built by Microsoft. FasterKV is highly configurable, and how you configure it will affect the performance of your stream.
To configure your stream to use FasterKV storage, add the following to the builder:
builder
.WithStateOptions(() => new StateManagerOptions()
{
PersistentStorage = new FasterKvPersistentStorage(new FasterKVSettings<long, SpanByte>()
{
// Set the fasterKV configuration here
...
})
});
Useful configuration options
| Property | Description |
|---|---|
| LogDevice | The log device that will write to storage |
| MemorySize | How much memory FasterKV can use |
| PageSize | how large a page is |
| CheckpointDir | Where checkpoints should be stored |
| CheckpointManager | Checkpoint manager, useful if using Azure Storage. |
Storing to disk
This is an example of a configuration to store to a disk.
var baseDirectory = "/persistence/"
builder.WithStateOptions(() => new StateManagerOptions()
{
// Set cache page count to reduce the memory usage
CachePageCount = 10000,
PersistentStorage = new FasterKvPersistentStorage(new FasterKVSettings<long, SpanByte>()
{
// Checkpoint directory
CheckpointDir = $"{baseDirectory}/checkpoints",
// A local file log device
LogDevice = Devices.CreateLogDevice($"{baseDirectory}/log"),
// Redice memory usage of fasterKV, to limit memory usage
MemorySize = 1024L * 1024L * 64,
// Page size
PageSize = 1024 * 1024 * 16,
}),
TemporaryStorageOptions = new FileCacheOptions()
{
// Path where the temporary cache is stored
DirectoryPath = $"./temp"
}
})
Storing to Azure Storage
Storing the data in an Azure Storage requires a bit more configuration, especially a checkpoint manager.
// Create azure storage device
var log = new AzureStorageDevice(STORAGE_STRING, BASE_CONTAINER, "", "hlog.log");
// Create azure storage backed checkpoint manager
var checkpointManager = new DeviceLogCommitCheckpointManager(
new AzureStorageNamedDeviceFactory(STORAGE_STRING),
new DefaultCheckpointNamingScheme($"{BASE_CONTAINER}/checkpoints/"));
builder.WithStateOptions(() => new StateManagerOptions()
{
// Set cache page count to reduce the memory usage
CachePageCount = 10000,
PersistentStorage = new FasterKvPersistentStorage(new FasterKVSettings<long, SpanByte>()
{
CheckpointManager = checkpointManager,
LogDevice = log,
// Redice memory usage of fasterKV, to limit memory usage
MemorySize = 1024L * 1024L * 64,
// Page size
PageSize = 1024 * 1024 * 16,
}),
TemporaryStorageOptions = new FileCacheOptions()
{
// Path where the temporary cache is stored
DirectoryPath = $"./temp"
}
})
Temporary file cache storage (Legacy)
This is a legacy option. Consider using Temporary Development Storage for new projects.
This storage solution is useful when developing or running unit tests on a stream. All data will be cleared between each run, but it will be persisted to local disk to reduce RAM usage and allow you to run streams with alot of data.
The implementation of this is using the same solution as the intermediate file cache solution where modified pages are stored between checkpoints.
To configure your stream to use this storage solution, add the following to the stream builder:
builder
.WithStateOptions(() => new StateManagerOptions()
{
// This is non persistent storage, use FasterKV persistence storage instead if you want persistent storage
PersistentStorage = new FileCachePersistentStorage(new FlowtideDotNet.Storage.FileCacheOptions()
{
DirectoryPath = "./tmp"
})
});
Configuration
| Property | Default value | Description |
|---|---|---|
| DirectoryPath | ./data/tempFiles | Path where the files will be stored |
SQL Server storage (Experimental)
SQL Server storage support is still experimental.
Store persistent data to sql server.
Before using this storage solution you must manually create required tables using this creation script: Sql tables creation script.
The sql user running the system requires the following specific permissions:
SELECTINSERTDELETEUPDATE
builder.Services.AddFlowtideStream("yourstream")
[...]
.AddStorage(s =>
{
// register sql server storage using default settings
s.AddSqlServerStorage("[connectionstring]");
// register sql server storge using function to retrieve the connection string.
s.AddSqlServerStorage(() => "[connectionstring]");
// or use the overload to specify more settings
s.AddSqlServerStorage(new SqlServerPersistentStorageSettings()
{
ConnectionStringFunc = () => builder.Configuration.GetConnectionString("[connectionstring]"),
// if you created the tables on a non default schema (or with another name) you can specify the full name for the tables used here.
// it's also possible to specify the database name as part of table name.
StreamTableName = "[MySchema].[Streams]",
StreamPageTableName = "[MyDatabase].[MySchema].[StreamPages2]"
});
});
Storage Architecture
When using FasterKV or file cache storage, the stream storage is built on a three-tier architecture: the in-memory cache, the local disk modified page cache, and the persistent data.
When using Reservoir storage, the architecture expands to five tiers: RAM pages (LRU), disk spillover, an in-flight buffer, a local disk cache, and persistent cloud storage. See Reservoir Storage System for details.
A data page is fetched using the following logic:
Compression
It is possible to compress pages in the state. The option that exist today is to compress pages with Zstd. Most storage backends add zstd compression by default to save on network throughput and storage size.
To set compression, it is set under add storage:
builder.AddStorage(b => {
...
// Use zstd page compression
b.ZstdPageCompression();
// Use no compression even if the storage medium added compression
b.NoCompression();
});