Open-Source Wikis

/

Redis

/

Background

/

Design decisions

redis/redis

Design decisions

The recurring questions about why Redis is built the way it is.

Why single-threaded command execution

Redis runs every command on a single thread. This is the design choice that shapes everything else. The reasoning:

  • Memory access dominates. A typical Redis command reads or writes a few cache lines. Locking adds overhead comparable to the work being done; lock-free designs are subtle and easy to get wrong. A single-threaded loop avoids the question entirely.
  • Atomicity is free. Every command is naturally atomic with respect to every other command. INCR, LPUSH, MULTI/EXEC, Lua scripts — none of them need explicit locks. Operators don't need to reason about lock granularity.
  • Predictable latency. No mutex contention, no thread thrashing. The 99th-percentile latency of a single-threaded server is easier to characterise than a multi-threaded one.
  • Profiling is simpler. A flame graph of redis-server's hot path is one stack of frames, not N. Optimisation is straightforward.

The cost is that Redis can't use more than one core for command logic. The mitigation is that one core is usually enough — Redis can sustain hundreds of thousands of commands per second on a modest CPU. When CPU does become the bottleneck, IO threads parallelise the I/O around command logic.

Why fork-based persistence

BGSAVE and BGREWRITEAOF fork() and let the child do the work in copy-on-write memory. Why fork instead of streaming the dataset to a worker thread?

  • No locking. The child has a consistent snapshot of memory at fork time. The parent can keep mutating; the child sees the original.
  • No incremental serialiser. Writing a snapshot from a fork means iterating regular in-memory data structures. Writing it incrementally would require structures to be stable under mutation, which would impose lock-or-copy on every keyspace operation.
  • Robust against bugs. If the child segfaults, the parent's state is fine. A worker thread that corrupts memory takes the parent down with it.

The cost is that fork() is expensive — kernel must duplicate the page tables — and the COW dirties pages as the parent writes. On a 100-GB heap a full BGSAVE can use 10–20% extra RAM during the child's lifetime.

Why a single binary

The same redis-server executable runs in standalone, cluster, and Sentinel modes. It's also linked as redis-check-rdb and redis-check-aof. The decision dates to ~2013 and persists because:

  • Code reuse. Sentinel and the check tools share most of src/. Maintaining three binaries would either duplicate code or expose internal headers across packages.
  • Deployment. One file to install. The mode is chosen by a config flag or by argv[0].
  • Testing. All modes are exercised by the same build pipeline.

Why RESP and not a binary protocol

RESP is a text-ish, length-prefixed protocol. It is comparatively easy to implement (a working parser is < 200 lines of C) and friendly to debugging — nc localhost 6379 followed by SET foo bar works.

A binary protocol would save a few bytes per command and a few microseconds per parse, but the network and the database are both faster than the parser. The simplicity wins.

RESP3 added richer types (maps, sets, big numbers, push frames) without breaking RESP2 — clients negotiate via HELLO. The protocol stays human-readable.

Why no schema, no types beyond what we provide

Redis doesn't have user-defined types in the core. The closest thing is the Module API, which lets shared libraries register types at runtime. The argument:

  • A type system is opinionated. Different applications want different things. Sticking to opaque strings and the eight or so built-in container types covers most cases without imposing a worldview.
  • Schema changes are expensive in any database. Without a schema, evolving the data model is a client-side concern. The server sees byte arrays.
  • Performance is bounded by what the operations are, not what the values are. Hash-table lookup is the same speed regardless of value semantics.

When the type system is the right abstraction (e.g. JSON documents, vector embeddings), the modules system covers it.

Why hash slots = 16384

Cluster has exactly 16384 slots, computed by CRC16(key) % 16384. Why that number?

  • Heartbeat overhead. Cluster nodes gossip a slot bitmap in every PING. 16384 bits = 2 KiB. Larger numbers (65536 = 8 KiB) measurably increase bus traffic.
  • Cluster size limit. Practically, 1000 master nodes is a soft limit. With 16384 slots that gives ≥ 16 slots per master — fine grained enough for rebalancing, coarse enough for cheap gossip.

The constant is documented in src/cluster_legacy.c. Changing it would break wire compatibility with every existing client and node.

Why AOF rewrite uses fork

Same reasoning as BGSAVE. The rewrite child reads the dataset and writes a fresh AOF; the parent keeps appending mutations to a separate buffer that the child also reads. Forking is the cheapest way to get a consistent snapshot.

Why diskless replication

Disk-backed replication has the master fork, write the RDB to disk, then ship it. With diskless replication the child writes the RDB straight to the network. Reasons:

  • No double IO. The same bytes don't go to disk and then to network.
  • No disk-space requirement on the master. Useful when the master is memory-rich and disk-poor (cloud instances often).
  • Faster on fast networks. On 10 GbE a multi-GB RDB transfers in seconds; the disk write would have been the bottleneck.

The rdbchannel mode (separate TCP socket for RDB) is a refinement: it lets the live command stream continue while the RDB is being transferred.

Why a Tcl test harness

Tcl was the natural fit in 2010. It has a built-in event loop, simple syntax, a small footprint, and expect-style I/O. The harness has accreted hundreds of tests since; rewriting it in another language has never been worth the cost. New contributors learn enough Tcl in an afternoon to add a test.

Why incremental rehashing

dict.c rehashes the table one bucket at a time over many subsequent operations. The reason is simple: synchronous rehashing of a 100M-entry dict would freeze the server for seconds. By spreading the work across thousands of operations, every operation pays a tiny constant overhead and no operation pays a giant one.

Why listpack instead of ziplist

Ziplist had a "previous entry length" field on every entry. When you inserted an entry whose new length-encoding required more bytes for the prev-length field of the next entry, the next entry's prev-length would grow, which could in turn make its successor grow, and so on. Pathological inputs caused O(N²) reallocation cascades.

Listpack uses a back-jump length (a constant-overhead per-entry field) instead. No cascade. Migration to listpack happened in Redis 7.0.

Why the LICENSE changed

Redis was BSD-3-licensed for most of its history. In 2024, Redis Ltd. relicensed under a tri-license: RSALv2 / SSPLv1 / AGPLv3. The reasoning given by Redis Ltd. is to limit cloud providers from offering Redis as a managed service without contribution back. Older contributions remain BSD-3 — REDISCONTRIBUTIONS.txt enumerates which.

The codebase still accepts contributions; the CLA in CONTRIBUTING.md requires that contributors agree to license their work under the tri-license.

Why no built-in TLS (until 6.0)

For the first 11 years Redis had no TLS support; operators were expected to terminate TLS at a sidecar (stunnel, HAProxy). The reasoning at the time was "we're not in the network-security business". Practical pressure (cluster bus over a public network, replication across data centres) eventually outweighed the simplicity argument. Redis 6.0 (May 2020) shipped TLS as a first-class option, implemented as a connection vtable in src/tls.c. The module form (redis-tls.so) was added later for distributions that prefer to ship a TLS-disabled binary.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Design decisions – Redis wiki | Factory