brust 2.0.0-alpha.5
Cache adapters / Redis and Dragonfly

Redis and Dragonfly

One package serves both servers: adapter = "redis" or adapter = "dragonfly". Native only; push broadcast over pub/sub.

Setup

~/my-app
$ bun add @brust/cache-redis

Package @brust/cache-redis. It uses Bun’s built-in RedisClient, so it adds no npm dependency. It needs Bun 1.2.23 or newer and a Redis-compatible server 7.2 or newer. Bun’s Redis pub/sub is still marked experimental by Bun; the adapter turns any lost message into a tier-1 clear rather than a stale page (below).

Bun only. It does not run on Workers, because a Worker has no TCP client the adapter can rely on. On Workers, use a custom adapter over an HTTP-based Redis service.

brust.toml
[cache]
adapter = "redis"              # or "dragonfly"
namespace = "my-app"

[cache.redis]
url = "env:REDIS_URL"          # always env:NAME; a literal URL is a build error

Run the app with REDIS_URL set. Credentials live only inside the URL. The adapter’s log lines show host:port and never the password.

~/my-app
$ REDIS_URL=rediss://user:password@host:6380/0 brust start

Options

These go under [cache.redis]. An unknown key stops the start with cache-redis: unknown option "<key>".

KeyDefaultMeaning
urlrequiredredis://, rediss://, redis+tls:// or redis+unix://. Must be written as env:NAME. If the variable is unset the start fails and names the variable.
tlsfalsetrue, false, or a table with reject_unauthorized, server_name, ca_file (a path to a PEM file, read at start).
connect_timeout_ms2000100..60000. One bounded connect at start.
max_body_bytes10485761..67108864. Larger pages are not stored.
marker_ttl_ms8640000060000..604800000. How long an invalidation marker is remembered; it should be longer than your longest page TTL.
unlink_batch5001..10000. Pages deleted per UNLINK after an invalidation.
reconnect_min_ms10010..60000. First reconnect delay.
reconnect_max_ms500010..600000. Cap of the exponential backoff (plus or minus 20% jitter).

What it keeps in the server

Every key of a namespace carries the hash tag {brust:<namespace>}, so the scripts also work on Redis Cluster (all keys in one slot).

KeyHolds
{brust:ns}:page:<key>One page: a small header and the identity body, expiring with the page TTL. The key includes the build id.
{brust:ns}:tag:<tag>, {brust:ns}:path:<METHOD> <path>Sets of the page keys under a tag or a path.
{brust:ns}:inval:tag:<tag>, {brust:ns}:inval:path:<METHOD> <path>The sequence number of the last invalidation of that name, kept marker_ttl_ms.
{brust:ns}:seqThe invalidation counter. This is the stamp.
{brust:ns}:stage:<id>A 30-second staging key used while writing a page.
channel brust:<ns>:invalPub/sub channel for invalidation events.

Only page keys are scoped by build id. The index sets, markers, counter and channel are shared by all builds, so a rolling deploy keeps invalidating across old and new instances.

Guard and invalidation

  • A stale write is refused exactly. get returns the counter as the stamp. set runs a Lua script that checks the invalidation markers of the page’s tags and of its METHOD path, and refuses (returns false) if any is newer than the stamp. A page rendered before an invalidation therefore cannot be stored after it, even if the two race.
  • Invalidate and publish are one script. It increments the counter, raises the markers and drops the index sets. The adapter then deletes the pages (UNLINK, in batches) and publishes {seq, origin, args} on the channel. Every script declares all its keys, as Dragonfly requires.
  • Receiving. Worker 0 of each process holds the subscription and replays events into the process’s tier 1. Events from the same process are skipped.

Connections

Each Bun worker keeps one command connection. Worker 0 keeps one more for the subscription. A process with W workers therefore holds W + 1 connections to the server. Size maxclients for the number of processes times (W + 1). Beyond that, you pay for your own server.

Outages and what tier 1 does

  • If the server is unreachable at start, the app still starts: the adapter tries one bounded connect, logs a warning and keeps retrying in the background.
  • While disconnected, commands fail at once; they are not queued. Tier-2 get/set failures count as errors, failed publishes as publish_errors, and pages keep being served from tier 1 and rendered on a miss. Logged failures are rate-limited (the first, then every 10th).
  • After every re-subscribe the adapter reads the counter. If it moved beyond the last event this process saw, tier 1 is cleared once (resets +1), so an event missed during the outage cannot leave a stale page.
Limits

Two limits are tracked as follow-ups. Out-of-order delivery under concurrent invalidation: the publish happens after the script, so two processes invalidating at the same moment can deliver events out of order. Other instances read that as a gap and clear their tier 1; this costs hit rate under bursts, it does not serve stale pages. Orphaned pages after a dropped UNLINK batch: the index sets are deleted inside the script, but pages are deleted afterwards. If the connection drops in between, those pages stay in Redis until their TTL. Instances that replayed the event reject them through the invalidation floor, so the cost is storage and an occasional extra render, not a stale page.

Dragonfly

  • Same package, adapter = "dragonfly". It was run against Dragonfly v2.0.2 in brust’s tests.
  • Dragonfly requires every key a script touches to be declared; the adapter’s scripts do.
  • When running the container yourself, pass --ulimit memlock=-1.