# Radish: putting Redis semantics on Cloudflare Durable Objects and SQLite

> Radish is a Redis-compatible server that runs inside a Cloudflare Durable Object with SQLite as storage and R2 as a cold tier. The design trades most of Redis's operational surface for the platform's, and the author states plainly that it is not production ready.

**Dhravya/radish** — Super fast drop-in replacement of Redis, but runs on Cloudflare Durable Objects

- Repository: https://github.com/Dhravya/radish
- Website: https://radish.dhr.wtf
- Stars: 534 · Forks: 31
- Language: TypeScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/dhravya-radish

## A second life after the licence change

The readme opens with the project's history, and it is unusual enough to change how you read the code.

Radish began in 2024 as a Go reimplementation of Redis, written when Redis changed to a source-available licence. The readme frames the current TypeScript version as a second life and a different bet: the original escaped the licence, this escapes the operations, by putting Redis semantics on a substrate that already solves durability and failover.

So this is not a fresh project. It is a rewrite of a working implementation, and the Go code is preserved at a tag called go-implementation and remains MIT licensed. The readme is gracious about it, saying nothing about it was wrong, it just needed a machine to run on.

That framing sets expectations properly. The first version solved a legal problem; the second solves a different one, the operational cost of running a stateful server. The stated gains are concrete: SQLite is the store, a Durable Object is the process, and R2 is the cold tier. There is no cluster to shard, no failover to configure, and no AOF policy to tune, because the readme argues those are properties of the platform rather than things to operate.

The current repository is TypeScript. The primary language is listed as TypeScript, the package.json has the type module, and the runtime is Bun, with the scripts running bun test and bun run shim/tcp-shim.ts. The topics still say golang, redis and redis-server, which is a leftover from the first life.

The status is stated in bold at the top of the readme and is worth repeating: not production ready. The readme says to see the limits and not implemented sections before considering it for data you care about, and adds that it does run on Cloudflare, with redis-cli driving a deployed Durable Object unmodified, but that it has no production miles.

## The Worker authenticates and then leaves the data path

The architecture diagram in the readme is the clearest thing here, and the prose around it explains the design decisions that the diagram shows.

The key claim is that the Worker is not in the data path. It authenticates the upgrade, calls redis.getByName(name).fetch(request), and steps out. The upgrade runs inside the Durable Object and calls ctx.acceptWebSocket(...) on the DurableObjectState, so the socket terminates there. Every RESP frame after the handshake reaches the DO's webSocketMessage handler with no per-message Worker invocation.

The reason that matters is hibernation. Because it is acceptWebSocket rather than accept, the socket is hibernatable, which means the object can be evicted between commands while the connection stays open. That is why the connection state rides in the socket attachment, and it is what makes the whole cost model work: a Durable Object that has to be resident in memory between commands is not much cheaper than a small server.

The next layer is the shim, and the readme is unusually honest about it. The shim exists because redis-cli speaks TCP and a Durable Object cannot accept TCP. It has no protocol knowledge, bytes in and bytes out, and it is temporary: Cloudflare's connect() handler is in private beta, and on celld the handler does not exist and would have to be added. When either lands, the shim is deleted and clients reach the Durable Object with nothing in between.

So the client side is redis-cli, ioredis or redis-py over TCP on port 6379, sending raw RESP bytes, into a shim that translates only the transport, over a websocket with an x-radish-auth header, into a Worker router that authenticates the upgrade and exits, and finally into the Durable Object where the socket lives.

The last piece of the model is that one name is one instance. The readme says that ?db=cli and ?db=sessions are different Durable Objects with different SQLite databases, migrating between machines independently, with no cluster, no resharding and no failover to operate. Each has its own SQLite, its own lease, its own failover, and 10 GB each.

## Compatibility is established by differential testing

The way correctness is argued is the most interesting technical content in the readme, and it is rare to see stated this plainly.

The compatibility contract is pinned to Redis 7.4.11, with reply shapes verified for RESP2 and RESP3 separately, because they differ per command and are tested per command.

Correctness is established by differential testing, not by assertion. The same seeded command stream runs against a real redis-server and against radish, and the raw RESP bytes are compared. Any difference is a bug unless it is listed in the deviations table. The readme explains why with a sentence that explains a lot about testing philosophy: unit tests can only assert what their author believed Redis does, four times during development that belief was wrong and the harness caught it.

The numbers the readme gives are worth quoting as the project's own claims rather than as independent measurements. Commands: 136 of Redis 7.4.11's 250, with 124 in the dispatch table and 12 connection scoped. Tests: 624, with about 159,700 assertions. Differential: 10 seeds times about 2,025 commands against redis:7.4.11, byte identical. Source: 7,980 lines.

A hundred and thirty six of two hundred and fifty is a little over half of Redis's command surface, which is a meaningful gap. The readme's framing is that compatibility is a contract with a specific pinned version and a specific list of exceptions, not a claim of full Redis parity. That framing is more useful to an adopter than a percentage would be.

The Makefile in the repository root also shows how the differential harness is run. The package scripts include a diff entry that runs the differential script directly.

## The deliberate deviations, and why each one exists

The deviations table is the most useful part of the readme for an engineer deciding whether radish can hold their data, because each row is a place where behaviour differs from Redis, and each has a stated reason.

SELECT accepts only index 0. One named Durable Object is one instance, so a second database does not exist, and CONFIG GET databases reports 1 consistently.

ERR scan cursor expired. The SCAN cursors are server side and evictable after 60 seconds idle, while Redis's are stateless and cannot expire, so the reply has no upstream equivalent. The readme justifies it as an explicit error rather than a successful looking empty result, which would silently violate the full iteration guarantee. That is a good reason. A cursor that silently returns nothing is the worst kind of bug in a scan.

List position space exhaustion refuses. Lists use fractional indices, and exhausting the float space at one end returns an error rather than drifting toward negative infinity. The readme notes this requires about 2^53 pushes at one end, which the 10 GB cap makes unreachable, so it is a theoretical correctness point rather than a practical one.

EXEC rolls back on infrastructure failure. An ordinary command error inside MULTI still preserves successful siblings, as Redis does, but a storage failure rolls the whole transaction back rather than committing a half applied mutation. The readme notes Redis has no equivalent because it has no storage layer that can fail this way. The reasoning is sound, and the behaviour is stricter than Redis rather than looser.

Values are capped at 1 MiB, because Cloudflare caps a SQLite row or BLOB at 2 MB, and advertising Redis's 512 MiB would be a limit no row can hold. That is a hard ceiling you have to design around, and it is the deviation most likely to bite.

And SCAN MATCH with a star pattern matches the empty string, where the literal stringmatchlen returns false, a quirk masked in Redis by the KEYS allkeys short circuit. The readme says it keeps the more literal behaviour.

## How to run it and how to break it

The scripts in the package manifest are the whole developer workflow, and they are unusually few.

```json
    "dev": "./scripts/alchemy dev",
    "shim": "bun run shim/tcp-shim.ts",
    "test": "bun test",
    "typecheck": "tsc --noEmit",
    "deploy": "./scripts/alchemy deploy --yes",
    "destroy": "./scripts/alchemy destroy --yes",
    "diff": "bun run test/differential.ts",
```

The tool is Alchemy, the infrastructure-as-code layer that creates the Worker, the Durable Object and the R2 bucket. The dev script starts it locally, deploy pushes it, and destroy tears it down. The shim script starts the TCP to websocket translator that lets a local redis-cli talk to a local Durable Object. The diff script runs the differential harness against a real Redis. The auth script refreshes the Cloudflare profile credentials.

The runtime dependencies are Effect, at a release candidate version, with the Bun and Node platform packages pinned to matching release candidates, and the overrides block pins all four Effect packages to the same version to stop a transitive upgrade from splitting them. There is also a patchedDependencies entry for fast-glob 3.3.3 and picomatch 4.0.7, with the patch files in a patches/ directory. That level of dependency control is a sign of a project that has been bitten by transitive version drift, and it is also a maintenance burden.

The repository layout confirms the split: a src/ directory for the Durable Object, a shim/ directory for the TCP shim, a test/ directory, a web/ directory for the console, scripts/ for the Alchemy entry points, an alchemy.run.ts at the root describing the infrastructure, and a REVIEW.md file.

The one thing the readme does not do is give you the limits and the not implemented list inline. It refers you to sections further down the page, so if you are evaluating rather than reading for pleasure, that is the part to find before anything else.

## Where radish is the wrong tool

There are several honest reasons not to put this in front of data you care about, and the author gives most of them.

The first is the status line. Not production ready, and no production miles. This is a project that runs on Cloudflare and has been exercised by its own test suite, and that is the extent of the claim.

The second is the command coverage. 136 of 250 commands is a little over half. If your application uses a scripting command, a stream command, or a cluster command, it is not there. A Redis client library that probes for capabilities at connect time may also behave in ways the readme does not cover.

The third is the 1 MiB value cap. For a cache of small values that is nothing. For a session store, a queue with large payloads, or anything holding a serialised document over a megabyte, it is a hard wall imposed by the platform.

The fourth is the shim in the path. Until Cloudflare's connect() handler ships, every TCP client goes through a translator the readme calls the only piece that is not real. It has no protocol knowledge, which is a virtue for its size, but it does mean an extra hop and an extra failure surface, and it exists only because Durable Objects cannot accept TCP.

The fifth is the release history. There is a single GitHub release, v0.0.1 from 2024-03-26, which belongs to the Go implementation. The current rewrite carries version 0.1.0 in the package manifest and is marked private, so it is not published to a registry, and the project has no published releases of the TypeScript version at all. There is no changelog of the new work, and no tags to pin.

On the alternative, the difference in approach against running Redis itself is that radish substitutes the platform for the operational work rather than doing the operational work better. Redis on a small instance is a well understood artefact: a binary, a config file, a persistence policy and a monitoring story. Radish gives you durability, failover and elasticity from the platform, and in exchange you inherit the platform's limits, the command coverage gap, and the fact that the compatibility claim is only as good as the differential test suite.

The alternative worth weighing is not another Redis implementation but a different data model. If what you need is a cache or a key value store on Cloudflare, the platform's own primitives, such as the Cache API and Workers KV, may cover the use case with less fidelity to Redis but also less machinery. Redis semantics are what radish sells, and if you do not need them, the fidelity is wasted work.

## Conclusion

Radish suits a developer who wants Redis semantics on Cloudflare without running a Redis instance, and a reader who wants to study a differential testing approach to compatibility work, since the readme explains the method in unusual detail. It does not suit production data, because the project states it is not production ready and lists its deliberate deviations from Redis 7.4.11. Before you evaluate it further, read the limits and not implemented sections the readme points to, and note that the release history is a single v0.0.1 tag from 2024, with the current TypeScript rewrite carrying a package version of 0.1.0 and no published releases.

## FAQ

### What is Radish?

Radish is a Redis-compatible server that runs inside a Cloudflare Durable Object, with SQLite as the store and R2 as a cold tier. It implements 136 of Redis 7.4.11's 250 commands and is licensed MIT.

### Is Radish ready for production use?

No. The readme states the status as not production ready, notes it has no production miles, and points to its limits and not implemented sections before you consider it for data you care about.

### How does Radish prove it is compatible with Redis?

By differential testing rather than assertion. The same seeded command stream runs against a real redis-server and against radish and the raw RESP bytes are compared, with 10 seeds and about 2,025 commands against redis:7.4.11.

### What does the TCP shim in Radish do?

It exists because redis-cli speaks TCP and a Durable Object cannot accept TCP. It has no protocol knowledge, only translates bytes in and bytes out, and the readme says it will be deleted once Cloudflare's connect() handler ships or is added to celld.

### Which Redis commands does Radish deliberately deviate on?

SELECT accepts only index 0, SCAN cursors can expire with an explicit error, list position space exhaustion returns an error, EXEC rolls back on infrastructure failure, values are capped at 1 MiB, and SCAN MATCH with a star pattern matches the empty string. Each deviation has a test pinning it.

## Sources

- [Dhravya/radish on GitHub](https://github.com/Dhravya/radish)
- [License: MIT](https://github.com/Dhravya/radish/blob/main/LICENSE)
- [Project website](https://radish.dhr.wtf)
- [README](https://github.com/Dhravya/radish/blob/main/README.md)
- [Releases](https://github.com/Dhravya/radish/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dhravya-radish
