Skip to content
These docs cover Strata 0.9.0-preview.1.It is a preview, and the API can still change before 1.0.

Use Redis

In this guide you keep a graph’s checkpoints in Redis. You:

  1. set up Redis so that it keeps what Strata writes;
  2. create the Redis checkpoint store, with a key prefix;
  3. run a thread and look at the keys it writes;
  4. tune retries, and let finished threads expire;
  5. register the store in a host, read it with the strata CLI, and share it between C# and Rust.

Choose a store compares Redis with the other stores. This page doesn’t repeat that comparison.

  • Install Strata as shown in Installation. In .NET, the Redis stores are in Datalium.Strata.Storage, which Datalium.Strata.Hosting brings. In Rust, they are in datalium-strata-memory.
  • You need a single Redis primary. The stores are tested against Redis 8. Redis Cluster and Sentinel aren’t supported (see Known limitations).

A checkpoint survives a Redis restart only if Redis writes it to disk. Strata can’t change that: it depends on Redis’s own persistence settings.

  • Turn on the append-only file (AOF). With appendonly yes and the default appendfsync everysec, a crash of the machine loses at most about the last second of writes. appendfsync always writes each command to disk before it returns, and is slower.
  • RDB snapshots alone save on a schedule. A crash loses everything written since the last snapshot, including threads that were waiting.
  • Keep maxmemory-policy at noeviction. When Redis is full, it then refuses the save, and the run fails, instead of evicting a waiting thread’s checkpoints. An allkeys-* policy can evict any key Strata wrote.

This starts a Redis 8 container with AOF turned on, and its data in a Docker volume:

Terminal window
docker run -d --name strata-redis -p 6379:6379 -v strata-redis-data:/data redis:8 redis-server --appendonly yes

For throwaway development servers, see Running Postgres and Redis locally.

Give the store a URL of the form redis://[:password@]host[:port][/db], and a key prefix. Every key the store writes starts with the prefix, strata by default. Give each application or environment its own prefix, so they can share one Redis.

using Datalium.Strata.Storage.Redis;
using var store = await RedisCheckpointStore.ConnectAsync("redis://localhost:6379", new RedisStoreOptions
{
Prefix = "acme",
FinishedThreadTtl = TimeSpan.FromDays(30),
});

ConnectAsync also takes a StackExchange.Redis configuration string, such as redis.internal:6380,password=…,ssl=true. That is how you connect over TLS. It connects before it returns, and fails after 5 seconds if the server can’t be reached. The store owns the connection and closes it when you dispose it. It indexes the prefix on its first call.

To share your application’s own multiplexer, give the store a database from it. The store then doesn’t own the connection, and sends nothing until its first call:

using StackExchange.Redis;
var multiplexer = await ConnectionMultiplexer.ConnectAsync("localhost:6379");
var store = new RedisCheckpointStore(multiplexer.GetDatabase(), new RedisStoreOptions { Prefix = "acme" });

Your multiplexer’s own settings apply, such as TLS certificates and timeouts.

The FinishedThreadTtl / with_finished_thread_ttl line is optional. Let finished threads expire explains it.

This program runs a refund graph against the store. Prepare runs, the thread waits for approval, then Pay runs. The graph uses the EveryNode policy, so the run writes a checkpoint after each node and a completed one at the end (see When checkpoints are written). It approves refund-1, and leaves refund-2 waiting.

using Datalium.Strata.Graph;
using Datalium.Strata.Interruption;
using Datalium.Strata.Storage.Redis;
var graph = new StateGraph<Refund>()
.AddNode("Prepare", refund => refund with { Status = "awaiting approval" })
.AddInterruptBoundary("Prepare")
.AddNode("Pay", refund => refund with { Status = "paid" })
.AddEdge("Prepare", "Pay")
.Compile(new CompilationOptions { Checkpointing = CheckpointPolicy.EveryNode });
using var store = await RedisCheckpointStore.ConnectAsync("redis://localhost:6379", new RedisStoreOptions
{
Prefix = "acme",
FinishedThreadTtl = TimeSpan.FromDays(30),
});
var paused = await graph.RunAsync("refund-1", new Refund("Ada", 42m, "new"), store);
Console.WriteLine($"refund-1: {paused.GetType().Name}");
var done = await graph.ResumeAsync("refund-1", HumanSignal.Approve("lead"), store);
Console.WriteLine($"refund-1: {done.GetType().Name}");
var waiting = await graph.RunAsync("refund-2", new Refund("Grace", 7m, "new"), store);
Console.WriteLine($"refund-2: {waiting.GetType().Name}");
public sealed record Refund(string Customer, decimal Amount, string Status);

Both programs print:

refund-1: Interrupted
refund-1: Completed
refund-2: Interrupted

List the keys under the prefix with redis-cli --scan --pattern 'acme:*':

acme:checkpoints:refund-1
acme:checkpoints:refund-2
acme:threads
acme:threads:completed
acme:threads:indexed
acme:threads:interrupted

Redis has no tables to create or migrate. Each thread is one Redis list, and a few sorted sets index the threads for listing:

Key What it holds
{prefix}:checkpoints:{thread_id} The thread’s log: one JSON checkpoint per save, oldest first.
{prefix}:threads Every thread, ordered by the time of its latest checkpoint.
{prefix}:threads:{state} The threads in one state, such as interrupted or completed. Redis deletes a set when it is empty.
{prefix}:threads:waking The waiting threads with a timer or a timeout.
{prefix}:threads:indexed A marker: the prefix has been indexed.
{prefix}:leases:{thread_id}, {prefix}:lease-tokens:{thread_id} A thread’s lease, while one is held, and its latest token.
{prefix}:blobs:{thread_id} The thread’s offloaded values, when you turn offload on.
{prefix}:history:{session_id} A session’s chat history, from the history store.

The full layout is in schema/README.md.

Each save is one Lua script. The script appends the checkpoint and moves the thread to the right index sets. Redis runs a script atomically, so any number of writers, from either engine, can save to one prefix without a lock. Conditional and fenced saves (see Threads and checkpoints) are scripts too, with their check in the same step. Still run the workers that can reach the same thread under thread leases.

The index needs current writers. The first store that uses a prefix indexes the threads already in it. After that, every save keeps the index up to date. If something else wrote to the prefix, such as an older version of Strata, RebuildIndexAsync() / rebuild_index() indexes every thread again from its latest checkpoint. It also removes threads whose lists are gone, and returns how many threads it indexed. It is safe to run while others write.

The store repeats a call that fails for a transient reason: a lost connection, a timeout, or a server that answers LOADING, BUSY or TRYAGAIN. By default it makes 4 attempts, waiting 50 ms at first and doubling up to 2 s, with random jitter. Any other error fails the call at once.

A save that fails after Redis may have run its script is looked up before it is tried again. If the checkpoint is already in the list, the save counts as done, so it is never written twice. A history append is repeated only if it never reached the server, so a failed append can still have been written.

You don’t manage reconnects. In .NET, the StackExchange.Redis multiplexer replaces a dropped connection. In Rust, the store holds a connection that replaces itself: the call that found it dropped fails and is retried, and calls made meanwhile wait for the new connection.

To change the policy:

using Datalium.Strata.Storage;
using Datalium.Strata.Storage.Redis;
using var store = await RedisCheckpointStore.ConnectAsync("redis://localhost:6379", new RedisStoreOptions
{
Prefix = "acme",
Retry = new StoreRetryPolicy { MaxAttempts = 6, MaxDelay = TimeSpan.FromSeconds(5) },
});

StoreRetryPolicy.None turns retries off.

Each repeated attempt adds one to the strata.store.retries counter, with the store and the operation as tags. In .NET it is on the Datalium.Strata.Storage meter. In Rust it goes through the metrics crate.

By default, nothing Strata writes to Redis expires. With FinishedThreadTtl (C#) or with_finished_thread_ttl (Rust), Redis deletes a finished thread for you:

  • A save that leaves the thread completed or ended gives its list, and its offloaded values, that time to live.
  • A save that leaves it in any other state removes the expiry, so a thread ID that runs again doesn’t expire mid-run.
  • The save script sets the expiry in the same step as the save.

After the run above, refund-1 expires in 30 days, and the waiting refund-2 never does:

$ redis-cli TTL acme:checkpoints:refund-1
(integer) 2592000
$ redis-cli TTL acme:checkpoints:refund-2
(integer) -1

Under the default Boundaries policy, a run that finishes writes no completed checkpoint, so only threads you end get the expiry. A thread that was resumed and then finished stays in Redis, in the resumed state. Use EveryNode, as above, or include resumed in the states a purge takes.

An expired thread leaves the index sets the next time a listing comes across it, or when you call RebuildIndexAsync() / rebuild_index().

Every writer to a prefix with a ttl must run a current version of Strata. A writer that doesn’t send the ttl neither sets nor clears an expiry.

Expiry or purge. The ttl runs inside Redis, with no job to schedule, and is fixed per store. PurgeAsync / purge, on every store, deletes finished threads older than a cutoff you choose at call time, and the host’s retention driver can call it on a schedule. You can use both. Control storage size covers purge and retention.

RedisHistoryStore keeps each session’s messages in one list, {prefix}:history:{session_id}. You create it the same way, with RedisHistoryStore.ConnectAsync(url, options) in C# or RedisHistoryStore::connect_with(url, options) in Rust (see Choose a store). It uses the prefix and the retry policy from the options, and ignores the rest.

History never expires, and retention doesn’t purge it. Delete old sessions’ lists yourself (see Known limitations). State explains what belongs in history.

AddRedisCheckpoints() and AddRedisHistory() read the Strata:Redis section, and the connection string ConnectionStrings:strata-redis:

builder.Services.AddStrata()
.AddRedisCheckpoints() // Strata:Redis, ConnectionStrings:strata-redis
.AddRedisHistory();
{
"ConnectionStrings": {
"strata-redis": "localhost:6379"
},
"Strata": {
"Redis": {
"Prefix": "acme",
"FinishedThreadTtl": "30.00:00:00"
}
}
}

The connection string can be a redis:// URL or a StackExchange.Redis configuration string. To use an IConnectionMultiplexer you registered yourself, set UseRegisteredMultiplexer to true. The section also takes Retry, Deltas and Offload. When the host starts, it calls the store once, so a Redis that can’t be reached stops the host from starting.

Hosting covers the rest of the host: drivers, health checks and shutdown.

Give the strata CLI the Redis URL. Add ?prefix= when your prefix isn’t strata. The prefix parameter is the CLI’s own: in your application, set the prefix in the options.

Terminal window
strata thread list --storage "redis://localhost:6379?prefix=acme"

After the run above:

+-----------+--------------+-------------+-------------+-------------+----------------------+
| Thread ID | Current Node | Sequence ID | State | Waiting For | Updated At |
+-----------+--------------+-------------+-------------+-------------+----------------------+
| refund-2 | Prepare | 1 | interrupted | - | 2026-10-30T14:47:50Z |
+-----------+--------------+-------------+-------------+-------------+----------------------+
| refund-1 | Pay | 3 | completed | - | 2026-10-30T14:47:50Z |
+-----------+--------------+-------------+-------------+-------------+----------------------+

The CLI connects through the Rust store, so it can’t reach a Redis that requires TLS. See the CLI reference.

Both engines use the same keys, JSON and scripts. A thread that a .NET service paused under a prefix can be resumed by a Rust service with the same prefix, and the other way round. Two things must hold:

  • Both state types serialize to the same JSON. In C#, pin each property’s name with [JsonPropertyName] (see Sharing a thread across languages).
  • Every writer to the prefix runs the same version of Strata. Redis has no migrations: the index and expiry logic is in each writer’s scripts (see Changing stores later).

Demo 12-pluggable-storage has freeze and resume modes that hand a thread between the engines through Redis: C#, Rust.