Cache
One ioredis client, a small service on top, and a deliberate choice to fail fast.
packages/cache owns the only Redis connection in the codebase. Everything
else, including the queues, derives its
connection options from here rather than constructing its own.
import { cache } from "@workspace/cache"
const user = await cache.getOrSet(
`user:${id}`,
() => drizzleDb.select().from(users).where(eq(users.id, id)),
{ ttl: 300 },
)getOrSet is the one worth reaching for: it collapses the read, the miss and
the write into a call that is hard to get wrong. The rest of the surface is
what you would expect — get, set, del, has, ttl, expire, incr,
decr, keys, delPattern, clear, mset, mget.
Fail fast, on purpose
// packages/cache/src/redis.ts
enableOfflineQueue: false
maxRetriesPerRequest: REDIS_MAX_RETRIES // default 3With the offline queue off, a command issued while Redis is unreachable rejects immediately instead of parking until a reconnection. That is the right behaviour on a request path: a page rendering without its cache is a degraded page, while a page waiting on a dead Redis is a timeout the user watches.
Which means a cache call can throw. Treat the cache as an optimisation and let a miss fall through to the source, rather than letting a Redis outage take a route down with it.
The queues make the opposite choice for the opposite reason, and background jobs explains what that costs a producer.
retryStrategy backs off between reconnection attempts, so an outage does not
turn into a reconnect storm against a recovering server.
One connection, one index
The client is a singleton. Importing getRedis() from anywhere returns the
same connection, which is what keeps a Bun process from opening one socket per
module.
REDIS_DB selects the logical database. If several apps share a Redis server,
give each its own index rather than a key prefix: prefixes are a convention
that a stray FLUSHDB ignores, and indexes are not.
What is cached today
Sessions and rate-limit counters, both through Better Auth and the API's rate-limit middleware. Application-level caching is left to you on purpose: caching the wrong query is how stale data ships, and the right things to cache are the ones your own traffic shows you.
examples.ts in the package holds worked patterns rather than production code.