System designCore9 min
In-memory data-structure stores
Redis is often introduced as a cache, but it is a server for data structures: counters, lists, sets, sorted rankings and streams, all in memory and changed by one command at a time. That design makes it fast and atomic, and it also explains its limits: one slow command stalls everyone, memory runs out, and durability is a setting.
1 · The idea
An in-memory store keeps data structures, not just values
A Q&A site for programmers has 3 million visits a day. Besides its PostgreSQL database it needs a dozen small, fast things: sessions, rate limits, a "hot questions" ranking, who's online, a recent-activity feed. Memcached would store each as an opaque value, and every change would mean reading it, editing it in the app and writing it back. Redis is a data structure server: the value has a type, and commands change it in place:
Type | What it is | On the Q&A site |
|---|---|---|
String | Bytes, or a number with atomic increment | Rate-limit counters per user per minute |
Hash | Field-value pairs, like a small record | A session: user ID, theme, last seen |
List | Strings in insertion order | Recent activity, capped at 100 items |
Set | Unique strings; add, remove, test in O(1) | Users online in each tag's chat room |
Sorted set | Unique strings ordered by a score | Hot questions ranked by a score (Counters & leaderboards) |
Stream | An append-only log read by consumers | Notifications waiting to be sent |
There are more, including geospatial indexes (Geospatial indexing), HyperLogLog for counting distinct visitors (HyperLogLog: counting distinct things) and vector sets. The point isn't the list: it's that the work happens next to the data. "Add 5 to this question's score and return its new rank" is one command, not a read-modify-write across the network.
2 · One at a time
Commands run one at a time, so each one is atomic
Redis serves all client requests on a single thread, one command at a time, in the way Node.js serves requests on one event loop. Most commands take well under a microsecond, so one thread handles a lot of traffic. And because nothing else runs during a command, each command is atomic with no locks:
A per-minute rate limit, safe under any concurrency
INCR rl:user:4410:2026-10-02T09:14 -> 37
EXPIRE rl:user:4410:2026-10-02T09:14 60
# 37 > 30 allowed per minute: reject with 429Two app servers incrementing the same counter at the same moment get 37 and 38, never 37 twice. Several commands that must happen together go in a Lua script or a MULTI block, which also run without interruption (Transactions & isolation levels covers what MULTI does and doesn't roll back).
The server is rarely the slow part; the network is. A round trip on a 1 Gbit/s network takes about 200 µs, so a page that issues commands one by one pays that every time. Sending them together, with multi-key commands like MGET or by pipelining, pays it once:
50 × 200 µs = 10 ms of waiting for a question page that loads 50 counters, against one round trip. The commands themselves take microseconds either way.
3 · Slow commands
One slow command stalls every client
The single thread has a cost: while one command runs, every other client waits. Constant-time commands such as GET, SET and LPUSH never notice. Commands that walk many elements do: sorting a large list, intersecting two big sets, or KEYS, which scans every key. KEYS in production is a very common source of latency:
Every page load on the site waits behind one maintenance command. SCAN does the same job in small batches, letting other commands run in between.
The usual causes and their fixes:
- Whole-keyspace commands. Use
SCANinstead ofKEYS, andSSCAN,HSCAN,ZSCANfor big collections. - Big values. A list with millions of items makes every command that reads it whole slow; cap it (
LTRIM) or split it. - Mass expiry. Many keys expiring in the same second can make Redis pause to clean them up; add jitter to expiry times.
- Heavy reads. Run slow queries on a replica. The slow log records which commands took too long.
4 · Memory
Memory is the limit, so decide what happens when it's full
Everything lives in RAM, so the dataset can't outgrow the machine. maxmemory sets the limit and maxmemory-policy decides what happens at it. The choice depends on whether the data can be rebuilt:
Policy | At the limit | Fits |
|---|---|---|
noeviction | Writes that add data fail with an error; reads still work | Data that must not vanish, such as queues and sessions |
allkeys-lru | Evict the least recently used keys | Caches; Redis's suggested default when some keys are hotter |
allkeys-lfu | Evict the least frequently used keys | Caches with stable popular items |
volatile-ttl | Evict keys with the shortest time left to live | Apps that mark expendable keys with short TTLs |
Don't mix a cache and data you want to keep in one Redis instance; run two. The Q&A site does exactly that: a cache instance with allkeys-lru, and a second instance for sessions, rate limits and the notification stream with noeviction and alerts well before it fills. Leave headroom below the machine's RAM as well: replication and persistence buffers aren't counted against maxmemory (Cache invalidation & eviction covers how the LRU approximation works).
5 · Durability
Durability is a setting, and each setting loses something different
Data in memory is gone when the process dies, unless it was written to disk. Redis offers two mechanisms, alone or together:
- RDB snapshots write the whole dataset to a file every few minutes, from a forked child process. Compact, good for backups and fast restarts, but a crash loses everything since the last snapshot.
- The append-only file (AOF) logs every write. How much it can lose depends on how often it forces the file to disk (
fsync).
Writes lost in the worst case after a crash. Fsync every second is Redis's default and recommended AOF setting; fsync on every write is safest and very slow. Redis suggests using both AOF and RDB for safety comparable to PostgreSQL's.
Persistence has its own latency cost. Snapshots and AOF rewrites fork the process, and the fork copies its page table: about 48 MB for a 24 GB instance, which can pause the server for milliseconds, or much longer on some virtual machines. Keep instances moderately sized, and don't schedule snapshots on every replica at the same moment.
6 · Replicas & cluster
Replicas and clusters add availability, not strict consistency
Replication is asynchronous by default: the primary answers the client, then streams the write to its replicas. If the primary dies before a write reaches a replica and the replica is promoted, that acknowledged write is gone. The WAIT command makes the client wait for replicas to confirm, which shrinks the window, but it does not make the system strongly consistent (Database replication).
Redis Cluster spreads keys across several primaries. Every key maps to one of 16,384 hash slots, by CRC16 of the key mod 16,384, and each primary owns a share of the slots:
Clients cache the slot map and talk to the right primary directly; a node that doesn't own a slot answers MOVED with the right address. Each primary has replicas of its own, copied asynchronously.
Multi-key commands only work when all keys are in the same slot. Hash tags force that: only the part in braces is hashed, so {user:4410}.session and {user:4410}.prefs always share a slot. Memcached takes the opposite approach: servers don't know about each other at all, with no replication, and the client picks a server by hashing the key (Consistent hashing).
7 · In practice
Keep in memory what you can rebuild, or can afford to lose
The Q&A site keeps the questions and answers themselves in PostgreSQL. Everything in Redis is either derived from that, and can be rebuilt (cached pages, the hot-questions ranking), or short-lived and acceptable to lose a second of (rate counters, sessions, online status). That split is what makes an in-memory store safe to rely on.
- Pick the structure first
A sorted set or a counter does the work in one atomic command instead of a read-modify-write in the app.
- Separate cache from state
One instance that may evict, another that must not, each with its own memory alarm.
- Choose durability on purpose
None for pure caches, AOF every second for state, plus snapshots for backups.
- Ban slow commands
No KEYS in production, cap list and set sizes, watch the slow log.
- Batch round trips
Pipelines and multi-key commands, with hash tags when keys must share a slot.
Check yourself
4 questions
Takeaways
Remember this
- Redis stores typed data structures and changes them in place, so work like incrementing or ranking is one command next to the data.
- One thread runs one command at a time: every command is atomic, and one slow command delays every client.
- Memory is the limit: choose an eviction policy per instance, and keep caches and must-keep state in separate instances.
- Durability is configurable: snapshots lose minutes, AOF every second loses about a second, and replication is asynchronous.
- Redis Cluster maps keys to 16,384 slots; hash tags keep related keys together for multi-key commands.