Skip to content

Persistence

The effect/unstable/persistence package provides a small stack of durable storage primitives: a low-level key/value store, a schema-aware store for request results, and three higher-level building blocks (a durable cache, a durable job queue, and a shared rate limiter). Each layer builds on the one below it, and a single Redis service can power all of the Redis-backed variants at once.

import {
KeyValueStore,
Persistable,
PersistedCache,
PersistedQueue,
Persistence,
RateLimiter,
Redis
} from "effect/unstable/persistence"

Think of the package as layers stacked from raw bytes up to ready-made features:

  • KeyValueStore — the lowest level. A Map-like service over strings, numbers, JSON, Uint8Array, and schema-encoded values, backed by memory, the filesystem, SQL, or the Web Storage API. Use it for small, lightweight durable state.
  • Persistence / Persistable — a schema-aware store that reads and writes encoded Exit values keyed by each request’s PrimaryKey. It is the layer that knows how to serialize a request result (success or error) with the request’s schemas and apply per-entry TTLs.
  • PersistedCache — durable memoization. Looks up a Persistable request, returns the stored result if present, otherwise runs your lookup and persists the Exit. Survives restarts and is shared across processes that share a backing store.
  • PersistedQueue — a durable, at-least-once job queue (an outbox). Offers are persisted before they are taken, so work survives crashes.
  • RateLimiter — shared fixed-window / token-bucket limits enforced across fibers and processes (e.g. a cross-process Redis store).

Redis is not a feature module — it is the integration seam underneath all the Redis-backed layers. You adapt an existing Redis client into a Redis service, and the Persistence, PersistedQueue, and RateLimiter Redis layers all consume it.

Lightweight durable state

Small string / number / JSON / binary values you want to read and write like a Map.

Use KeyValueStore.

Memoize expensive requests

Idempotent lookups that are slow or costly and should be reused across restarts and workers.

Use PersistedCache.

Durable background jobs

An outbox / job queue where each item must be processed at least once even if the process crashes.

Use PersistedQueue.

Cross-process quotas

Shared throttling / rate limits enforced across multiple fibers or processes.

Use RateLimiter.

Raw encoded Exit storage

Direct control over schema-encoded Exit values keyed by request primary key, beneath the cache.

Use Persistence.

TaskModule
Lightweight durable string/binary/JSON stateKeyValueStore
Memoize expensive idempotent requests across restartsPersistedCache
Durable background jobs / outboxPersistedQueue
Cross-process quotas / throttlingRateLimiter
Raw schema-encoded Exit storagePersistence

Most modules are split into a high-level service plus a pluggable backing store layer that decides where data physically lives. Choose a backing layer and provide it (along with Redis, SqlClient, or a KeyValueStore when the chosen layer requires one).

ModuleMemoryKeyValueStoreSQLRedisOther
KeyValueStorelayerMemorylayerSqllayerStorage (Web Storage), layerFileSystem (disk)
PersistencelayerMemorylayerKvslayerSql, layerSqlMultiTablelayerRedis
PersistedQueuelayerStoreMemorylayerStoreSqllayerStoreRedis
RateLimiterlayerStoreMemorylayerStoreRedis

PersistedCache has no backing layer of its own — it is built on top of Persistence, so you provide one of the Persistence layers above.

The Redis module is small enough to document in full here. It defines the low-level service used by every Redis-backed persistence layer. It deliberately does not create or own connections — you adapt an existing client (such as node-redis or ioredis) by implementing a single send function. There is no built-in client layer; wiring your client up is the integration point.

A Context.Service exposing send(command, ...args) for ordinary Redis commands and eval(script) for cached Lua scripts. Higher-level layers require this service in their environment.

import { Effect } from "effect"
import { Redis } from "effect/unstable/persistence"
const program = Effect.gen(function* () {
const redis = yield* Redis
yield* redis.send("SET", "greeting", "hello")
const value = yield* redis.send<string>("GET", "greeting")
// => "hello"
return value
})

Wraps a raw command sender into a Redis service. It builds an internal cache that loads each Lua script once via SCRIPT LOAD, remembers its SHA, and runs it with EVALSHA. This is how you turn a real client into the service — wrap the result in Layer.effect(Redis)(...).

import { Effect, Layer } from "effect"
import { Redis } from "effect/unstable/persistence"
// `client` here is your own connected Redis client (node-redis, ioredis, ...).
declare const client: {
sendCommand: (args: ReadonlyArray<string>) => Promise<unknown>
}
const RedisLive = Layer.effect(Redis)(
Redis.make({
// Map every command + arg list onto your client, and turn any client or
// network failure into a RedisError.
send: <A = unknown>(command: string, ...args: ReadonlyArray<string>) =>
Effect.tryPromise({
try: () => client.sendCommand([command, ...args]) as Promise<A>,
catch: (cause) => new Redis.RedisError({ cause })
})
})
)
// => Layer<Redis>

A Schema.ErrorClass (tag "RedisError") carrying the underlying cause as a Schema.Defect. Raised by command and script execution; your send implementation should map client failures into it.

import { Redis } from "effect/unstable/persistence"
const error = new Redis.RedisError({ cause: new Error("ECONNREFUSED") })
error._tag // => "RedisError"

Constructs a typed Script descriptor. f maps your typed parameters to the ordered argument list passed to Lua, and numberOfKeys (a number or a function of the params) tells Redis how many leading arguments are KEYS versus ARGV. The result type defaults to void.

import { Redis } from "effect/unstable/persistence"
// INCRBY a counter key by an amount. The key is a Redis KEY; the amount is ARGV.
const incrBy = Redis.script(
(key: string, amount: number) => [key, amount],
{
lua: `return redis.call("INCRBY", KEYS[1], ARGV[1])`,
numberOfKeys: 1
}
)
// => Script<{ params: [string, number]; result: void }>

The Script<Config> interface holds the Lua source, the param-to-arg mapping, and the key count. Because Lua return types are opaque to TypeScript, refine the result type with .withReturnType<R>(), then run it via redis.eval(script).

import { Effect } from "effect"
import { Redis } from "effect/unstable/persistence"
const incrBy = Redis.script(
(key: string, amount: number) => [key, amount],
{
lua: `return redis.call("INCRBY", KEYS[1], ARGV[1])`,
numberOfKeys: 1
}
).withReturnType<number>()
// => Script<{ params: [string, number]; result: number }>
const program = Effect.gen(function* () {
const redis = yield* Redis
const total = yield* redis.eval(incrBy)("visits", 5)
// => number, e.g. 5
return total
})