TILLCACHE · TILLDEV

The connection string,
without the cluster.

Managed key-value, durable queues, and pub/sub — over one HTTPS endpoint and one token. A Redis-compatible API you already know, a backend you can swap per namespace, and no nodes to size, patch, or fail over at 3 a.m.

LIVE · serving
kv · queues · pub/sub · managed · durable-objects · byo-redis
WHAT’S INSIDE

One client. Everything fast.

THE KV LAYER

Reads in single-digit ms.

Set a value with a TTL, read it back, bump a counter — the calls are the ones you already reach for. Keys are strongly consistent within a region and eventually consistent across regions, with explicit knobs when you need to flip them.

incr, expire, hashes, sets, sorted sets — enough primitive to build a rate limiter, a leaderboard, or a session store without reaching for anything else.

Key-value reference →
ts · kv.ts
import { createClient } from '@tillstack/cache-node'

const cache = createClient(process.env.TILLCACHE_URL)

// Store a session token; let it expire in an hour.
await cache.set('session:' + id, token, { ex: 3600 })
const live = await cache.get('session:' + id)

// Atomic counter — a rate limiter in two calls.
const hits = await cache.incr('rl:' + ip)
if (hits === 1) await cache.expire('rl:' + ip, 60)
if (hits > 100) throw new TooManyRequests()
THE QUEUE LAYER

Push a job. Forget it.

A durable work queue on the same connection as your KV. Producers push; consumers consume and ack by returning. Idempotency keys are the default, dead-letter queues are wired in from minute zero, and nothing quietly disappears.

Consumer groups, ack timeouts, visibility windows — boring on purpose, so background work is a call, not an architecture.

Queue reference →
ts · queue.ts
// Producer — durable the moment push() resolves.
const emails = cache.queue('emails')
await emails.push({ to: user.email, template: 'welcome' })

// Consumer — ack on return, redeliver on throw.
emails.consume(async (msg) => {
  await send(msg.body)
})

// A message that keeps throwing lands in emails.dlq
// after maxRetries — nothing is silently dropped.
WHAT YOU GET, BY DEFAULT
push()

A job is durable the moment the call resolves — it survives a consumer crash, a deploy, and a region blip.

consume()

Return to ack, throw to redeliver. A visibility timeout hides an in-flight message so two workers never take the same one.

idempotency

Attach an idempotency key and a duplicate push is a no-op. Exactly-once effects without a distributed-lock research project.

dead-letter

A message that keeps failing lands in the namespace DLQ after maxRetries — visible, replayable, never silently dropped.

THE PUB/SUB LAYER

Publish once. Fan out.

One publish reaches every subscriber on the topic — presence, live updates, cross-node cache invalidation, the classic fan-out shapes. Subscribe with exact names or wildcard patterns.

A short replay window covers the subscriber that reconnects a beat late, so a dropped socket doesn’t mean a dropped message. Same endpoint, same token, no second service to stand up.

Pub/sub reference →
ts · presence.ts
// Publish once — every subscriber on the topic hears it.
await cache.publish('room:42', { type: 'join', user: id })

// Anywhere else, on any node:
const room = cache.subscribe('room:42')
room.on('message', (msg) => {
  broadcast(msg)   // fan out to the connected clients
})

// Wildcards too: cache.subscribe('room:*')
NO LOCK-IN, BY DESIGN

The API stays. The backend is yours to pick.

A namespace is the unit of choice. Run your sessions on our managed store, your edge flags on Cloudflare Durable Objects, and your regulated lookups on a Redis endpoint you control — all behind the same client and the same token. The code doesn’t change when the backend does.

THE DEFAULT · REGION-ANCHORED

Managed

Our managed, Upstash-backed store. A namespace is anchored to a region and served from there — nothing to provision, nothing to patch. The right choice for most namespaces.

OPT-IN · PER-POP EDGE

Durable Objects

Back a namespace with Cloudflare Durable Objects: one authoritative instance per point of presence, for state you want decided at the edge closest to the user. Opt into it per namespace.

BRING YOUR OWN BACKEND

Your own Redis

Point a namespace at a Redis endpoint you already run. TillCache brokers the protocol and the observability — the data lives on your infrastructure, and we never store it.

yaml · namespaces.yaml
# Each namespace picks its own backend. The client API is
# identical no matter which one is underneath.

sessions:                     # hot, region-local
  backend: managed            # our Upstash-backed store

edge-flags:                   # read everywhere, low latency
  backend: durable-objects    # Cloudflare — one instance per POP

pii-lookups:                  # data must stay on your infra
  backend: byo                # your own Redis; we never store it
  url: rediss://…@redis.internal:6379
REDIS-COMPATIBLE

The API you already know.

TillCache speaks a drop-in, ioredis-style API, so the code you already wrote against Redis mostly just runs. One connection string from the dashboard, one token, and you’re serving.

Three SDKs share the same surface — server, edge, and Cloudflare — so a call reads the same whether it runs in a Node process, a Vercel Edge function, or a Worker at the POP.

Quickstart →
ts · worker.ts
// @tillstack/cache-cloudflare — inside a Worker.
import { createClient } from '@tillstack/cache-cloudflare'

export default {
  async fetch(req: Request, env: Env) {
    const cache = createClient(env.TILLCACHE_URL)
    const ip = req.headers.get('cf-connecting-ip')!
    const hits = await cache.incr('rl:' + ip)
    if (hits > 100) return new Response('slow down', { status: 429 })
    return fetch(req)
  },
}
THREE SDKS, ONE SURFACE
NODE · BUN · DENO
@tillstack/cache-node

The server SDK. createClient(url) and you have set/get, counters, hashes, sets, queues, and pub/sub — an ioredis-shaped surface with connection pooling handled for you.

EDGE RUNTIMES
@tillstack/cache-edge

A fetch-based client for Vercel Edge, Deno Deploy, and other edge runtimes — no TCP socket required. The same calls, over HTTPS, from a Web-standard runtime.

CLOUDFLARE WORKERS
@tillstack/cache-cloudflare

Built for Workers: read the URL from your binding, share a client across isolates, and reach a Durable-Objects-backed namespace from the same POP it lives on.

IT WATCHES ITSELF

The cache you can actually see.

Because TillCache is part of TillDev, the numbers you need to reason about it are already in TillPulse — no exporter to wire, no dashboard to build. The cache reports its own health next to the app it serves.

Hot keys

The keys taking the most traffic, ranked — so a thundering-herd key or a missing TTL shows up as a chart, not a 2 a.m. incident.

Queue depth & lag

Backlog and consumer lag per queue, over time. Watch a spike drain, and get alerted before a slow consumer turns into a stuck one.

Slow commands

Commands over your latency budget, surfaced with the key pattern behind them — the cache equivalent of a slow-query log, already plotted.

NOT A SEPARATE BILL

Same workspace. Same login.

TillCache isn’t a separate service to procure — it’s the fast-data layer of the TillDev workspace you already have. It reports to TillPulse, bills on the same invoice, and answers to the same members and roles.

01 · REPORTS TO

TillPulse

Hot keys, queue lag, and slow commands land in the same observability surface as your crashes and performance. One place to look when something is slow.

02 · SHIPS WITH

One workspace

One login, one bill, one set of members and roles. Add TillCache to a workspace that already runs TillPulse and it inherits the org, the team, and the billing you already have.

03 · RECORDS

One audit log

Namespace creation, backend changes, and token rotation write to the shared TillDev audit log, next to your Pulse, Auth, and Shield events — one history, not four.

NO SURPRISE BILLS

The free tier has a hard cap, not a soft one. Cross it and TillCache slows or pauses the namespace and tells you — it does not quietly meter you into a four-figure invoice.

TURN IT ON

KV, queues, and pub/sub over one connection string — with a backend you can move whenever you want. Copy the URL, install the SDK, and skip the cluster entirely.

PART OF TILLDEV

One of seven.

You don't have to use the rest. But they fit together — same workspace, same audit log, same shortcut to switch between them. Add what you need when you need it.