ShipKit docs
The stack

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 3

With 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.

On this page