Redis and Dragonfly
One package serves both servers: adapter = "redis" or adapter = "dragonfly". Native only; push broadcast over pub/sub.
Setup
$ bun add @brust/cache-redisPackage @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.
[cache]
adapter = "redis" # or "dragonfly"
namespace = "my-app"
[cache.redis]
url = "env:REDIS_URL" # always env:NAME; a literal URL is a build errorRun the app with REDIS_URL set. Credentials live only inside the URL. The adapter’s log lines show host:port and never the password.
$ REDIS_URL=rediss://user:password@host:6380/0 brust startOptions
These go under [cache.redis]. An unknown key stops the start with cache-redis: unknown option "<key>".
| Key | Default | Meaning |
|---|---|---|
url | required | redis://, rediss://, redis+tls:// or redis+unix://. Must be written as env:NAME. If the variable is unset the start fails and names the variable. |
tls | false | true, false, or a table with reject_unauthorized, server_name, ca_file (a path to a PEM file, read at start). |
connect_timeout_ms | 2000 | 100..60000. One bounded connect at start. |
max_body_bytes | 1048576 | 1..67108864. Larger pages are not stored. |
marker_ttl_ms | 86400000 | 60000..604800000. How long an invalidation marker is remembered; it should be longer than your longest page TTL. |
unlink_batch | 500 | 1..10000. Pages deleted per UNLINK after an invalidation. |
reconnect_min_ms | 100 | 10..60000. First reconnect delay. |
reconnect_max_ms | 5000 | 10..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).
| Key | Holds |
|---|---|
{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}:seq | The invalidation counter. This is the stamp. |
{brust:ns}:stage:<id> | A 30-second staging key used while writing a page. |
channel brust:<ns>:inval | Pub/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.
getreturns the counter as the stamp.setruns a Lua script that checks the invalidation markers of the page’s tags and of itsMETHOD 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/setfailures count aserrors, failed publishes aspublish_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.
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.