radish docs source

Redis, but on
Durable Objects.

A Redis-compatible server that runs inside a Cloudflare Durable Object. SQLite is the store, R2 is the cold tier. Your client does not know the difference.

commands
136
of 250
tests
624
0 failing
differential
10/10
seeds byte-identical
ack'd writes lost
0
on crash
Architecture
  ┌───────────┐                        ┌──────────┐
  │ redis-cli │──TCP──▶ shim ──wss──▶  │  Worker  │  authenticates,
  └───────────┘         ▲              └────┬─────┘  then exits
                        │                   │ ?db=sessions
      translates only;  │                   ▼
      deleted once      │          ┌──────────────────┐
      inbound TCP lands │          │     RedisDO      │  socket lives here
                                   │  ┌────────────┐  │  hibernates between
                                   │  │   SQLite   │  │  commands
                                   │  └──────┬─────┘  │
                                   └─────────┼────────┘
                                     evict ▼ │ ▲ fault in
                                   ┌─────────┴────────┐
                                   │   R2  cold tier  │  unbounded
                                   └──────────────────┘

the socket terminates inside the durable object, via ctx.acceptWebSocket(). after the handshake the worker is not in the data path at all.

Deploy it to your Cloudflare account

One Worker, one Durable Object namespace, one R2 bucket. It fails closed: with no token it returns 503 rather than serving whoever finds the URL.

ioredis, redis-py, go-redis and redis-cli all work unchanged. The shim only exists because a Durable Object cannot accept raw TCP yet; it disappears when connect() ships.

$ git clone github.com/Dhravya/radish
$ cd radish && bun install
$ echo "RADISH_AUTH_TOKEN=$(openssl rand -hex 24)" > .env

$ bun run dev          # local worker, :1337
$ bun run deploy       # your cloudflare account

$ bun run shim         # TCP :6379 to the worker
$ redis-cli -p 6379 set hello world
OK

Key characteristics

Durability
writers per instance 1
acknowledged writes lost on crash 0
rollback on storage failure yes
point-in-time restore none
Correctness
commands implemented 136 / 250
seeds byte-identical vs redis 7.4.11 10 / 10
commands diffed against real redis ~20,000
assertions 159,689
Throughput
GET 333,333/s
SET 117,647/s
redis GET, same machine 1,428,571/s
SQL statements per GET 1
Capacity
SQLite per named instance 10 GB
beyond that R2 cold tier
instances unlimited
max value size 1 MiB

One order of magnitude of throughput, traded for durability and zero operations. If that trade is wrong for your workload, nothing here fixes it, so use Redis.

All of it, on GitHub
github.com/Dhravya/radish
7,980 lines · MIT · the differential harness is in test/

Most of Redis deletes itself

Redis built its machinery to survive a world without durable single-writer storage. A Durable Object already has one, so the machinery has nothing left to do.

One name is one database. ?db=sessions and ?db=tenant-4821 are separate objects with separate SQLite databases, migrating between machines independently.

redis mechanism            radish
single-threaded loop    the input gate
AOF + fsync policy     the output gate
RDB + fork()           SQLite is the snapshot
cluster + resharding   one name = one instance
replica failover       the platform migrates it
maxmemory eviction     the R2 cold tier

Tested for correctness, byte for byte

Every command Radish implements is byte-identical to Redis 7.4.11. The same seeded command stream runs against a real redis-server and against Radish, and the raw protocol bytes are compared. Any difference is a bug.

A test suite can only assert what its author believed Redis does. That belief was wrong five times. TTL rounds half-up rather than up, and GETRANGE has an early-empty rule that fires before clamping. The harness caught every one.

$ bun run diff
seed 7        PASS  2024 commands, byte-identical
seed 1234     PASS  2021 commands, byte-identical
seed 42       PASS  2027 commands, byte-identical
seed 8675309  PASS  2027 commands, byte-identical
seed 555      PASS  2027 commands, byte-identical
seed 2024     PASS  2015 commands, byte-identical

10 seeds · ~20,000 commands · 0 divergences

What this is not

Blocking BLPOP BRPOP BLMOVE BLMPOP
Streams XADD XRANGE XREAD, consumer groups
Scripting EVAL EVALSHA FUNCTION
Zset set-ops ZUNION ZINTER ZDIFF and the *STORE forms
Introspection OBJECT DUMP RESTORE
Also absent cluster, replication, ACL

WATCH is implemented, so transactions are not structurally incomplete. Durable storage is not a backup. There is no export or point-in-time restore. Not production ready: it runs on Cloudflare but has no production miles.