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.
┌───────────┐ ┌──────────┐
│ 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
| writers per instance | 1 |
| acknowledged writes lost on crash | 0 |
| rollback on storage failure | yes |
| point-in-time restore | none |
| commands implemented | 136 / 250 |
| seeds byte-identical vs redis 7.4.11 | 10 / 10 |
| commands diffed against real redis | ~20,000 |
| assertions | 159,689 |
| GET | 333,333/s |
| SET | 117,647/s |
| redis GET, same machine | 1,428,571/s |
| SQL statements per GET | 1 |
| 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.
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.