Skip to content

Technologies

Redis

How Redis keeps data in memory, what each of its data structures and settings costs, and where it stops being safe or enough.

Concise article — AI can help

Copy this prompt and paste it into your AI chat:

Help me study this article for practical system design.
Article: https://tech0ver.github.io/sysdes/technologies/redis.md
Ask my goal and preferred depth: a quick recap, a walkthrough, or a deeper discussion. Use the article examples, flag your additions, and let me set the pace.

Or give your AI the article link with your own prompt:

Redis is the in-memory store that many designs put next to the main database, for cachesCacheA copy of data or results that is faster to read than the original and can be rebuilt from it. It cuts latency and load on the source while it answers most reads. Losing it makes a system slower but not wrong.Read more: Caching: When a cache pays off, sessions, counters, rankings, queues, and locks. To defend it in a design, know what each structure and setting costs on one server, which confirmed writes it can lose, and when the load needs more servers or a different system.

A live streaming service, like Twitch, keeps accounts, channels, follows, and payments in PostgreSQL. Every channel page shows the broadcast title, the number of live viewers, and the chat. When a big streamer goes live, hundreds of thousands of viewers open the same page within minutes, and the viewer count changes many times a second.

The database can serve these reads from its cache, but each one is still a query on the one server that also takes every write. The counter is worse: every update of one row waits for the update before it, and each one writes to the log (more on contention on one row). Some data is read again and again, some changes many times a second, and much of it can be rebuilt or lost: a cached page, a session, a viewer count.

Redis is a server that keeps all its data in memory, so the dataset must fit in RAMRandom-access memoryThe main memory of a server: fast, but lost on restart and much more expensive than disk. Data and indexes that fit in RAM are read without disk access.Read more: Wikipedia: Random-access memory. Every value sits under a key, and the value is a data structure, such as a string, a hash, a list, or a sorted set (more on choosing a structure). A client sends commands over TCPTransmission Control ProtocolA transport protocol that delivers a reliable, ordered stream of bytes over a connection. TCP opens a connection with a handshake, numbers every byte, and resends what gets lost. This makes it reliable, but a lost packet delays all data after it. HTTP/1.1, HTTP/2, and most databases run on TCP.Read more: Networking: Delivering data, and each command works on a structure inside the server: GET reads a cached page, INCR adds one to a counter, ZADD puts a channel into a ranking.

On a server that is not overloaded, a small command like these takes much less time than the network round tripRound tripThe time for a message to reach the other side and for the answer to come back. Also called RTT (round-trip time). Every handshake costs at least one round trip, so distance between client and server adds up fast.Read more: MDN: Round trip time, which is about 0.2 ms inside a data center (Redis). So a client waits mostly for the network. One thread runs all commands, one after another (more on one thread). In an example from the Redis benchmark docs, one server with 50 clients that wait for each reply handled about 180,000 SET commands per second (Redis), and sending commands in batches multiplies that.

Where the time of one command goes A GET inside one data center takes about 200 microseconds: about 100 to reach the server, less than one microsecond of work in the server, and about 100 for the reply to come back. The work in the server is a thin line on this scale. REQUEST ON THE NETWORK REPLY ON THE NETWORK COMMAND < 1 µS 0 100 µS 200 µS

Here Redis does not replace the database: accounts and payments stay in PostgreSQL. Redis holds copies and short-lived data next to it, and a write it confirmed can be lost in a crash or a failover, depending on its settings. Each copy also has to be kept in step with the database (more on updating a cache). So add Redis when the database cannot keep up with these reads and writes, not before: a few slow or paid lookups fit a cache table inside PostgreSQL.

Redis next to the database The app servers send every page view to Redis, which holds the page cache, sessions, viewer counts, and the live ranking in memory and can lose its latest writes. Writes of record go to PostgreSQL, which holds accounts, channels, follows, and payments. PostgreSQL is the source of truth, and Redis holds copies of some of its data. App servers 200 servers EVERY PAGE VIEW WRITES OF RECORD Redis in RAM, can lose recent writes page cache sessions viewer counts live ranking PostgreSQL on disk, source of truth accounts channels follows payments COPIES

Since 2024 there are two servers with this core. Redis moved from the BSD license to source-available licenses with version 7.4. In response, the Linux Foundation started Valkey (Linux Foundation), a fork of Redis 7.2.4, released under BSD, and it stays under BSD (Valkey). Redis 8 added the open-source AGPLv3 as a third license option (Redis). Both support the same core protocol, data structures, persistence, replicationReplicationKeeping a copy of the same data on several nodes. Copies let the data survive the loss of a node, spread reads, and sit close to users. The hard part is keeping them in step while the data changes.Read more: Replication: Keeping a copy on another node, and shardingShardOne part of a dataset that is split across nodes; sharding is the split itself. Each row lives in exactly one shard, and each shard has its own leader and followers, so data and writes can grow past one machine.Read more: Sharding: Splitting a table across nodes, and most client libraries work with both. Since the fork, each adds features on its own, sometimes the same feature under different command names, and Valkey cannot load the data files of Redis 7.4 and later (more on Redis and Valkey).

Keep durableDurabilityThe promise that a confirmed write survives a crash or a power loss. A write is durable only after a flush, such as fsync, puts it on disk. The store chooses whether to confirm a write before or after that flush.Read more: Storage engines: Surviving a crash business records, such as accounts and payments, in the database. Put data in Redis when it is read on almost every request or changes many times a second, and when losing the latest writes is acceptable or the data can be rebuilt.

The channel page reads five things from Redis: the broadcast info (title, game, start time), the viewer count, the last chat messages, the set of moderators, and, on the front page, the live channels ranked by viewers. Each one goes under its own key, and the structure behind the key decides which operations are cheap.

A key is a string, and Redis has no tables or namespaces: the naming scheme takes their place. The common form is type:id:part, such as channel:42:info. Keys can hold any bytes, but each long key costs memory and time to compare, so hash a long natural key instead of using it as is (Redis). The structures that cover most designs:

Structure Holds Cheap operations In the example
String Bytes, up to 512 MB GET, SET, INCR: O(1) A rendered page, the viewer count
Hash Fields with string values HGET, HSET, HINCRBY: O(1) channel:42:info with title and game
List Strings in insertion order LPUSH, RPOP: O(1); LINDEX: O(N) The last 100 chat messages
Set Unique strings SADD, SISMEMBER: O(1); SINTER: grows with the smallest set The moderators of a channel
Sorted set Unique strings, each with a number (score) ZADD, ZRANK: O(log N); ZRANGE: O(log N + M) Live channels by viewer count
Stream An append-only logAppend-only logA file of records that only grows at the end; old records are never changed. Appending is the cheapest write, and a newer record for a key replaces the older one. Many databases keep a log inside, for recovery or as their main storage.Read more: Storage engines: From memory to disk of entries XADD: O(1); XRANGE: grows with the entries returned “Went live” events

The costs are for one element: a command with several fields or members pays for each. N is the size of the collection, and M the number of elements returned.

The channel page as Redis keys Five keys serve the channel page. channel:42:info is a hash with the title, game, and start time. channel:42:viewers is a string holding the count 18342. channel:42:chat is a list of the latest messages, newest first, kept at 100. channel:42:mods is a set of user IDs. live is a sorted set of channels scored by viewers: channel 42 with 18342, channel 7 with 9120, channel 15 with 4410. KEY TYPE VALUE channel:42:info HASH title: Finals day 2 game: chess started: 18:00 channel:42:viewers STRING 18342 channel:42:chat LIST msg 9031 msg 9030 msg 9029 … 97 more channel:42:mods SET user:17 user:203 user:88 live SORTED SET 42: 18342 7: 9120 15: 4410 …

The commands do the work inside the server, so the client sends one short command instead of reading a value, changing it, and writing it back: HINCRBY adds to one field, ZINCRBY moves a channel up the ranking, and SINTER intersects two sets. A list bounded with LPUSH and then LTRIM 0 99 keeps the latest 100 messages and drops the rest (Carlson).

An object can be stored three ways. A string holds it as one serialized blob: one read gets it all, but any change rewrites it whole. A hash holds flat fields that can be read and changed one at a time, and since Redis 7.4 and Valkey 9.0 each field can have its own expiry. JSONJavaScript Object NotationA text format for nested data: objects with named fields, lists, strings, numbers, and booleans. Document stores keep records as JSON or a binary form of it, and relational databases have JSON columns.Read more: Wikipedia: JSON, built into Redis 8 and a module in Valkey, holds nested documents and updates a path inside them, at more memory and CPU than a hash (Redis). Use a string for an object that is read and written whole, a hash for a flat object updated field by field, and JSON for nested data that is read or changed in parts.

One object stored three ways The same channel info, with a title, a game, and two moderators, stored as a string, a hash, and JSON. To change the title, a string is read and written whole; a hash writes one field, but the moderators must be packed into one string; JSON writes one path and keeps the list of moderators as a list. String one serialized blob Hash flat fields JSON nested document {"title":"Finals", "game":"chess", "mods":[17,203]} title Finals game chess mods "17,203" $.title: "Finals" $.game: "chess" $.mods: [17, 203] CHANGE THE TITLE GET, then SET all Reads and writes it all CHANGE THE TITLE HSET … title Writes one field CHANGE THE TITLE JSON.SET … $.title Writes one path

Redis finds a value only by its key. To find channels by game, keep your own indexIndexExtra data, built from the records, that finds a record without a full scan. An index speeds up reads, but it takes space and every write must update it too. So a database indexes only what its queries need.Read more: Storage engines: Finding a record fast: a set game:chess:channels that the app updates together with the channel (more on several values in one step). When the data needs queries by several fields, use the search indexes that Redis 8 has built in (more on searching in memory). Extra data about the members of a set or sorted set goes into a hash keyed by the member.

A command that touches every element of a big structure, such as HGETALL, SMEMBERS, or LRANGE 0 -1, takes time in proportion to its size, and other clients wait meanwhile (more on slow commands). Keep collections bounded with LTRIM and ZREMRANGEBYRANK, read them in pages, or split them over several keys.

Choose the structure by how the data is read and changed. Let a command change the value inside the server instead of reading and writing it from the client, and keep every collection bounded.

The service keeps a session for each of 20 million signed-in viewers and a hash for each of a million channels. All of it has to fit in RAM, together with the room that Redis needs to run.

Each key costs more than its bytes: Redis keeps an entry in its key table, an object header for the value, and an entry for the expiry if the key has one. For small values this overhead can be larger than the data, so estimate by loading a sample and reading MEMORY USAGE for a few keys and INFO memory for the total, instead of adding up raw sizes.

Small collections use much less memory: a hash or a sorted set below a size limit is stored as one compact array instead of a table with pointers. By default a hash stays compact up to 512 fields, with field names and values up to 64 bytes, and a sorted set up to 128 members of up to 64 bytes. A list is a chain of such compact arrays of about 8 KB each (Redis). Compact encoding uses up to ten times less memory, five times less on average (Redis). The price is CPU: a lookup inside the array scans it, so raising the limits a lot makes access slow.

A compact hash and a hash table Left: a small hash with three fields is stored as one compact array of fields and values, with little overhead, and a lookup scans the array. Right: past the size limit, the hash becomes a table of buckets that point to separate entries, each with its own pointers; a lookup is fast, but every entry costs extra memory. Small hash one compact array Large hash a table of entries title Finals game chess start 18:00 up to 512 fields, values up to 64 B Little overhead, lookups scan BUCKETS 0 1 2 3 title Finals game chess start 18:00 each entry: key, value, pointers Fast lookups, more memory

This allows a trick for many small values. Instead of one key per channel, put up to a few hundred channels into one hash: channel:42017 becomes field 17 of key channels:420. In one test, 370,000 city records took 44 MB in one big hash and 12 MB in hashes split this way (Carlson). The app maps IDs to buckets itself, and before Redis 7.4 the fields could not have their own TTLTime To LiveHow long a piece of data stays valid before it must be refreshed or dropped. Caches and DNS answers use a TTL to limit how long stored data can be reused. A short TTL means fresher data but more refreshes. A long TTL means fewer refreshes, but changes spread more slowly.Read more: Caching: Putting a cache in front of the database. For yes-or-no facts about users with dense numeric IDs, a bitmap stores one bit per user: 100 million users take 12 MB (Redis).

Grouping small values into hashes Left: one key per channel, so each channel costs a key entry and an object of its own. Right: the app puts channels into hashes by their ID: channel 42017 becomes field 17 of the hash channels:420, next to channels 42018 and 42019, so one key holds a hundred channels in a compact array. One key per channel A hash per 100 channels channel:42017 object {title…} channel:42018 object {title…} channel:42019 object {title…} … one key each channels:420 17 {title…} 18 {title…} 19 {title…} … 100 fields Overhead per channel Overhead per 100 channels

Size the server above the data itself:

  • Saving to disk forks the process, and every page that changes during the save is copied (more on saving to disk). With heavy writes, this copy can approach the size of the data.
  • Replication buffers and buffers for slow clients hold memory outside the data (more on copies).
  • Memory freed by deleted keys often stays with the process, so size for the peak, not for the average (Redis).

One book suggests keeping the data at 50–65% of the server’s RAM to leave room for the save and the buffers (Carlson). Treat it as a starting point and check the peak under load. Valkey 9.1 cut the overhead of each key further, by up to 44% of the overhead of small strings (Valkey).

Memory on a Redis server besides the data The RAM of one server as a bar. The data takes a little over half. The rest is room for pages copied during a save to disk, for the buffers of replicas and slow clients, and for memory freed by deleted keys that the process still holds, with some free space left. DATA FORK COPIES BUFFERS FREED FREE ABOUT 50–65% OF RAM HEADROOM

Estimate memory from a measured sample, not from raw sizes. Group many tiny values into hashes or bitmaps, keep compact limits near their defaults, and leave headroom for saving to disk and for buffers.

A moderator tool lists all viewers of a big channel with SMEMBERS on a set of half a million members. While Redis builds the reply, chat messages, viewer counts, and page reads from every other client wait.

Redis runs commands on one thread, one after another. So no command of another client runs in the middle of an INCR, and many app servers can update one counter without any coordination of their own. It also means that a command which touches N elements holds up everyone for the time it takes. The usual culprits are KEYS, which walks the whole key space, full reads of big collections, set operations on big sets, and DEL of a big key, which frees its memory before it returns (Redis). A long Lua script, a program that runs inside the server, blocks it the same way (more on scripts).

The fixes:

  • Iterate with SCAN, SSCAN, HSCAN, and ZSCAN. Each call does a bounded amount of work and returns a cursor and some elements; COUNT is a hint, not a page size, and a call may return none. Continue until the cursor is 0. A full pass returns every element that existed for the whole pass, and may return some of them twice (Redis).
  • Delete big keys with UNLINK, which frees the memory in a background thread.
  • Keep collections bounded, and run slow reads that can be a moment old on a replicaReplicaOne copy of the data in a replicated system. In a leader-based design, a replica is either the leader or a follower.Read more: Replication: Keeping a copy on another node, a copy of the server (more on copies).
One slow command blocks the other clients Time runs down. Left: client A runs SMEMBERS on a set of 500,000 members, one long command, and the GET of client C and the INCR of client D wait until it ends. Right: client A reads the same set with SSCAN in batches of 100, and the commands of C and D run between the batches. One big read SMEMBERS on 500,000 members MAIN THREAD GET B SMEMBERS viewers:42 A GET C INCR D C, D WAIT TIME Batches SSCAN with a cursor MAIN THREAD GET B SSCAN viewers:42 A GET C SSCAN viewers:42 A INCR D SSCAN viewers:42 A … A

To find slow commands, read SLOWLOG: by default it keeps the last 128 commands that took longer than 10 ms (Redis). One core at 100% while the request rate is normal is another sign.

Since Redis 6, I/O threads can read and write sockets and parse requests, while the commands still run on the main thread. They are off by default and pay off on servers with four or more cores. In the Redis 8 announcement, 8 I/O threads gave up to 112% more throughput (Redis), and Valkey 8 reached 1.2 million requests per second on one server, against 380,000 before its threading changes (Valkey). These are vendor numbers. I/O threads raise the ceiling for many small commands. A single slow command still blocks everyone.

I/O threads and the main thread Clients connect to Redis. I/O threads read requests from the sockets, parse them, and write the replies back. One main thread runs every command, one at a time, on the data in memory. Client 1 Client 2 Client 3 REDIS SERVER I/O thread read, parse, write I/O thread read, parse, write I/O thread read, parse, write Main thread one command at a time Data in memory

When the main thread itself is full, one server is the limit. Stripe ran its rate limiters on one Redis server and saw a single core pinned at 100% and a steady rate of timeouts. Moving to a cluster of 10 servers removed the errors, with little effect on latency (Stripe, 2018). To use more cores, run more Redis processes and spread the keys over them (more on many servers).

Running KEYS in production. It reads every key in one command and stops all other clients until it ends. Use SCAN.

The front page shows the viewer counts of 50 channels, and going live sets the broadcast info, adds the channel to the ranking, and resets its counters. Sent one by one, each command waits for the reply of the one before.

A small command takes far less time on the server than its reply takes to cross the network. At a round trip of 0.2 ms, one client that waits for each reply sends at most about 5,000 commands per second, however fast the server is; across regions, at 50 ms, it sends 20. In one test on a 1 Gbit/s network with a 0.27 ms round trip, a five-command update ran 739 times per second one command at a time and 2,841 times per second when the five were sent together (Carlson).

Three ways to pay fewer round trips, in order of preference (Redis):

  1. One command for many values: MGET reads the 50 counters at once, and HMGET reads several fields of one hash. For writes, MSET, HSET with several fields, and SADD with several members do the same.
  2. Pipelining: the client sends many commands without waiting, then reads all the replies. It also saves the server a system call per command, so throughput can grow up to about ten times (Redis). The server keeps the replies in memory until the client reads them, so send large jobs in batches, for example of 10,000 commands.
  3. A script, when a command needs the result of the one before it (more on scripts).
Three reads one by one, in a pipeline, and as one command Time runs down. Left: the client sends GET a, waits for the reply, then GET b, then GET c: three round trips. Middle: a pipeline sends the three GETs without waiting and then reads three replies: one round trip. Right: one MGET of a, b, and c and one reply: one round trip and one command. One by one 3 round trips Pipeline 1 round trip MGET 1 round trip, 1 command CLIENT SERVER CLIENT SERVER CLIENT SERVER GET a REPLY GET b REPLY GET c REPLY GET a GET b GET c REPLY REPLY REPLY MGET a b c 3 VALUES

A pipeline is not a transactionTransactionA group of reads and writes that the database applies as one unit. Either all of its changes happen or none do, and concurrent transactions do not see each other's half-done work.Read more: Transactions: Changing several rows at once: commands of other clients can run between its commands, and if the client fails halfway, the first part is already applied.

Connections cost too. Opening a connection for each command can cut throughput to a quarter or less, so keep long-lived connections in your client’s pool, or share them through its multiplexer (Carlson). Give blocking reads, such as a worker waiting for a job, and transactions their own connection, since they hold it while they wait. Many idle connections are not free either: in the Redis benchmark, a server with 30,000 connections handled half the throughput of one with 100 (Redis).

Read and write related values with one multi-key command or a pipeline. Reuse long-lived connections, and move logic that needs a reply before the next command into a script.

The chat lets a viewer send at most 5 messages in 10 seconds. The check reads a counter, compares it with the limit, and adds one; two app servers that serve the same viewer can both read 4 and both let a message through. Ending a broadcast must remove the channel from the ranking, delete its viewer count, and add an event, all together.

MULTI and EXEC wrap commands into a transaction: Redis queues them and, at EXEC, runs them in a row with no command of another client in between. If the client disconnects before EXEC, nothing runs. There is no rollbackRollbackThe end of a transaction that discards all of its changes. The app rolls back when a check fails, and after a crash the database rolls back every transaction that did not commit.Read more: Transactions: Changing several rows at once: a command with a syntax error makes EXEC refuse the whole transaction, but a command that fails while running, for example on a key of the wrong type, fails alone and the others stay applied (Redis). The client gets the results only after EXEC, so it cannot use one result to decide what to queue next: a transaction alone cannot “check, then act”.

A failed command inside a transaction A transaction queues three commands: SET title, SADD on the same key, and INCR viewers. At EXEC, the SET runs, the SADD fails because title holds a string, not a set, and the INCR still runs. Nothing is rolled back. QUEUED AFTER MULTI RESULT AT EXEC SET title "Finals" OK APPLIED SADD title x WRONGTYPE error FAILED ALONE INCR viewers 18343 APPLIED No rollback: the other two stay applied

WATCH adds that check. The client watches a key, reads it, and queues the change; if another client changed the key before EXEC, EXEC returns nothing and the client retries. This optimistic approach works while conflicts are rare. On one busy key it collapses: in a test with five buyers and five sellers on one market key, clients completed fewer than 600 purchases with 161,000 retries, while locks on single items completed 111,000 purchases with none (Carlson).

WATCH aborts a transaction and the client retries Time runs down. Client A watches the key limit:17 and reads 4. Client B adds one to the key. A sends MULTI, INCR, and EXEC; because the watched key changed, EXEC returns nil and nothing runs. A watches again, reads 5, sees the limit is reached, and does not add. CLIENT A REDIS CLIENT B WATCH limit:17 GET limit:17 4 INCR limit:17 5 MULTI, INCR, EXEC NIL: KEY CHANGED WATCH, GET 5: LIMIT REACHED

A Lua script runs on the server as one command: it can read, decide, and write, and nothing else runs until it ends. One script checks the chat limit and adds one in a single round trip. Like a transaction, it has no rollback: if it fails after a write, that write stays, so check inputs and key types before changing anything. Uber caches database rows in Redis and writes them with a script that compares the row’s timestamp with the cached one, so that an older row never replaces a newer one (Uber, 2024). The rules for scripts:

  • A script blocks the server like a slow command. After 5 seconds by default, Redis answers other clients with a busy error. A script that has not written yet can be killed; one that has written can only be stopped by shutting down without saving (Redis).
  • Pass the names of all keys the script touches in its list of keys, separately from its other arguments. Redis needs them to route the script in a cluster (more on many servers).
  • Redis caches scripts sent with EVAL, but the cache can lose them at any time: on a restart, a failoverFailoverMoving work to another instance when the active one fails. In replication, a follower that has every confirmed write becomes the leader. A timeout only suspects the failure, so the old leader must be fenced.Read more: Replication: Replacing a failed leader, SCRIPT FLUSH, or, since Redis 7.4, when it evicts old scripts. A client that calls a script by its hash must send the script again when Redis answers NOSCRIPT. Redis functions, since Redis 7.0, are named scripts that Redis stores and copies with the data (Redis).
Checking a limit with two commands and with one script Time runs down. Left: app servers 1 and 2 both GET the counter of a viewer and see 4, below the limit of 5; both INCR it, and the viewer sends 6 messages. Right: each app server runs one script that checks and adds in one step; the first gets 5 and is allowed, the second is refused. GET, then INCR two commands One script check and add in one step APP 1 REDIS APP 2 APP 1 REDIS APP 2 GET 4 GET 4 INCR 5 INCR 6 6 messages pass the limit of 5 EVAL LIMIT 5, OK EVAL LIMIT REFUSED The second message is refused

Many such scripts are now single commands. SET with NX writes only if the key is absent, and with IFEQ only if the current value matches, which is a compare-and-setCompare-and-setA write that succeeds only if the value is still the one the writer read. If another writer changed it first, the write does nothing, and the writer reads again and retries.Read more: Transactions: Two writers on one value without a script (Redis 8.4, Valkey 8.1) (Redis). Redis 8.8 added INCREX, which adds to a counter with an upper bound and an expiry in one command, the core of a rate limitRate limitA cap on how many requests a client may send in a period of time. Rate limits protect a service from overload and abuse. Requests over the limit usually get HTTP 429.Read more: Networking: When a call fails (Redis). The limit algorithms themselves are a topic of their own. A change that fits one hash, such as HSET of several fields, is already atomicAtomicityThe rule that a set of changes is applied fully or not at all. A failure in the middle never leaves part of the changes behind. A transaction gives it to several rows, and most stores give it to a single record.Read more: Transactions: Changing several rows at once, so keeping values that change together in one key avoids transactions altogether.

A lost reply does not mean that a command failed: an INCR may have run before the connection broke, so retry only what is safe to repeat (more on failed calls). These steps hold only inside one Redis server. They do not make Redis and the database change together (more on atomic changes).

Put values that change together into one key when you can. Use a script or a single conditional command for “check, then write”, WATCH only when conflicts are rare, and MULTI for a group of writes that must not interleave with other clients.

A session should disappear after 30 days without visits, a cached page after five minutes, and a viewer’s “watching” mark a minute after the player stops sending heartbeats.

Each key can have a TTL: SET with EX, or EXPIRE on an existing key. Redis stores the moment of expiry as an absolute time, so it follows the server’s clock; moving the clock forward expires keys early (Redis). A command that replaces the value, such as SET, removes the TTL, while a command that changes the value in place, such as INCR, LPUSH, or HSET, keeps it. A cached page rewritten with a plain SET becomes permanent, so when you replace a short-lived value, give it its expiry again or keep the old one with KEEPTTL. Fields of a hash can expire on their own since Redis 7.4 and Valkey 9.0.

Which commands keep a TTL Commands on one key in time order. SET with EX 60 gives it a TTL of 60 seconds. INCR changes the value in place and keeps the TTL. SET with KEEPTTL keeps it too. A plain SET replaces the value and removes the TTL, so the key never expires. COMMAND AFTER IT SET viewers:42 0 EX 60 TTL 60 s INCR viewers:42 TTL 55 s, kept SET viewers:42 7 KEEPTTL TTL 50 s, kept SET viewers:42 9 no TTL: kept forever

An expired key is never returned, but its memory is freed later. Redis deletes a key when a command touches it, and a background cycle, about ten times a second, samples keys that have a TTL and deletes the expired ones. While many keys in the samples turn out to be expired, it keeps sampling, until it runs out of its time budget. When a large share of keys expires in the same second, for example a million “watching” marks set with one expiry time at the end of a tournament, this cleanup takes CPU from commands and raises latency (Redis). The same moment empties the cache for all those keys at once (more on many misses at once). Add a random spread to TTLs that would otherwise end together.

How Redis deletes expired keys in the background About ten times a second, Redis samples keys that have a TTL and deletes the expired ones. While many keys in the samples are expired and the cycle still has time left, it samples again; otherwise it stops until the next cycle. When a million keys expire in the same second, most samples are expired, and the cycle keeps running up to its time budget, taking CPU from commands. Each cycle about every 100 ms Sample keys that have a TTL Delete the expired ones Many expired and time left? YES: SAMPLE AGAIN NO: STOP UNTIL THE NEXT CYCLE Many keys with one expiry time keep the cycle busy up to its time budget

A sliding expiry, where each visit moves the session’s end forward, turns every read into a write that goes to replicas and to disk. Refresh it only when several minutes have passed since the last refresh. Replicas do not delete expired keys themselves: they wait for the delete from the server they copy, and until then hide expired keys from reads.

Redis can send an event when it deletes an expired key, but it fires when the deletion happens, not at the exact expiry time, so it is not a timer for jobs (Fugaro and Ortensi).

Give every copy and every short-lived value a TTL, and set it again or keep it with KEEPTTL when you replace the value. Spread TTLs that would end together, and do not use expiry events as timers for work.

A big tournament doubles the number of live channels, and the cache grows with them until Redis reaches the memory of its server.

By default Redis has no memory limit: it grows until the operating system starts swapping its pages to disk, which makes every command slow, or kills the process. Set maxmemory below the RAM, leaving room for saving to disk and for buffers (more on fitting in memory). The buffers that hold writes for replicas and for the log on disk are not counted in maxmemory, and the process can use more memory than the limit, so this room is needed even with the limit set; mem_not_counted_for_evict in INFO memory shows those buffers (Redis).

At the limit, the evictionEvictionDropping a value from a full cache to make room for another. A policy such as LRU or LFU picks the value, whether or not the source changed. The next read of it misses.Read more: Caching: Choosing what to drop when memory is full policy decides what happens to a write that needs more memory:

Policy At the limit Fits
noeviction (default) Writes that add data fail with an error; reads work Data that must not vanish: sessions kept only here, queues
allkeys-lru Drops the key that was used longest ago A cache, as the default choice
allkeys-lfu Drops the key used least often, with counts that fade over time A cache where popularity is stable and one-off reads would push out hot keysHot keyOne key value that gets more traffic than a whole shard can serve. Adding or moving shards does not help, since the key lives on one shard. Cache it for reads, or salt it for writes.Read more: Sharding: One key with too much traffic
volatile-lru, volatile-lfu The same as LRULeast Recently UsedAn eviction policy that drops the value read longest ago. It is the usual default for caches. A single pass over cold data can push out the hot values.Read more: Caching: Choosing what to drop when memory is full and LFULeast Frequently UsedAn eviction policy that drops the value read least often. It keeps steady favorites through a scan. Its counts must decay, or values popular yesterday stay too long.Read more: Caching: Choosing what to drop when memory is full, but only among keys with a TTL One server for a cache with TTLs and data without them
volatile-ttl Drops the key with a TTL that expires soonest Keys whose TTL marks how long they are worth keeping
allkeys-random Drops a random key Keys read about equally often

Redis does not track exact use: it samples five keys and drops the best candidate among them, and ten samples come very close to exact LRU (Redis). Redis 8.6 added LRM policies, which count only writes as use. The volatile-* policies behave like noeviction when no key has a TTL, and a cache key written without a TTL by mistake can never be evicted. Redis’s own advice is to run two servers instead: one cache with allkeys-lru and one store with noeviction.

allkeys-lru and volatile-lru on a server with cache and sessions One server holds cached pages and sessions; page:3 was written without a TTL by mistake. At the memory limit, allkeys-lru drops the key used longest ago, session:a, and logs a viewer out. volatile-lru drops only keys with a TTL, page:1 and then page:2; page:3 and the sessions are never dropped, and once no key with a TTL is left, writes fail. allkeys-lru any key can go volatile-lru only keys with a TTL can go KEY EXPIRY LAST USE page:1 TTL 9 min ago page:2 TTL 1 min ago page:3 no TTL 20 min ago session:a no TTL 30 min ago session:b no TTL now KEY EXPIRY LAST USE page:1 TTL 9 min ago page:2 TTL 1 min ago page:3 no TTL 20 min ago session:a no TTL 30 min ago session:b no TTL now Drops session:a: a logout Then writes fail: page:3 stays

Eviction is fine for copies and harmful for data kept only in Redis: an evicted session logs a viewer out. Watch the hit ratioHit ratioThe share of requests a cache answers without going to the source. It sets how much load and latency a cache removes. It depends on the number of keys, the memory, and how often values change.Read more: Caching: When a cache pays off and the count of evicted keys together (more on choosing what to drop). Replicas do not evict by themselves: they apply the primary’s deletions, so give them the same memory as the primary.

Always set maxmemory. Use allkeys-lru for a cache and noeviction for data kept only in Redis, on separate servers, and alert on evictions in the store and on a falling hit ratio in the cache.

The channel page shows how many viewers watch right now, the broadcast summary shows how many different viewers came today, and the front page ranks live channels by viewers.

INCR, INCRBY, and HINCRBY change a counter atomically in one command, so many app servers can count the same thing without locks. A popular channel turns its counter into one very busy key; adding up increments in each app server and sending them once a second cuts the writes to one per server (more on a hot key). For counts per minute or per hour, keep one hash per counter with a field per time bucket, and let old fields expire.

“Watching right now” needs to forget viewers who left without saying so. A sorted set per channel can hold the viewer as member and the Unix time of the last heartbeat as score: ZADD updates the score, ZREMRANGEBYSCORE removes members older than a cutoff, and ZCARD gives the count. It holds one member per viewer, so a channel with half a million viewers keeps half a million members in memory. How to track presence at scale is a topic of its own.

Counting viewers who are watching now The sorted set watching:42 holds one member per viewer, scored by the Unix time of the last heartbeat. At time 1761307230, ZREMRANGEBYSCORE removes members with scores up to 1761307170, a minute ago: user:88 and user:9. ZCARD then returns 3. watching:42 MEMBER LAST HEARTBEAT, UNIX TIME user:17 1761307228 user:203 1761307225 user:51 1761307201 user:88 1761307170 user:9 1761307140 A MINUTE OLD ZADD watching:42 1761307228 user:17 each heartbeat ZREMRANGEBYSCORE watching:42 -inf 1761307170 at 1761307230: removes 2 ZCARD watching:42 returns 3

“Different viewers today” with a set of viewer IDs is exact, but it grows with every viewer. A HyperLogLog estimates the number of distinct items in at most 12 KB per key, with a standard error of 0.81% (Redis). PFADD adds a viewer and PFCOUNT returns the estimate. Daily counts cannot be added up into a weekly one, since a viewer who came every day would count seven times; PFMERGE merges the daily keys into a weekly estimate instead (Fugaro and Ortensi).

Distinct viewers with a set and with a HyperLogLog Left: a set stores every viewer ID, so it is exact but grows with the number of viewers. Right: a HyperLogLog stores no IDs, takes at most 12 KB, and gives an estimate with a standard error of about 0.81%. Below: the HyperLogLogs of seven days are merged with PFMERGE into a weekly estimate, in which a viewer who came every day is not counted seven times. Set SADD, SCARD HyperLogLog PFADD, PFCOUNT user:17 user:203 user:88 user:51 … 1 million IDs Exact, grows with every viewer 12 KB at most no IDs inside ≈ 1,000,000, standard error 0.81% mon tue wed thu fri sat sun PFMERGE week: one estimate Adding seven daily counts would count a daily viewer seven times.

A sorted set keeps members ordered by score, so it is a ranking: ZINCRBY changes a channel’s score, ZREVRANGE live 0 9 returns the top ten, and ZREVRANK the position of one channel, each in O(log N) time plus the members it returns. Members with equal scores are ordered by their bytes. Keep the set bounded, for example with ZREMRANGEBYRANK, and remove channels that went offline.

A live ranking in a sorted set The sorted set live holds channels scored by viewers. ZINCRBY adds 6000 to channel 15, which moves from third to second place. ZREVRANGE live 0 2 returns the top three: 42, 15, and 7. ZREVRANK of channel 15 returns 1, its position counted from 0. Before CHANNEL SCORE 42 18,342 7 9,120 15 4,410 23 3,980 9 1,200 After CHANNEL SCORE 42 18,342 15 10,410 7 9,120 23 3,980 9 1,200 ZINCRBY live 6000 15 TOP 3 ZREVRANGE live 0 2 → 42, 15, 7; ZREVRANK live 15 → 1

Redis 8 also has built-in structures for counts that may be approximate: Top-K for the most frequent items, Count-Min sketch for the frequency of each item, and Bloom filtersBloom filterA small bit array that says whether a key is definitely absent or maybe present. It never misses a key that is there, but it sometimes says "maybe" for a key that is not. A lookup by key uses it to skip files that cannot hold the key; range reads get no help.Read more: Storage engines: Keeping reads cheap for “seen before” checks. Valkey provides Bloom filters as a module. How to combine these into counters at scale is a pattern of its own.

Use INCR for exact counters, a sorted set of heartbeats for “online now”, HyperLogLog for distinct counts that may be off by about one percent, and a bounded sorted set for rankings.

Viewers’ chat connections are spread over 200 app servers. A message sent to a channel must reach every server that holds a viewer of that channel, at once.

Pub/Sub delivers each message to the clients subscribed to its channel at that moment. Each app server subscribes to the channels its viewers watch, over one connection per Redis server, not one per chat channel, and PUBLISH sends a message to all of them; PSUBSCRIBE subscribes to a pattern of names. Redis stores nothing: delivery is at most once, and a server that was disconnected for a second has lost that second’s messages for good (Redis). After a reconnect, it reloads the recent history from a list or a stream (more on work that survives a worker crash).

One PUBLISH reaches the subscribed servers App server 1 publishes a message to the channel chat:42. Redis sends it at once to app servers 2 and 3, which are subscribed to chat:42. App server 4 is subscribed too, but its connection dropped a moment ago, so it never gets the message. Redis keeps no copy. App server 1 PUBLISH chat:42 Redis stores nothing MESSAGE App server 2 subscribed App server 3 subscribed App server 4 reconnecting

A subscriber that reads slower than messages arrive fills its output buffer on the Redis server. By default Redis disconnects it when the buffer passes 32 MB, or stays above 8 MB for a minute (Redis). The subscriber then misses messages, as with any other disconnect.

In a cluster, PUBLISH sends each message to every node, so the work grows with the number of nodes instead of spreading over them. Figma moved its pub/sub load to a clustered Redis service, and a few weeks later one node stayed at 100% CPU; the cause was a bug in how the cluster’s internal links buffer these messages, and the fix was to move pub/sub off the cluster (Figma, 2022). Sharded Pub/Sub (SPUBLISH, SSUBSCRIBE, since Redis 7.0) maps each channel to a slot, like a key (more on slots), and keeps its messages within the shard that owns it: its primary and its replicas. Adding shards adds capacity. It has no pattern subscriptions, and the client must support it (Redis). How to route messages to millions of connections is a topic of its own.

PUBLISH and SPUBLISH in a cluster A cluster of three shards, A, B, and C, each a primary with replicas. Left: PUBLISH to chat:42 reaches shard A, which forwards it to every other shard over the cluster bus, whether or not they have subscribers. Right: SPUBLISH goes to shard B, which owns the slot of chat:42, and stays inside it; subscribers of chat:42 connect to its primary or its replicas. PUBLISH every shard gets every message SPUBLISH only the shard that owns the slot Shard A with replicas Shard B with replicas Shard C with replicas PUBLISH CLUSTER BUS Shard A with replicas Shard B with replicas Shard C with replicas SPUBLISH Load grows with shards Load stays in one shard

Pub/Sub adds almost no delay and keeps no state, so it is cheap. In return, any message sent while a subscriber is away or too slow is lost, and the sender never learns about it.

Handing out work that survives a worker crash

Section titled “Handing out work that survives a worker crash”

When a big streamer goes live, two million viewers who follow the channel must get a notification. The app splits them into jobs of a thousand viewers each, and a pool of workers sends them. A worker can crash in the middle of a job, and the job must then go to another worker.

A list is the simplest queue: the app pushes jobs with LPUSH, and workers wait for them with BRPOP. The pop removes the job, so a worker that crashes after the pop loses it. A safer form moves the job atomically to the worker’s own processing list with BLMOVE and deletes it there when done; a separate check returns jobs that stayed in a processing list too long. A sorted set scored by start time can hold delayed jobs (Carlson).

A worker crash with BRPOP and with BLMOVE Left: a worker takes job 1 from the list jobs with BRPOP and crashes; the job is no longer in Redis and is lost. Right: the worker moves job 1 with BLMOVE into its own list processing:w1 and crashes; the job is still there, and a check moves it back to jobs after a timeout. BRPOP the pop removes the job BLMOVE the job moves to a processing list jobs job 3 job 2 Worker jobs job 3 job 2 Worker job 1 LOST A crash loses the job job 1 processing:w1 BACK AFTER TIMEOUT A crash keeps the job

A Redis stream is an append-only log in one key. XADD appends an entry, and Redis gives it an ID made of the time in milliseconds and a sequence number. A consumer group shares the entries among its workers: XREADGROUP hands each new entry to one worker and keeps it in the group’s list of pending entries until the worker confirms it with XACK. Entries of a worker that went silent stay pending; XAUTOCLAIM reassigns those idle longer than a set time to another worker. Each entry counts its deliveries, so an entry that has failed many times can be moved to a separate stream for people to look at (Redis). This is at-least-once delivery: a job can run twice, so the handler must be safe to repeat.

A consumer group hands each entry to one worker The group notifier reads the stream notify. 101-0 went to worker 1, which acknowledged it with XACK, so it left the pending list. 102-0 went to worker 1, which then died; it stayed pending, and XAUTOCLAIM gave it to worker 3 once it was idle too long. 103-0 and 104-0 went to worker 2 and are pending until it acknowledges them. 105-0 has not been delivered yet. ENTRY GIVEN TO STATE IN THE GROUP 101-0 Worker 1 acknowledged: XACK 102-0 Worker 1 pending, idle too long 103-0 Worker 2 pending until XACK 104-0 Worker 2 pending until XACK 105-0 not delivered yet XAUTOCLAIM Worker 3 Each entry goes to one worker and stays pending until XACK

Several groups can read the same stream, each getting every entry: the notifier and the analytics job each keep their own position.

Two groups read one stream The stream notify holds entries 101-0 to 105-0, and XADD appends new ones. The group notifier has received 101-0 to 104-0 and reads 105-0 next. The group analytics has received 101-0 and 102-0 and reads 103-0 next. Both groups get every entry, each at its own position. notify 101-0 102-0 103-0 104-0 105-0 XADD GROUP RECEIVED notifier NEXT: 105-0 analytics NEXT: 103-0 Each group gets every entry and keeps its own position

A stream holds its entries until they are trimmed, by length with XADD ... MAXLEN ~ 1000000, where ~ lets Redis cut whole internal blocks cheaply, or by ID with MINID. To keep a time window, turn the cutoff time into an ID: IDs start with the time. Trimming does not wait for the groups: it can delete entries that no worker has processed yet, and a pending entry then has no data to retry. Keep enough history for the slowest group and watch its lag, or, since Redis 8.2, trim with ACKED to delete only entries that every group has acknowledged; then a stuck group stops the trimming. Since Redis 8.6, XADD can drop an entry that a producer sends twice after a retry. The producer passes a stable producer ID and, with IDMP, the same message ID on every retry; with IDMPAUTO, Redis derives the message ID from the content. Redis remembers these IDs only for a set time, 100 seconds by default, and a set number per producer, so size both for the longest retry (Redis). The handlers on the other side still run at least once.

A stream is not a distributed log. It is one key on one server, so its throughput and size are limited by that server and its memory: to spread the load, split it into several streams, for example by viewer ID. It is only as safe as Redis’s saving and copying: a failover can lose entries that were acknowledged (more on losing writes in a failover). In a test on a slow machine of its time, 99.9% of entries reached a consumer within 2 ms, and XADD with pipelining reaches half a million to a million entries per second (Redis). Use a log or a broker built for it when the history to keep does not fit in RAM, partitions must scale on their own, or the work must survive a failover of Redis. Jobs that must be created in the same transaction as the business data fit a job table in the database better (more on jobs in PostgreSQL).

Use a stream with a consumer group for jobs that must survive a worker crash, make every handler safe to run twice, and trim only what all groups have processed. Use a plain list only for jobs you can afford to lose, and a dedicated log or broker when the history outgrows RAM or the work must survive a failover of Redis.

When a broadcast ends, a worker gets the job to build its highlights video. The worker pauses for a while, the job goes to a second worker, and now both may build the same video. When a channel’s cached page expires, only one app server should rebuild it from the database.

A lock in Redis is a key that only one client can create:

SET lock:highlights:42 7f3a9c NX PX 30000

NX writes the key only if it does not exist, and PX 30000 deletes it after 30 seconds, so the lock frees itself if its holder dies. The value is a random token of the holder. Release the lock only if the key still holds your token: DELEX with IFEQ in Redis 8.4, DELIFEQ in Valkey 9.0, or a short script on older versions (Redis). A plain DEL can remove a lock that another worker took after yours expired.

Releasing a lock with DEL and with a token check Time runs down. Worker A takes the lock with token a1 and pauses longer than the TTL. The key expires, and worker B takes the lock with token b7. Left: A wakes up and sends DEL, which deletes B's lock. Right: A sends DELEX with IFEQ a1; the key holds b7, so nothing is deleted. DEL deletes any value WORKER A REDIS WORKER B SET … a1 NX PX OK PAUSE TTL ends SET … b7 NX PX OK DEL DELETES B'S LOCK DELEX … IFEQ a1 deletes only token a1 WORKER A REDIS WORKER B SET … a1 NX PX OK PAUSE TTL ends SET … b7 NX PX OK DELEX … IFEQ a1 0: NOT YOURS B now works without a lock B's lock stays

The expiry is also the weak spot. If the holder stops for longer than the TTL, for a garbage collection pause or a slow network call, the lock expires and a second worker takes it while the first still works. A long job extends its lock while it runs, only if the key still holds its token, in one conditional command or script, and stops when the extension fails; a pause can still outlast the extension. A failover breaks the lock too: the primary confirms the key, dies before a replica gets it, and the promoted replica lets a second worker take the same lock (Redis).

A failover gives the same lock to two workers Time runs down. Worker A takes the lock on the primary, which answers OK and dies before sending the key to the replica. The replica is promoted. Worker B asks the new primary for the same lock, the key is not there, and B gets it too. Both workers now hold the lock. WORKER A PRIMARY REPLICA WORKER B SET lock … NX OK NEVER SENT CRASH promoted SET lock … NX OK A and B both hold the lock

Redlock, proposed by the author of Redis, takes the lock on a majority of five independent servers to survive a failure. Martin Kleppmann argued that its safety depends on timing assumptions and that it gives no number that the protected resource can check, so it is not safe for correctness (Kleppmann); Salvatore Sanfilippo answered that the algorithm checks the elapsed time and is safe under bounded clock driftClock driftThe difference in how fast two clocks run. A quartz clock that drifts by 200 parts per million, a common assumption, gains about 17 seconds a day without syncing.Read more: Consistency: Clocks that disagree (Sanfilippo). The practical rule follows from what a double holder costs (more on letting only one node act): a lock that only saves duplicate work, like the highlights video, is fine on one Redis key; a lock that protects data from corruption needs a consensusConsensusSeveral nodes agreeing on one value or one order of writes, even when some fail. Crash-tolerant majority protocols need 2F + 1 voting nodes to keep working after F crashes, and progress needs timely messages. Raft, Paxos, and ZAB are common algorithms.Read more: Consistency: Agreeing on one value store and a check at the resource, or a lock in the database that holds the data (more on locks in PostgreSQL).

Use a Redis lock to avoid duplicate work, with a random token, an expiry, and a release that checks the token. When two holders would corrupt data, use a consensus store with fencing tokensFencing tokenA number that grows with every grant of a lock, checked by the system that receives the work. The receiver durably keeps the largest token it has accepted and, in the same atomic operation as the change, rejects older ones. A stale holder is stopped only where a newer token has already arrived.Read more: Consistency: Letting only one node act, or the database’s own locks.

Viewers search live channels by words in the title and filter by game and language. A “near you” shelf shows broadcasts from the viewer’s city, and a recommendation shelf shows broadcasts similar to the one being watched.

Redis Search, built into Redis 8, keeps secondary indexesSecondary indexAn additional index that finds records by columns other than the primary key. Values need not be unique, so each entry also identifies its record: by its place in the table or by its primary key. Every write that changes the indexed columns must update it.Read more: Storage engines: Searching by other columns over hashes or JSON documents whose keys start with a given prefix. An index lists the fields and their types: TEXT for words, TAG for exact values such as a game, NUMERIC for ranges, GEO for places, and VECTOR for embeddingsEmbeddingA vector of numbers that a model computes from a text or an image. Similar meanings get vectors close to each other, so a nearest-neighbor search finds similar items. Vectors from different models cannot be compared.Read more: Databases: Searching by meaning. Redis updates the index on every write to an indexed key, so a search sees the new and changed documents, and both the index and the writes cost memory and CPU on the same server. A new index fills with the existing documents in the background, and a document whose fields do not fit the schemaSchemaThe structure that records follow: their fields and the types of the fields. A relational table enforces its schema on every write. Without that check, the code that reads the data still relies on one.Read more: Databases: Records with different fields is not indexed, so check FT.INFO for progress and errors. FT.SEARCH runs a query, and FT.AGGREGATE groups and counts results, for example the number of live channels per game. An existing index can only gain fields; to change one, build a new index, wait until it is filled, and switch an alias to it (Fugaro and Ortensi). In a cluster, each shard indexes its own keys, and a query asks every shard. Valkey offers Valkey Search as a module, built first for vectors and extended to text, tags, and numbers in version 1.2 (Valkey).

A search index over hashes Hashes with keys starting with channel: hold a title, a game, and a viewer count. The index idx:channels covers them: the title as TEXT, the game as TAG, the viewers as NUMERIC. Each HSET to an indexed key updates the index. FT.SEARCH for chess channels with "finals" in the title reads the index and returns channel:42. HASHES channel:42 Finals day 2 · chess channel:7 Speedrun · minecraft channel:15 Openings · chess idx:channels title: TEXT game: TAG viewers: NUMERIC HSET FT.SEARCH idx:channels @game:{chess} finals channel:42 Each write to an indexed key also updates the index

Without the search module, Redis still has geo commands. GEOADD stores points in a sorted set, with the score encoding the position as a geohash, and GEOSEARCH finds members within a radius or a box: it reads the geohash areas around the point and then checks the exact distance (Redis). Updating a position is one GEOADD, so it suits objects that move.

How GEOSEARCH finds members within a radius A grid of geohash areas. To find members within the radius of a point, Redis reads the nine areas around the point and gets every member in them as a candidate. Then it checks the exact distance: members inside the circle are returned, members in the areas but outside the circle are dropped, and members in other areas are not read at all. areas around the point: read inside the radius: returned in the areas, outside the radius: dropped other areas: not read

Vector search compares an embedding of the current broadcast with the embeddings of others. The embeddings come from a model outside Redis. A FLAT index compares the query with every vector and is exact; an HNSWHierarchical navigable small worldAn approximate vector index built as a layered graph of nearby vectors. It is fast with good recall, but takes memory and time to build.Read more: PostgreSQL: Searching by meaning index walks a graph, finds most nearest neighbors much faster, and may miss some. A query can filter by tags or numbers first, for example only broadcasts in the viewer’s language (Fugaro and Ortensi). Redis 8 also has vector sets, a separate structure for the same task.

All of this lives in RAM next to everything else in the server. It fits when the data is already in Redis, fits in memory, and needs filters and short answers. When the data lives in PostgreSQL and search is a small part of the load, search it there (more on search in PostgreSQL). Redis Search has stemming, fuzzy and prefix matching, scoring, and aggregations; use a separate search engineSearch engineA separate store built for full-text search, such as Elasticsearch or OpenSearch. It adds language analysis, tuned relevance, and facets. In most designs it holds a copy of data owned elsewhere, which lags behind.Read more: Databases: Searching by words when the index outgrows memory, when the needed analyzers, ranking, or aggregations go beyond Redis Search, or when search must scale apart from the rest of Redis.

Search inside Redis when the data is already there and fits in memory. Index only the fields that queries use, and move to a search engine when the index outgrows RAM or search quality needs more than filters and simple relevance.

Redis restarts after a kernel update. Sessions and the live ranking should come back; the cache may start empty, but then every read goes to the database at once (more on losing the cache).

Redis has two ways to keep data on disk, and they map to the general choices of an in-memory store (more on keeping data in memory):

  • An RDB snapshot (Redis Database file) holds the whole dataset at one moment. Redis forks, and the child process writes the file while the parent goes on serving. By default it saves after an hour with one change, after 5 minutes with 100 changes, or after a minute with 10,000 changes (Redis). A crash loses everything written since the last snapshotSnapshotThe state of data at one moment. An in-memory store saves one to disk for recovery and replays the log written after it. A transaction can read from one, so data does not change under it.Read more: Storage engines: Back to memory, usually minutes. The file is compact, loads fast, and is the natural backupBackupA copy of data as it was at an earlier moment. Replicas copy a bad write within seconds, so recovering data from before it needs an older copy. A backup is proven only by a test restore.Read more: Replication: Undoing a bad write.
  • The AOF (append-only file) is a log of every write command. With appendfsync everysec, a background thread flushes it about once a second, so a crash of the machine usually loses about the last second; a crash of the Redis process alone loses nothing that reached the operating system. With always, Redis flushes before it replies, grouping the writes of many clients into one flush. With no, the operating system flushes when it likes, on Linux about every 30 seconds (Redis). The log is compacted in the background by a rewrite, which forks too.

The AOF is off by default, so a default Redis can lose minutes of writes. On a laptop benchmark with pipelining, everysec handled 118,483 SET commands per second with a p99 of 41 ms, and always handled 15,340 with a p99 of 65 ms (Fugaro and Ortensi). Redis’s own advice is to turn on both for a safety close to PostgreSQL’s, or to use snapshots alone if a few minutes of loss is acceptable; on restart, Redis loads the AOF, the more complete of the two.

What a crash loses with snapshots and with the AOF Two timelines over ten minutes, with a crash at 8:20. Top: snapshots at 0:00 and 5:00, so the crash loses everything written after 5:00, more than three minutes. Bottom: the AOF is flushed once a second, so a crash of the machine usually loses about the last second. Snapshots every 5 min RDB RDB LOST: 3 MIN 20 S AOF flushed every second LOST: ABOUT 1 S CRASH 8:20 0:00 5:00 10:00

A fork copies the process’s page table, and the server stops while it does. Measurements in the Redis docs, from older hardware, range from about 10 ms per GB of data on physical machines to about 240 ms per GB on old Xen-based cloud instances (Redis), so a large server can pause for a noticeable time. Measure it on your own servers: INFO reports the last fork time in latest_fork_usec. After the fork, every page the parent changes is copied, so a busy server needs memory for those copies (more on fitting in memory). Turn off transparent huge pages in Linux: with them, a few writes copy most of the memory and the latency jumps.

Copy-on-write after a fork After a fork, the parent process and the child that writes the snapshot share the same memory pages. The child only reads them. When the parent changes a key on page 3, the kernel copies page 3 for the parent, so each changed page takes memory twice until the save ends. Parent serves clients Child writes the snapshot SHARED PAGES page 1 page 2 page 3 page 4 page 5 page 6 PARENT WRITES page 3' a copy for the parent: memory used twice

When a save fails, for example on a full disk, Redis by default refuses writes until a save succeeds, so alert on failed saves and AOF writes. A restart reads the whole dataset back into memory before Redis serves it, so a large server comes back slowly. Copy snapshots to storage outside the machine on a schedule, and test restoring them. A replica is not a backup: an accidental FLUSHALL reaches it within milliseconds (more on undoing a bad write). Redis 8.10 added BACKUP commands that take consistent backups on each node of a cluster without all nodes forking at once (Redis).

For a cache, turn persistence off or keep snapshots for a faster warm-up. For data kept only in Redis, turn on the AOF with everysec and keep snapshots for backups; use always only where losing the last second is not acceptable and the write rate allows it.

If the Redis server’s machine dies, its data is gone until a restore, and the front page reads all hit one server. A copy on a second machine answers both problems.

A replica is a copy of a Redis server that connects to it with REPLICAOF; the server it copies is the primary, which Redis also calls a master. The primary forks, sends a snapshot, and then sends every write command to the replica. Replication is asynchronous: the primary replies to the client without waiting for replicas to confirm the write. After a short break, a replica resumes from a backlog of recent writes on the primary; if the backlog has moved past it, the primary sends a full snapshot again, with another fork and a full copy over the network. The backlog is 1 MB by default (Redis), which a busy server fills in a fraction of a second, so raise it. While a replica loads a full snapshot, it blocks its clients, for many seconds on a large dataset (Redis).

Partial and full resync after a break The primary has written up to offset 5,000 MB of its replication stream and keeps the last 1 MB, from 4,999 to 5,000, in its backlog. Replica 1 broke off at 4,999.5 MB, inside the backlog, and gets only the missing part. Replica 2 broke off at 4,990 MB, before the backlog starts, so the primary forks and sends a full snapshot. Stream of the primary BACKLOG: 1 MB 4,988 MB 4,990 MB 4,995 MB 5,000 MB Replica 1 partial: only the missing writes Replica 2 full: fork and send a snapshot

Replicas can take reads that may be a moment old, such as the front page ranking (more on reading from a copy that is behind). They reject writes by default.

Two settings narrow the window of lost writes:

  • WAIT N timeout blocks until N replicas confirm all earlier writes of the same connection, so send it on the connection that wrote. It returns how many replicas confirmed. It makes losing a write unlikely, not impossible: a failover can still pick a replica without it. A count below N means the write is already applied on the primary, and it is unknown whether it will survive (Redis). WAITAOF waits for the write to be flushed to the AOF on the primary, on replicas, or both, and returns both counts.
  • min-replicas-to-write 1 with min-replicas-max-lag 10 makes the primary refuse writes when no replica has confirmed anything for 10 seconds. A primary cut off from all its replicas then stops taking writes that would be lost, at the price of refusing writes when the replicas are down.
A primary without persistence restarts empty Time runs down. The primary holds 20 GB with persistence off, and two replicas copy it. The primary crashes and restarts at once with no data. The replicas reconnect, and the primary sends each of them its empty dataset as a full snapshot, which replaces their own data. All three now hold nothing. PRIMARY REPLICA 1 REPLICA 2 20 GB 20 GB 20 GB CRASH restart: 0 keys EMPTY SNAPSHOT EMPTY SNAPSHOT 0 keys 0 keys Every copy is gone

A primary without persistence that restarts automatically. It comes back empty, its replicas copy the empty dataset, and every copy is gone (Redis). Turn on persistence on the primary, or turn off its automatic restart.

The primary’s machine dies during a big broadcast. A replica has almost all the data, but the app servers still send writes to the dead address.

Sentinel runs failover for a primary and its replicas. It is a separate process; run at least three on machines that fail independently, such as in different zones. When a set number of Sentinels, the quorum, cannot reach the primary, they mark it as down. Then a Sentinel needs votes from a majority of all Sentinels, or from the quorum if it is larger, before it promotes a replica, so a Sentinel cut off with a minority never fails over. It picks the replica by priority and by how many of the primary’s writes it received, points the other replicas at it, and tells clients the new address: clients ask the Sentinels where the primary is (Redis). The time to detect a failure is a setting, down-after-milliseconds, 30 seconds by default (Redis).

Sentinel on three machines Three machines in three zones. Each runs a Redis server and a Sentinel: the primary in zone A and replicas in zones B and C. The Sentinels watch the primary; with a quorum of 2 they mark it down, and a majority of them must agree to promote a replica. App servers ask the Sentinels where the primary is and then connect to it. ZONE A Primary Sentinel ZONE B Replica Sentinel ZONE C Replica Sentinel App servers WHERE IS THE PRIMARY? THEN CONNECT

An unplanned failover can lose writes. Writes that the old primary confirmed but had not yet sent are gone. Worse, if the old primary is cut off with some app servers, it keeps taking their writes until it rejoins as a replica and throws them away. Redis’s documentation describes Redis with Sentinel as an eventually consistentEventual consistencyThe promise that replicas match once writes stop and every change can arrive. It says nothing about how long that takes or what a read returns in the meantime. Session guarantees need extra routing or freshness checks.Read more: Replication: Reading from a copy that is behind system where the last failover wins. In a 2013 test with a network partitionNetwork partitionA network failure that splits nodes into groups that cannot reach each other. During a partition, a system with copies cannot both stay linearizable and complete every request. A node sees a partition only as a timeout.Read more: Consistency: Answering during a network partition, Sentinel lost 1,126 of 1,998 confirmed writes (Kingsbury). min-replicas-to-write limits this window to about the lag setting only when the cut-off primary also loses the replicas it counts; with a replica on its side of the cut, it keeps taking writes. Nothing in Redis’s replication removes the window (more on replacing a failed leader).

Writes lost when a cut-off primary rejoins Time runs down. A network partition cuts the primary off together with app server 2. On the other side, the Sentinels promote the replica, and app server 1 writes to it. App server 2 keeps writing x = 5 to the old primary, which answers OK. When the partition heals, the old primary becomes a replica of the new one and drops x = 5. With min-replicas-to-write, the old primary would have refused writes 10 seconds after losing its replicas. APP 2 OLD PRIMARY NEW PRIMARY APP 1 PARTITION promoted SET y 1 OK SET x 5 OK rejoins as replica DROPS x = 5 FULL COPY With min-replicas-to-write 1 and no replica on its side, the old primary refuses writes after 10 s.

So keep data that must not lose a confirmed write in the database, and treat what lives only in Redis as data that can lose its last seconds. Stores that keep the Redis interface but add a replicated log exist as managed services: Amazon MemoryDB confirms a write only after it is stored in a log across several zones (AWS). A Raft-based module for Redis itself was tested in 2020 (Jepsen) and is still marked as not ready for production (RedisRaft).

A planned switch, for an upgrade, can avoid the loss when it waits for the replica to catch up. Without Sentinel, the FAILOVER command (Redis 6.2) pauses writes on the primary, waits until the replica has all of them, and only then swaps the roles (Redis), and CLUSTER FAILOVER does the same inside a cluster. With Sentinel, make the switch through Sentinel, which has no such wait: SENTINEL FAILOVER promotes a replica as if the primary had failed. Pause the app’s writes and check that a replica has caught up before you call it. Test unplanned failovers too, with the real client library, since every client must follow the Sentinels and drop connections to the old primary.

Say that Redis replication is asynchronous, so a failover can lose the last confirmed writes. Keep durable business records in the database, use Redis for copies and for data whose recent loss is acceptable, and name WAIT and min-replicas-to-write as ways to narrow the window, not to close it.

The data grows to 300 GB, and during big events the main thread of the primary stays near 100%. Replicas help only with reads; the writes and the memory need more primaries.

Redis Cluster splits the keys into 16,384 hash slots: the slot of a key is the CRC16 of the key modulo 16,384, and each primary owns a range of slots and has its own replicas (more on splitting by hash of the key). There is no proxy. A client keeps a map of slots to servers and sends each command straight to the right primary. A server that gets a key it does not own answers MOVED with the right address, and the client updates its map; while a slot moves, ASK sends a single request to the new owner. A cluster with N primaries handles about N times the load of one server at the latency of one (Redis).

MOVED and ASK redirects Time runs down. Left: the client's map is out of date and it sends GET to node A; A answers MOVED with node B, the client updates its map and sends GET to B, which returns the value. Right: slot 9152 is moving from A to B and the key has already moved; A answers ASK with node B, and the client sends ASKING and GET to B for this one request without changing its map. MOVED the slot now lives on B CLIENT NODE A NODE B GET KEY MOVED 9152 B GET KEY VALUE the client updates its map ASK the slot is moving to B CLIENT NODE A NODE B GET KEY ASK 9152 B ASKING, GET KEY VALUE the map stays as it is

Commands on several keys, transactions, and scripts work only when all their keys are in one slot. A hash tag puts keys in the same slot: only the part in braces is hashed, so {channel:42}:info and {channel:42}:chat land together and one script can update both. Use tags only for keys that change together: all keys with one tag sit on one server, and a popular tag becomes a hot spotHot shardA shard that gets far more load than the others. It runs out of capacity while other shards sit idle. Causes include a large tenant or keys written in order. One key that alone needs more than a shard is a hot key.Read more: Sharding: One tenant bigger than the rest (more on a hot key). One Redis server can hold several separate key spaces, numbered databases chosen with SELECT, but a cluster has only database 0, so an app that separates its data that way must switch to key prefixes. Valkey 9.0 added numbered databases to cluster mode (Valkey). Moving an existing app to a cluster means rewriting every multi-key command whose keys do not share a tag.

Keys, hash slots, and primaries Each key maps to one of 16,384 slots by the CRC16 of the part in braces. {channel:42}:info and {channel:42}:chat share the tag channel:42 and land in slot 3599 on primary A. {channel:7}:info goes to slot 9152 on primary B, and {channel:23}:info to slot 13448 on primary C. Primary A owns slots 0 to 5460, B 5461 to 10922, and C 10923 to 16383. KEY SLOT CRC16 MOD 16384 PRIMARY {channel:42}:info 3599 A {channel:42}:chat 3599 A {channel:7}:info 9152 B {channel:23}:info 13448 C primary A: 0–5460 primary B: 5461–10922 primary C: 10923–16383 3599 9152 13448 0 16383

The cluster runs its own failover. When a majority of primaries cannot reach a primary for cluster-node-timeout, 15 seconds by default (Redis), one of its replicas takes over, usually within a second or two after that. A primary cut off in a minority stops taking writes after the same timeout. Writes are copied asynchronously, as with Sentinel, so a failover can lose confirmed writes (Redis). If any slot has no live server, the whole cluster stops answering by default; for a cache, cluster-require-full-coverage no keeps the other slots available. The smallest useful cluster has three primaries, and the recommended start is three primaries with one replica each (Redis).

Adding a primary does not move anything by itself: an operator or a tool moves slots to it. The classic move copies the keys of a slot one by one, so a request for a key in flight is redirected, multi-key commands on that slot may fail until it ends, and a very large key can stall the move. Redis 8.4 and Valkey 9.0 move a whole slot at once, and the old owner serves the slot until the move completes (Redis, Valkey). The design scales to about 1,000 nodes (Redis); Valkey reports clusters of 2,000 nodes (Valkey).

Sharding in the client or in a proxy is the alternative: each client hashes the key to one of several independent servers. It works with any server and any client, but resharding and failover become the app’s job (more on finding the shard for a request).

Move to Redis Cluster when one primary runs out of memory or of its one core for commands. Design keys so that values used together share a hash tag, keep tags small, and plan the client library’s cluster support before the move.

During the finals, half of all viewers open one channel. Its info hash and viewer counter sit on one primary, which reaches 100% CPU while the others are almost idle.

Sharding spreads keys, not the load on one key (more on one key with too much traffic). First find the key. Redis 8.6 added HOTKEYS, which reports the most used keys, and Redis 8.2 added CLUSTER SLOT-STATS with the CPU time and network traffic of each slot (Redis, Redis). redis-cli --hotkeys works with an LFU eviction policy, and redis-cli --bigkeys finds the largest keys.

For reads, the fixes go from cheap to costly (more on a key too hot for one cache node):

  • Keep the value in each app server’s memory for a second or two. Since Redis 6, client-side caching lets the server send an invalidationInvalidationRemoving a cached copy because the original changed. The usual way is to delete the key after the write commits, so the next read loads the new value. Keep a TTL as well, since a delete can lose a race.Read more: Caching: Updating the cache when data changes when a key that a client read changes. In the default mode, the server remembers which client read which key, which costs server memory; in the broadcast mode, it keeps only the key prefixes each client subscribed to and sends an invalidation for every matching change. Use a client library that supports this: it must drop its copies when it loses the connection, and handle an invalidation that arrives before an older reply (Redis).
  • Read from replicas, or keep several copies of the key under names that hash to different slots.
Client-side caching with invalidation Time runs down. App server 1 turns on tracking and reads channel:42:info; Redis remembers that it did, and the app keeps a local copy that serves later reads without a request. App server 2 changes the title. Redis sends app server 1 an invalidation for channel:42:info, and app server 1 drops its copy; its next read goes to Redis. APP SERVER 1 REDIS APP SERVER 2 CLIENT TRACKING ON HGETALL channel:42:info VALUE remembers: app 1 read it local copy READS SERVED LOCALLY HSET … title "Semis" INVALIDATE channel:42:info copy dropped

For writes, add up increments in the app and send them once a second, or split a counter into several keys (more on one key with too much traffic). Twitter, which ran its timelines on Redis, was building a client-side cache tier for hot keys for the same reason (High Scalability, 2014).

A big key is the other kind of hot spot: a set of several million members is slow to read, to delete, to copy to a new replica, and to move to another shard, and it cannot be split across servers. Split it into buckets by a hash of the member, and delete big keys with UNLINK.

Measure which keys are hot before an event that will make them hot. Cache hot reads in the app servers with server-assisted invalidation, spread hot writes over several keys, and keep every key small enough to move.

The team now decides what to run in production: Redis or Valkey, and a managed service or its own servers. It also picks how to run Redis, and whether one Redis serves everything.

Redis and Valkey share the same core, and a client that uses the commands of Redis 7.2 works with both. They differ in the rest:

Redis and Valkey releases since 2023 Two timelines from 2023 to 2026. Redis: 7.2 under BSD in 2023, 7.4 under RSALv2 and SSPLv1 in July 2024, 8.0 with AGPLv3 added in May 2025, and 8.10 in 2026. Valkey forks from Redis 7.2.4 in March 2024 under BSD: 7.2.5 in April 2024, 8.0 in September 2024, 9.0 in October 2025, and 9.1 in May 2026. Redis 7.2 BSD 7.4 RSAL, SSPL 8.0 + AGPL 8.10 Valkey 7.2.5 BSD 8.0 9.0 9.1 FORK OF 7.2.4 2024 2025 2026
Redis Valkey
Owner and license Redis Ltd.; RSALv2 or SSPLv1 since 7.4, AGPLv3 added in 8.0 Linux Foundation; BSD
Built in Search, JSON, time series, probabilistic types, vector sets Core only; JSON, Bloom, and search as modules in a bundle
Only here INCREX, idempotent XADD, HOTKEYS, BACKUP Numbered databases in a cluster, access rules per database, clusters of 2,000 nodes
Same feature, other name DELEX DELIFEQ
Data files Redis 7.4 and later cannot be loaded by Valkey Reads files of Redis up to 7.2

When it added Valkey to ElastiCache in 2024, AWS priced it up to 20% lower than its other engines for servers and 33% lower for serverless (AWS). Pick by license and by the features the design needs, and check each feature added after the fork: some work in both, some have other names, and some exist in only one (Valkey). For a pure cache of strings, Memcached is a simpler option: it has no persistence, no replication, and no structures, but it uses all cores in one process. A managed service runs failover, patches, and backups, and keeps the limits of the data model. Check how each one confirms writes and fails over: most keep asynchronous copies, while MemoryDB writes to a durable log across zones before it confirms.

Redis itself offers three ways to run it. A single primary holds all the keys and may have replicas. When it fails, an operator promotes a replica, or, without replicas, restarts it, restores a backup, or rebuilds the data. Sentinel keeps the same shape and adds automatic failover: three or more Sentinels watch the primary and promote a replica (more on Sentinel). A cluster splits the keys over three or more primaries, each with its own replicas, and its nodes fail over by themselves, so it needs no Sentinel (more on the cluster). One deployment uses one of these ways, but a system can run several: for example, a cluster for the large cache and a primary with Sentinel for sessions and queues. Start with one primary and Sentinel, or a managed service that fails over for you, and move to a cluster when the data or the write load outgrows one server.

Three ways to run Redis Left: a single primary with two replicas; when the primary fails, an operator promotes a replica. Middle: the same primary and replicas, watched by three Sentinels that promote a replica by themselves. Right: a cluster of three shards, each a primary with its own replica and its own part of the keys; the nodes fail over by themselves, without Sentinel. Single primary replicas optional Sentinel all keys on one primary Cluster keys split over primaries Primary Replica Replica Primary Replica Replica Sentinel Sentinel Sentinel WATCH THE PRIMARY Primary Replica SHARD 1 Primary Replica SHARD 2 Primary Replica SHARD 3 Failover by hand Automatic failover Failover per shard

Run separate servers for separate roles. A cache needs allkeys-lru and little or no persistence; data kept only in Redis needs noeviction and the AOF; queues and pub/sub load the server in their own way. Separate servers keep these settings apart and limit how far one problem spreads: after its incident, Figma split its Redis usage over several clusters (Figma, 2022).

One Redis for everything and one Redis per role Left: one server holds the cache, sessions, the job queue, and pub/sub, so all of them share one eviction policy, one persistence setting, and one main thread; a problem in one role hits all. Right: four servers, each with settings for its role: the cache with allkeys-lru and no AOF, sessions and the queue with noeviction and the AOF, pub/sub with no persistence. One server shared settings A server per role settings per role cache sessions job queue pub/sub one eviction policy one persistence setting one main thread cache allkeys-lru, no AOF sessions noeviction, AOF job queue noeviction, AOF pub/sub no persistence A problem in one role hits all A problem stays in one role

Redis is built for trusted clients in a trusted network (Redis). Never expose its portPortA number that identifies a program on a machine, so one IP address can serve many programs. A connection goes to an IP address and a port, for example port 443 for HTTPS. L4 load balancers route traffic by IP address and port.Read more: Networking: Finding the server to the internet: one FLUSHALL from an attacker deletes everything. With no password set, protected mode answers only local clients. Create users with ACL rules that limit commands and key patterns per app, and turn on TLSTransport Layer SecurityThe protocol that encrypts a connection and proves the server is who it claims to be. During the TLS handshake, the client checks the server certificate and both sides agree on keys. After that, all data is encrypted. HTTPS is HTTP over TLS.Read more: Networking: Securing the connection for clients, replication, and the cluster bus when traffic crosses networks you do not trust.

A few checks keep a server healthy: no swap, transparent huge pages off, vm.overcommit_memory = 1 so that a fork does not fail on a busy server, memory with headroom for forks, a maxclients limit above the real number of connections, and alerts on memory use, evictions, replication lagReplication lagThe delay between a write on the leader and the same write on a follower. It is usually under a second but has no upper limit, and reads from a follower that is behind return old data.Read more: Replication: Reading from a copy that is behind, failed saves, and the slow log. Test failover and restores on a schedule: measure how long they take and how much data comes back.

Choose Redis or Valkey by license and needed features, and avoid commands that exist in only one when you want to keep the choice open. Run one primary with Sentinel, or a managed service, until one server is not enough, then a cluster. Give each role its own server with its own settings, keep Redis in a private network with ACL users, and test failover and restores.

  • Hot data off the database: Redis keeps all data in RAM and runs commands on one thread; a small command takes far less time than the network round trip. Use it for copies and short-lived data, and keep durable business records in the database.
  • A structure for each access: pick string, hash, list, set, sorted set, or stream by how the data is read and changed; let commands change values in the server; keep collections bounded.
  • Fitting in memory: estimate from a measured sample; small collections are compact; group tiny values into hashes or bitmaps; leave headroom for forks and buffers.
  • One slow command blocks all: no other command runs in the middle of one; avoid KEYS and full reads of big keys, use SCAN and UNLINK; I/O threads do not help a slow command.
  • Round trips: multi-key commands, then pipelines, then scripts; reuse long-lived connections.
  • Several values as one step: MULTI/EXEC without rollback; WATCH while conflicts are rare; scripts or conditional commands for “check, then write”; scripts block the server and do not roll back.
  • Expiry: set the TTL again or keep it with KEEPTTL when you replace a value; spread TTLs that end together; expiry events are not timers.
  • Memory runs out: always set maxmemory; allkeys-lru for a cache, noeviction for data, on separate servers.
  • Counting and ranking: INCR for exact counts, heartbeats in a sorted set for “online now”, HyperLogLog for distinct counts with about 1% standard error, bounded sorted sets for rankings.
  • Pushing events: Pub/Sub delivers at most once and drops slow subscribers; in a cluster, sharded Pub/Sub keeps messages in one shard when its channel model and the client fit.
  • Work that survives a worker crash: streams with consumer groups give at-least-once delivery on one server; handlers must be safe to repeat; trim only processed entries; long history or no loss on failover needs a dedicated log.
  • One worker at a time: SET NX PX with a token and a checked release is fine against duplicate work; for correctness, use database locks, or consensus with fencingFencingStopping an old leader from writing after a new one takes over. Each new leader gets a higher number, and nodes reject writes with an older one. A timeout alone does not prove the old leader stopped.Read more: Replication: Replacing a failed leader checked by the resource.
  • Search in memory: Redis Search, geo commands, and vector indexes fit data that is already in Redis and fits in RAM.
  • Disk: snapshots lose the writes since the last successful snapshot, AOF everysec usually about a second; the AOF is off by default; forks pause the main process, so measure them.
  • Copies: replication is asynchronous; WAIT and min-replicas-to-write narrow the loss window; never auto-restart a primary without persistence.
  • Failover: Sentinel needs three or more processes and a majority; an unplanned failover can lose confirmed writes, so durable business records stay in the database.
  • Many servers: Redis Cluster uses 16,384 slots and client-side routing; multi-key work needs one slot via hash tags; failover and slot moves are built in.
  • A hot key: find it with HOTKEYS and slot stats; cache reads in the app with invalidation, spread writes over several keys, keep keys small.
  • Choosing and running: Redis and Valkey share the core and differ in license and newer features; one primary with Sentinel or a managed service until one server is not enough, then a cluster; one server per role; private network, ACL users, tested failover and restores.

© 2026 Grig Alex · CC BY-SA 4.0