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.

Choose a store

A checkpoint store is where Strata saves your threads. Strata ships four in each language: in-memory, SQLite, Postgres and Redis. Your graph code is the same with each of them, so you choose by where you run, how many processes write, and what must survive a restart. This page compares the stores, says when to use each, and shows how to create them.

Threads and checkpoints explains what a store keeps.

In-memory SQLite Postgres Redis
What you run Nothing A file. SQLite comes with the package and the crate. A Postgres server A Redis server, a single primary
Survives a restart No Yes Yes Yes, if Redis saves to disk (RDB or AOF)
Who can write One process The processes of one machine, one write at a time Many processes on many machines Many processes on many machines
Retries a transient failure — No Yes Yes
Checkpoints shared by C# and Rust — Yes Yes Yes
History store Yes Yes, but C# and Rust can’t read each other’s Yes, shared Yes, shared
Long-term memory store Yes Yes, with the sqlite-vec extension Yes, with pgvector Yes, with the Redis Query Engine (Redis 8 or Redis Stack)
Thread listing, leases and purge Yes Yes Yes Yes, plus an optional expiry for finished threads
Group commit — Yes Yes No
Delta checkpoints and value offload — Yes Yes Yes
TLS — — .NET only .NET only
The strata CLI reads it No Yes Yes Yes

A dash means the row doesn’t apply to that store.

Strata Cloud. A fifth store, a hosted one, is for the Strata Cloud private beta. The strata CLI can’t read it, and the retention driver doesn’t run against it. See Known limitations.

What the rows mean:

  • Who can write. The Postgres store takes a lock per thread before it appends, so concurrent writers wait instead of colliding. The Redis store appends with one script per save, which Redis runs atomically. SQLite lets one write through at a time per file. If two processes make a plain save to the same thread at once, one of them fails, and the store doesn’t retry it. On any store, run the workers that can reach the same thread under thread leases.
  • Retries. The Postgres and Redis stores repeat a call that failed for a transient reason, such as a dropped connection: 4 attempts by default, waiting 50 ms at first and doubling up to 2 s. A save that may already have been written is looked up first, so it is never written twice. Retry in the store’s options (C#) or with_retry (Rust) changes the policy.
  • Shared by C# and Rust. Both engines write the same tables and keys, so a thread that one engine paused can be resumed by the other. SQLite history is the exception: the two engines write it to different tables, so neither sees the other’s messages. See Architecture.
  • History and long-term memory. These are separate stores on the same backend. State explains what goes where. In .NET, you add the sqlite-vec extension yourself (see Known limitations).
  • TLS. The .NET Postgres store takes sslmode in its URL, or your own NpgsqlDataSource. The .NET Redis store takes a StackExchange.Redis configuration string with ssl=true, or your own IDatabase. The Rust stores, and the CLI, don’t connect over TLS. Known limitations gives the workarounds.

Use it for tests, examples and development. Nothing survives the process, and the store keeps every waiting thread’s checkpoint in memory. It also suits a run that never needs to outlive its process: under the default Boundaries policy, a run that goes straight through writes nothing anyway.

Use it for a service on one machine, a desktop or command-line tool, or development where threads must survive a restart. There is no server to run: the store is a file. Several processes on the machine can share the file, but their writes take turns, so SQLite doesn’t scale to many writers.

A SQLite file is also the simplest way to hand a paused thread from one engine to the other, as demo 09-cross-language-interop does (C#, Rust). Keep history that both engines read in Postgres or Redis.

Use it when several workers or machines run threads against one database, or when you already run Postgres. Concurrent writers wait on a lock per thread instead of failing. Group commit raises throughput with many writers (see Performance). The store retries transient failures and brings its tables up to date when it opens. With pgvector, long-term memory can live in the same database.

In Rust, the store doesn’t connect over TLS.

Use it when you want the shortest round trip per save, with many writers. It had the shortest one in the benchmarks. Two things to check first:

  • Whether a checkpoint survives a Redis restart depends on Redis’s own persistence settings (RDB or AOF), not on Strata. A Redis that doesn’t save to disk loses every waiting thread when it restarts.
  • The store needs a single Redis primary. Redis Cluster and Sentinel aren’t supported.

Finished threads can expire on their own (see Retention). In Rust, rediss:// URLs don’t work.

With the EveryNode policy, every node costs one store round trip. On the benchmark machine, a ten-node run took about 3 ms on Redis, 12 to 14 ms on Postgres and 25 ms on SQLite. With 64 writers, group commit saved about 15 times as many checkpoints a second on SQLite, and 1.8 to 2.5 times as many on Postgres. Performance has the full results and their caveats.

You create the stores once, at startup, and pass the checkpoint store to RunAsync / run, as the Quickstart shows. Each example below opens a checkpoint store and a history store on the same backend. In .NET, the stores are in the Datalium.Strata.Storage package, which Datalium.Strata.Hosting brings. In Rust, they are in the datalium-strata-memory crate. See Installation.

Demo 12-pluggable-storage runs one scenario against all four stores, and creates them this way: C#, Rust.

using Datalium.Strata.Storage;
using Datalium.Strata.Storage.Memory;
var checkpoints = new InMemoryCheckpointStore();
var history = new InMemoryHistoryStore();

In .NET, the SQLite stores are built on EF Core: you give each one a DbContext on the file. In Rust, you give each one the file’s path. The two stores can share one file.

using Datalium.Strata.Storage.Memory;
using Microsoft.EntityFrameworkCore;
var path = "strata.sqlite";
using var checkpoints = new EfCoreCheckpointStore(
new CheckpointDbContext(new DbContextOptionsBuilder<CheckpointDbContext>()
.UseSqlite($"Data Source={path}").Options));
using var history = new EfCoreHistoryStore(
new HistoryDbContext(new DbContextOptionsBuilder<HistoryDbContext>()
.UseSqlite($"Data Source={path}").Options));

A missing file is created. Opening a file creates the tables, or brings them up to date if an older version of Strata wrote them. A file that a newer version wrote is refused. For tests, EfCoreCheckpointStore.CreateInMemory() and SqliteCheckpointStore::open_in_memory() open a SQLite database that lives in memory.

Options go in SqliteStoreOptions: the fourth constructor argument in C#, or SqliteCheckpointStore::open_with(path, options) in Rust. See Turn on store options.

Give the store a URL of the form postgres://user:password@host:port/database. In .NET you can give an Npgsql connection string instead. In a URL, the only query parameter .NET accepts is sslmode.

using Datalium.Strata.Storage.Postgres;
var url = "postgres://strata:strata@localhost:5432/strata";
await using var checkpoints = await PostgresCheckpointStore.OpenAsync(url);
await using var history = await PostgresHistoryStore.OpenAsync(url);

OpenAsync (C#) and connect (Rust) connect, and create or upgrade the tables, before they return. A database that can’t be reached, or that a newer version of Strata wrote, fails at startup rather than at the first save. In .NET, the constructors don’t touch the database: the tables are prepared on first use. Each database records its version in a strata_schema_version table.

To upgrade the tables in a deploy step instead, set AutoMigrate = false in PostgresStoreOptions (C#), or with_auto_migrate(false) (Rust). The store then checks the tables and refuses a database that is behind, and your deploy step calls MigrateAsync / migrate. In C#, create that store with new PostgresCheckpointStore(url, options): OpenAsync always migrates, whatever AutoMigrate says. Use Postgres shows both.

In Rust, the store keeps a pool of up to 16 connections. with_pool_size changes it.

Give the store a URL of the form redis://[:password@]host[:port][/db]. In .NET you can give a StackExchange.Redis configuration string instead. Keys start with a prefix, strata by default. Give each application or environment its own prefix.

using Datalium.Strata.Storage.Redis;
var url = "redis://localhost:6379";
using var checkpoints = await RedisCheckpointStore.ConnectAsync(url, new RedisStoreOptions { Prefix = "acme" });
using var history = await RedisHistoryStore.ConnectAsync(url, new RedisStoreOptions { Prefix = "acme" });

Redis has no tables to create: each thread is one Redis list. The first store that uses a prefix indexes its threads, for listing. Give the CLI the same prefix in the URL: redis://localhost:6379?prefix=acme (see the CLI reference).

Group commit, delta checkpoints and value offload are off by default. You turn them on in the store’s options. This example turns on all three for Postgres:

using Datalium.Strata.Storage;
using Datalium.Strata.Storage.Postgres;
await using var checkpoints = await PostgresCheckpointStore.OpenAsync(url, new PostgresStoreOptions
{
GroupCommit = new GroupCommitOptions(),
Deltas = new DeltaOptions(),
Offload = new OffloadOptions(),
});

The SQLite options (SqliteStoreOptions) take the same three. The Redis options (RedisStoreOptions) take deltas and offload.

  • Group commit writes the saves that arrive while another write is under way together, in one transaction. It helps with many writers, and costs a little with one. A save is still acknowledged only after it is committed.
  • Delta checkpoints and value offload shrink what a large state costs to store. State explains both. Upgrade every reader, the strata CLI included, before a writer turns either on.

Stores never delete anything on their own. Every built-in store has PurgeAsync / purge, which deletes the finished threads (by default, completed or ended) whose latest checkpoint is older than a time you give. Call it on a schedule, or let the host’s retention driver call it. Retention purges checkpoints, not history.

The Redis stores can also expire finished threads: set FinishedThreadTtl in RedisStoreOptions (C#), or with_finished_thread_ttl (Rust). A thread’s keys then expire that long after it completes or is ended. It’s off by default.

In a .NET host, you register the stores instead of creating them: AddSqliteCheckpoints(), AddPostgresCheckpoints() or AddRedisCheckpoints(), and AddSqliteHistory(), AddPostgresHistory() or AddRedisHistory(). They read their settings from Strata:Sqlite, Strata:Postgres or Strata:Redis in configuration. In Rust, the settings of datalium-strata-host open the same stores. See Hosting.

The Postgres and Redis stores need a server. For development, the docker-compose.yml in datalium/strata starts throwaway ones, with Postgres 16 (with pgvector) and Redis 8. These are the versions the stores are tested against in CI. Nothing is persisted between runs:

5432/strata
docker compose up -d --wait
# redis://localhost:6379
docker compose down

The demo repositories ship a compose file on the same ports, so run one copy at a time.

Your graphs, nodes and state don’t change when you change stores. Only the code that creates the store, or the Add...Checkpoints() call, does.

The threads don’t move with it. Strata has no tool that copies threads from one store to another, and a new store starts empty. A thread that waits in the old store can only be resumed from the old store. One way to switch is to start new threads on the new store, and keep resuming the old store’s threads until none are left. strata thread list --status interrupted shows what still waits in a store.

Upgrading Strata on the same store needs no copy:

  • SQLite and Postgres databases move forward one recorded step at a time, when a store opens them or when you migrate them. There are no down migrations, and a database that a newer version wrote is refused.
  • Redis has no migrations. Every writer to a prefix must run the same version of Strata, because the scripts that keep the thread index are part of the writer.
  • Readers first. Upgrade everything that reads threads, including the strata CLI, before any writer starts using something new, such as deltas or offload.

You can turn delta checkpoints off again: a store reads delta rows whatever its own options say. Offloaded values are a little different. A store reads them only from the blob store its options name, which is the store’s own database unless you give another. If you give your own blob store, keep naming it in the options for as long as offloaded rows remain.

The compatibility rules are in schema/README.md.