We put a Redis server inside our runtime

eandre2 pts0 comments

We put a Redis server inside our runtime – Encore Blog

We put a Redis server inside our runtime<br>How we run an in-memory Redis inside the runtime for local development and tests, and how we keep it behaving the same as the Redis you run in production.

Jun 25, 2026<br>6 Min Read<br>Ivan Cernja

Jun 25, 2026<br>We put a Redis server inside our runtime<br>How we run an in-memory Redis inside the runtime for local development and tests, and how we keep it behaving the same as the Redis you run in production.

Ivan Cernja<br>6 Min Read

Encore runs the same backend code in local development, in tests, and in production. The infrastructure an application depends on is declared in its code, and Encore provisions that infrastructure for each environment. For local development to be useful, the infrastructure has to actually be present on your machine, and it has to behave the way it does in production.

Most of it is straightforward to stand up locally, with databases running in Docker and pub/sub running against a local NSQ daemon. A cache is harder, because the realistic options are to run a real Redis in Docker, which is another container to install and keep alive, or to replace it with a mock, which only behaves like Redis until your code relies on something the mock implements differently.

We took a different approach, where the runtime has an in-memory Redis server built into it that starts automatically in local development and tests, in the same process as the runtime. This post covers how that works, why we ported a Go implementation to Rust to get there, and how we make sure the in-memory server behaves the same as the Redis an application talks to in production.

An in-memory Redis, in the same process

Encore's Go runtime has worked this way for a long time. On the Go side, local development uses alicebob/miniredis, an in-memory Redis server written in Go, so that running an application locally needs no external Redis.

When we built the Rust runtime that powers TypeScript applications, we needed the same capability there. One option was to keep the Go implementation and run it as a separate process that the runtime starts and stops, but that means shipping a second binary and supervising another process alongside the runtime, with its own startup, shutdown, and failure modes. We wanted the in-memory server to live inside the runtime, the way the rest of the infrastructure layer does.

So we ported miniredis to Rust (#2300), where it runs as a library inside the runtime. The port is about 25,000 lines of Rust and implements the data types applications actually use: strings, hashes, lists, sets, sorted sets, streams, pub/sub, transactions, and Lua scripting. It is a real Redis server that listens on a TCP socket and speaks the Redis wire protocol (RESP), rather than a stub that emulates a subset of commands.

Porting it also meant carrying over the operational behavior the Go version had. miniredis keeps its own mock clock, so the embedded server runs a small background task that advances that clock once a second to keep time-based expiry working during a long session, and prunes back to a bounded number of keys so a local cache does not grow without limit:

// runtimes/core/src/cache/miniredis.rs<br>// Fast-forward time by 1s every second, and prune back to 100 keys<br>// every 15s, matching the old Go miniredis binary's cleanup.<br>async fn cleanup_task(server: Miniredis) {<br>let mut interval = tokio::time::interval(Duration::from_secs(1));<br>loop {<br>interval.tick().await;<br>server.fast_forward(Duration::from_secs(1));<br>// every 15s, prune down to 100 keys

How the runtime chooses where to connect

A cache in an Encore application is declared in code, the same way every other resource is:

import { CacheCluster, IntKeyspace, expireIn } from "encore.dev/storage/cache";

const cluster = new CacheCluster("rate-limit", {<br>evictionPolicy: "allkeys-lru",<br>});

const requestsPerUser = new IntKeyspaceuserId: string }>(cluster, {<br>keyPattern: "requests/:userId",<br>defaultExpiry: expireIn(10 * 1000),<br>});

That declaration is all the runtime needs. In a deployed environment, Encore provisions a real Redis and the runtime connects to it, and in local development and tests the runtime starts the built-in server on a local address and connects to that instead. The decision comes from the runtime configuration, where each Redis cluster carries an in_memory flag, and when that flag is set the runtime starts the embedded server rather than dialing the configured servers (#2322).

In the runtime that decision is small. When the embedded server is needed, the runtime starts it and hands the same Redis client it would use for a managed cluster a redis:// address pointing at the local server:

// runtimes/core/src/cache/manager.rs<br>// Use miniredis for testing or when any cluster has in_memory set.<br>let needs_miniredis = self.testing || self.clusters.iter().any(|c| c.in_memory);

if needs_miniredis {<br>let server =...

runtime redis server local inside encore

Related Articles