How Redis Actually Works
Try the interactive lab for this articleTake the quiz (6 questions · ~5 min)Redis is often described in one sentence: an in-memory key value store. That sentence is true, but it is too thin to be useful. It does not explain why a single process can serve hundreds of thousands of requests per second, why a background save can suddenly add gigabytes of memory pressure, why a hot key can turn a healthy cache into a latency problem, or why an innocent looking command like KEYS * can stall every client connected to the server.
The interesting part of Redis is that it is not a database that happens to keep a cache in RAM. Memory is the database. Persistence, replication, eviction, and failover are all side systems attached to a process whose core job is to keep a set of pointer-rich data structures consistent while moving bytes in and out of sockets with very little waiting.
That design gives Redis its speed, but it also gives it a very particular shape. Writes are cheap until the fork for BGSAVE turns later writes into copy-on-write faults. Reads are cheap until you ask for one operation that scales with the size of a million-element hash or a hundred-million-key database. Replication is simple when replicas keep up, then suddenly expensive when a backlog is too small and the server has to build another full snapshot. Operationally, Redis rewards teams that understand the shape of the internals and punishes teams that treat it like a magical black box called cache.
This article walks through the real machinery: the event loop, RESP parsing, the object model, the hash tables, expiration, eviction, persistence, replication, clustering, and the latency traps that appear when those pieces meet real workloads. The goal is not to memorise function names in server.c. The goal is to leave with a mental model strong enough that INFO memory, LATENCY LATEST, or a sudden fork spike immediately tells you what the server is likely doing.
Start with the right mental model
A good first picture of Redis is not “a remote hash map”. It is “a single process with one main execution thread, a set of socket buffers, a keyspace dictionary, and a handful of background side jobs”. Nearly everything that matters follows from that picture.
At the centre is the main thread. That thread:
- waits for sockets to become readable or writable
- parses incoming RESP frames into commands
- looks up the command implementation in a command table
- runs the command against the in-memory data structures
- appends replies to client output buffers
- updates auxiliary state such as expiration dictionaries, replication offsets, and append-only file buffers
Around that core are components that do not own normal command execution but influence its cost:
- the allocator, usually
jemalloc, which decides how memory is carved into bins and when fragmentation grows - the background child used for
BGSAVEandBGREWRITEAOF - optional I/O threads that help with socket reads and writes, while leaving command logic on the main thread
- periodic housekeeping loops that sample expirations, progress rehashing, rotate statistics, and drive time-based work
That architecture is why Redis behaves differently from PostgreSQL or MySQL. A relational database spends a lot of time coordinating locks, buffer pools, storage engines, and disk flush ordering across multiple worker threads. Redis spends most of its life in a much narrower loop: read bytes, mutate memory, queue bytes back out. The critical path is short.
It is also why people casually call Redis “single threaded” and then get confused when they hear about I/O threads, BIO threads, and forked children. The useful statement is narrower: normal command execution is serial on the main thread. That is the property that keeps the programming model simple. A command does not race another command halfway through updating a list or hash. Within that main thread, Redis still uses the kernel for multiplexed I/O and uses background helpers for jobs that can safely sit off to the side.
If you keep that picture in mind, a lot of odd behaviour stops being odd. A slow command hurts all clients because there is one main execution lane. A fork is scary because it duplicates page tables for the whole address space. Pipelines help because they reduce network round trips and syscalls without changing the serial execution model. maxmemory policies matter because memory pressure is not a side issue in Redis. It is the main operating constraint.
One event loop can serve thousands of sockets
Redis owes a large part of its throughput to an event loop built around readiness notification. On Linux that means epoll. On BSD systems it means kqueue. The pattern is the same either way: the process asks the kernel to tell it which file descriptors are ready, then it handles only those descriptors instead of blocking on one client at a time.
In simplified form the loop looks like this:
while (1) {
ready = aeApiPoll(timeout);
for (fd in ready.readable) readQueryFromClient(fd);
for (fd in ready.writable) sendReplyToClient(fd);
processTimeEvents();
}That is not the literal Redis source, but it is the right shape. The server does not spawn one thread per client. It keeps many client sockets non-blocking, and the kernel wakes it only when useful work is available.
This matters because most Redis commands are short. A GET on a hot string value is a dictionary lookup plus reply encoding. A SET is lookup, possible allocation, pointer updates, and reply encoding. If each request required a kernel thread, a context switch, a mutex schedule, and a blocking wakeup, the software overhead would start to dominate before the data structure work did.
The event loop changes the economics. One main thread can hold thousands of client connections open and pay almost no cost for idle clients. A busy Redis instance in Milan might have 20,000 connected application workers, but at any moment only a small fraction are actively reading or writing. Readiness notification means the server pays only for the active subset.
There is one complication worth being precise about. Since Redis 6, optional I/O threads can help with network reads and writes. This is often misunderstood as “Redis is multi-threaded now”. The actual design is narrower and less dramatic. I/O threads can assist with copying bytes between kernel sockets and user space buffers, but the command logic still runs on the main thread. That preserves the simple shared-nothing execution model for the keyspace while reducing some per-connection I/O cost on machines with many cores and high network fan-in.
That distinction matters operationally. Enabling I/O threads can improve throughput when the server is spending noticeable time on socket I/O, TLS framing, or client reply writes. It will not rescue a workload dominated by slow command logic such as big sorted-set ranges, huge Lua scripts, or pathological key scans. If the main thread is burning CPU inside command handlers, adding I/O threads is treating the wrong bottleneck.
The serial execution lane also explains why latency outliers can be so sharp. Suppose a team in Rotterdam uses Redis for session storage and leaderboard reads. Most commands are sub-millisecond. Then one deployment accidentally introduces HGETALL on a hash with 250,000 fields. That one command is not just slow for the caller. While it runs, the main thread is busy walking that structure and encoding the reply. Everyone else waits behind it.
That is why Redis documentation, and experienced operators, obsess over command complexity annotations like O(1), O(log N), O(N), and O(N*M). In a multi-threaded database, a slow query hurts the worker executing it plus whatever locks or I/O it contends on. In Redis, a slow command can directly stall the whole command lane.
RESP turns bytes into commands, and pipelining changes the economics
The network protocol on the wire is RESP, the Redis Serialization Protocol. RESP is intentionally simple: it is easy to parse, easy to generate, and maps neatly onto command arguments. Clients send arrays of bulk strings. The server replies with simple strings, errors, integers, bulk strings, arrays, maps, sets, and a few other structured forms in RESP3.
A SET command sent by a client might look like this:
*5
$3
SET
$8
user:142
$2
ok
$2
EX
$2
60That is one array of five bulk strings:
SETuser:142okEX60
Redis reads those bytes into a per-client query buffer, parses complete frames, and turns them into an internal argv-style vector. The next stage is command lookup. The command name is matched against a command table that records metadata such as arity, flags, key argument positions, ACL categories, and the function pointer that implements the command.
This is another place where the internals shape performance. The server is not parsing SQL or building an execution plan tree. The protocol is already close to the final function call. For simple commands the path is almost embarrassingly direct:
- read bytes from socket
- parse RESP array
- find
GETorSETin command table - validate arity and flags
- run handler
- serialize reply
The cost here is small enough that round trips and syscalls often matter as much as command logic. That is why pipelining is such a big deal in Redis. If a client sends 100 commands one by one and waits for each reply before sending the next, latency is dominated by network RTT and per-request wakeups. If the client sends all 100 in one pipeline, Redis can parse and execute them back to back in one burst.
The commands still execute serially. Pipelining does not make them parallel. It makes the transport cheaper. Instead of 100 request-response RTTs, you pay roughly one. Instead of many small syscalls, you get fewer larger buffer transfers. On a LAN inside a Frankfurt datacentre, that may only save hundreds of microseconds. Across availability zones, or from a JVM application with aggressive object churn in its network layer, it can be dramatic.
RESP simplicity also explains why output buffers matter. If a client reads slowly, Redis cannot block the main thread until the kernel empties the socket. It appends replies to that client’s output buffer and writes them when the socket becomes writable. A client that subscribes to a fast Pub/Sub stream but reads slowly can accumulate a large output buffer. At some threshold, Redis disconnects it rather than letting one misbehaving consumer grow memory without bound.
In other words, sockets are not just transport. In Redis they are part of the memory story. Query buffers, reply buffers, client lists, Pub/Sub fan-out queues, replication links, and blocked-client state all consume RAM inside the same address space as the data. A server with a modest dataset but huge client buffers can still run out of memory.
The keyspace is a hash table of objects, not a bare map
The phrase “key value store” makes Redis sound like a dictionary from strings to strings. Internally it is closer to a dictionary from string objects to type-tagged objects, with several layers of optimisation for small values, compact collections, and incremental resizing.
At the top level, the main keyspace is a hash table. Each entry maps a key to a value object. A database inside a Redis instance, such as DB 0, is essentially one dictionary plus some side dictionaries. Lookups are expected O(1) on average, with the usual hash-table caveats around load factor and collision behaviour.
The interesting bit is the value object. Redis supports strings, lists, sets, hashes, sorted sets, streams, bitmaps, HyperLogLogs, geospatial indexes, and more. It does not implement each of these with one fixed underlying structure. It uses different encodings depending on size and shape.
A few examples:
- small hashes may use compact listpack-style encodings instead of a full hash table
- small sets of integers may use
intset - larger sets graduate to a normal hash table
- lists are represented as quicklists, which are linked lists of packed listpack chunks
- sorted sets use a hash table plus skiplist combination for fast lookup and ordered range scans
That flexibility is one reason Redis is fast and memory efficient for small objects. A ten-field hash does not need the same machinery as a million-field hash. The tradeoff is that command cost can jump when an object changes encoding. A tiny set behaves differently from a very large one, even when the client API stays the same.
String values, the most common case, are also more than raw byte arrays. Redis uses SDS, Simple Dynamic Strings, which store length explicitly rather than relying on NUL termination like plain C strings. That means length retrieval is O(1), embedded zero bytes are fine, and append-heavy operations can reuse spare capacity.
A simplified mental layout for SET user:142 ok is:
keyspace dict
└── entry
├── key -> SDS("user:142")
└── val -> redisObject(type=string, encoding=embstr/raw, ptr=...)Even that picture is incomplete because the allocator adds its own size classes and metadata around the actual payload. Small values therefore carry overhead that surprises people who think “my key is 8 bytes and my value is 2 bytes, so I used 10 bytes”. In practice the key string, value object header, dictionary entry, allocator slab rounding, and possible expiration metadata mean the real footprint can be several times larger.
That is why Redis ships commands specifically for introspection:
SET user:142 ok
OBJECT ENCODING user:142
MEMORY USAGE user:142OBJECT ENCODING tells you how Redis chose to represent the object. MEMORY USAGE gives an approximate footprint including allocator effects. On workloads with millions of small keys, these details dominate capacity planning.
The hash table itself also deserves one precise point: Redis does incremental rehashing. When the table needs to grow, it does not usually stop the world and reinsert every key in one giant pause. Instead it keeps an old table and a new table, then gradually migrates buckets during later operations. That spreads the cost over time.
This is one of those internal details that explains visible behaviour. A server under write load may spend a small amount of extra work per command moving old buckets into the new table. That cost is far better than a single huge pause, but it is not zero. If a keyspace is expanding rapidly, you may see CPU time spent on the migration path even though no one issued a “rehash” command explicitly.
Expiration, eviction, and rehashing are background work with real budgets
A Redis key with a TTL is not just a key with a number attached. Redis keeps expiration metadata in a separate dictionary. That design is easy to miss, but it is important.
Why not store the TTL inline on every key? Because many keys have no expiration at all. Putting an expiry field on every entry would waste memory across the whole dataset. Redis instead stores expiry information only for volatile keys. Conceptually you have:
- the main keyspace dictionary: every key
- the expires dictionary: only keys that have TTLs, mapping to expiry timestamps
That split is elegant, but it creates work. Expired keys do not disappear by magic at the exact microsecond their TTL hits zero. Redis removes them in two ways.
Passive expiration happens when a client touches a key. If the server sees the key is logically expired, it deletes it before returning the result. This keeps the semantics correct for active traffic.
Active expiration is a background sampling process. Redis periodically looks at subsets of the expires dictionary and deletes keys that are already past their deadline. This matters because some keys may expire without ever being read again. If Redis relied only on passive expiration, dead keys could sit in memory indefinitely.
The active pass runs under time budgets, controlled partly by the server hz cadence and internal heuristics. That creates a tradeoff. Aggressive expiration scanning frees memory promptly but consumes CPU on the main thread. Conservative scanning saves CPU but allows more dead keys to remain resident for longer.
This is why workloads with enormous numbers of expiring keys can behave oddly. Imagine a notification system in Madrid writing 30 million short-lived tokens with a 90-second TTL. If expirations arrive in huge waves, Redis may spend noticeable time scanning and deleting expired keys. Memory usage can stay above the “logical” live set because expired keys are still waiting to be sampled out. Latency can wobble because the server is doing delete work in bursts.
Eviction is a related but different mechanism. Expiration says “this key is no longer logically valid after time T”. Eviction says “the server hit maxmemory, so some keys must be removed now to admit more writes or more data”.
Redis supports several policies:
noevictionallkeys-lruvolatile-lruallkeys-lfuvolatile-lfuallkeys-randomvolatile-randomvolatile-ttl
The names sound more precise than they are. For example, the LRU policies are approximations built from sampled metadata, not perfect global least-recently-used ordering. That is a good engineering trade. Perfect LRU would be too expensive on the hot path. Approximate LRU is cheap enough to use continuously.
The operational consequence is important: when Redis is near maxmemory, writes can become a little more expensive because each write may trigger sampling and eviction work first. Under heavy pressure the server is not just doing your application command. It is also hunting for memory to free.
Deleting large values can also be expensive. A giant hash or set may contain thousands or millions of nested allocations. Freeing that synchronously on the main thread can create a pause. Commands like UNLINK exist to make the key logically disappear immediately while handing the actual memory reclamation to background free workers. That does not reduce total work. It moves the slow part off the main path.
These details add up to a simple rule: in Redis, “background” does not mean “free”. Expiration cycles, approximate LRU state, rehash migration, and key freeing all exist to keep latency reasonable, but each consumes either CPU time, memory, or both.
Persistence is built around fork, append logs, and copy-on-write
Redis is in-memory first, but many deployments still need durability that survives process restarts, host failure, or operational mistakes. Redis provides two main persistence families: RDB snapshots and the append-only file, usually called AOF.
RDB snapshotting writes the dataset as a point-in-time binary image. A background save is typically triggered with BGSAVE. The key fact is that Redis does not stop serving traffic and walk the whole dataset in the main thread. It calls fork().
After fork(), there are two processes:
- the parent keeps serving live traffic
- the child gets a copy-on-write view of the current address space and serialises it to an RDB file
This is elegant because fork() makes a consistent point-in-time view cheap at the moment of the fork. The child sees memory exactly as it existed at that instant. The parent continues mutating the live dataset.
The catch is copy-on-write. Right after the fork, parent and child share the same physical pages. The moment the parent modifies a page, the kernel must copy that page so the child still sees the old contents. On a busy server this can duplicate a lot of memory.
This is the source of one of the most important Redis operational facts: background saves need headroom. A 40 GiB Redis instance in Paris that forks a child is not safe just because the child shares pages at first. If the parent dirties 8 GiB of pages during the save, total resident memory can jump sharply. If the host was already near its limit, the kernel can start reclaiming aggressively or the OOM killer can arrive.
The append-only file solves durability differently. Instead of periodically serialising the whole dataset, Redis appends mutating commands to a log. On restart, the server replays those commands to rebuild state.
The main durability modes are usually explained through appendfsync:
| Mode | What happens on write | Crash loss window | Cost profile |
|---|---|---|---|
always | fsync after each write batch | smallest | highest latency |
everysec | append immediately, fsync roughly once per second | up to about 1 second | common compromise |
no | leave flushing to the OS | larger and less bounded | lowest direct fsync cost |
The usual production choice is everysec. That means the command reply can return before the bytes are forced to stable storage, but the loss window is bounded to roughly one second under ordinary failure conditions. If you need stronger durability semantics, always exists, but it shifts more latency into the write path.
Recent Redis versions also support multi-part AOF layouts with a manifest, base file, and incremental files, which helps rewrite and startup behaviour. The high-level principle is unchanged: the AOF is a replayable command history rather than a page image.
A rewrite, triggered by BGREWRITEAOF, again uses a background child. The child does not simply copy the old AOF byte for byte. It builds a new minimal representation of the current dataset, so a long history like “increment this counter 900,000 times” can collapse into one SET of the final value. While the child is doing that, the parent buffers the new writes that happened after the fork and appends them to the new file near the end.
The persistence choices are easier to compare in practice than in abstract theory:
| Approach | What it stores | Restart speed | Fork pressure | Best for |
|---|---|---|---|---|
| RDB only | point-in-time snapshot | fast | yes | caches and datasets where some recent loss is acceptable |
AOF everysec | recent write history | slower than pure RDB | rewrite still forks | general purpose durable Redis |
| RDB + AOF | both | flexible | yes | many production deployments |
| no persistence | nothing on disk | empty restart | none | throwaway caches only |
The phrase “fork pressure” is not rhetorical. On large instances, the fork() itself can take measurable time because the kernel must duplicate page tables for a large virtual address space. Then the later parent writes can trigger copy-on-write duplication. Operators watch latest_fork_usec, used_memory_rss, and persistence timing carefully for this reason.
A background save that looks cheap in a staging environment with a 2 GiB dataset can become the dominant operational risk at 80 GiB.
Replication is driven by offsets, backlog bytes, and periodic full resync
Replication in Redis is conceptually simple: replicas need to apply the same stream of writes as the primary. The implementation is simple compared with a full relational WAL system, but it still has a few moving parts worth understanding.
Every primary maintains a replication ID and a replication offset. Think of the offset as “how many bytes of replication history have been produced so far”. As write commands arrive, the primary feeds them into the replication stream and advances the offset.
Connected replicas report what they have received and processed. If a replica disconnects briefly and comes back, the best case is a partial resynchronisation. The primary checks whether:
- the replica still recognises the same replication history ID
- the primary’s backlog buffer still contains the missing bytes since the replica’s last offset
If both are true, the primary can send only the gap. This is fast and cheap.
That backlog is a ring buffer in memory. Its size matters more than many teams realise. If network hiccups, maintenance reboots, or cross-zone jitter cause replicas to fall behind further than the backlog window, partial sync is impossible. The server must do a full resynchronisation.
Full resync is expensive because it uses the persistence machinery. The primary creates an RDB snapshot, sends that snapshot to the replica as a baseline, then streams later writes that occurred during the transfer. In other words, replication and persistence are not separate worlds. Full resync leans directly on snapshot generation.
This is one reason a too-small backlog can turn an ordinary network flap into a resource event. Suppose a shop platform in Athens has one primary and two replicas. A replica disappears for long enough that the backlog wraps. When it returns, the primary forks to create another snapshot, which adds page-table work and later copy-on-write memory pressure. If that happens repeatedly, the “real” problem is not only networking. It is replication sizing.
Replication is asynchronous by default. A successful write reply from the primary usually means “the primary accepted and processed the write”, not “every replica has it durably applied”. That is fine for many cache and queue use cases, but it matters a lot if Redis is storing data you cannot easily reconstruct.
Redis also exposes WAIT, which lets a client ask for acknowledgement from a number of replicas before proceeding. This improves the probability that a write survives primary failure, but it does not turn Redis into a consensus system. Replicas can still acknowledge before local fsync under some modes, a failover can still select a node that is behind, and asynchronous side effects still exist around the replication timeline.
The useful mental model is that Redis replication is excellent for scale-out reads, fast failover, and general high availability, but it is not a magical substitute for a quorum-replicated transaction log.
Sentinel and Cluster use this replication machinery as the substrate for failover. A promoted replica becomes the new primary because it already has most or all of the write stream. The quality of the failover therefore depends heavily on replication lag, backlog sizing, network health, and how much acknowledged data you are prepared to lose in the switchover.
Cluster mode shards the keyspace, not the command semantics
Many people first encounter Redis Cluster as “the thing that makes Redis distributed”. That description is too generous. Redis Cluster primarily gives you sharding plus failover, with some explicit limits around multi-key semantics.
The keyspace is divided into 16,384 hash slots. Each key is hashed, usually via CRC16 modulo 16,384, into one slot. Each primary node owns a subset of slots. Replicas mirror the primaries for failover.
If a client asks the wrong node for a key, the node replies with MOVED or sometimes ASK, telling the client where the slot currently lives. Smart clients maintain a slot map and route future requests directly.
That architecture scales a single logical keyspace out across multiple machines, but it comes with a contract. Many multi-key operations require all involved keys to live in the same slot. If they do not, the cluster cannot execute them atomically because there is no transparent distributed transaction engine under the hood.
Redis gives you hash tags to control this. A key like cart:{42}:items and cart:{42}:total hashes only the {42} portion, so related keys can be forced into the same slot. That is useful, but it is also a reminder that application designers must participate in the shard layout.
The failure model is equally important. During slot migration or failover, clients may receive redirects and transient errors. Writes are not globally serialised across the whole cluster. They are serialised per primary shard. A workload that needs one huge shared sorted set, one global transaction, or one cross-key script over arbitrary keys is a poor fit for cluster mode.
This is not a criticism. It is the design boundary. Redis Cluster is a pragmatic shard-and-replicate system optimised for availability and horizontal capacity. It is not Spanner, CockroachDB, or a distributed log with total ordering across the entire dataset.
Understanding that boundary prevents two classic mistakes:
- assuming cluster mode automatically makes every Redis use case scale cleanly
- assuming failover guarantees zero acknowledged-write loss in the face of asynchronous replication
Most Redis latency spikes come from four specific places
When Redis feels slow, the cause is rarely mysterious. It is usually one of a small number of mechanisms showing through.
1. Slow command complexity
The cleanest failure mode is a command that simply does too much work on the main thread.
Examples:
KEYS *over a huge databaseSMEMBERSon a giant set- large
ZRANGEorHGETALLreplies - Lua scripts or functions that loop over large collections
SORTwithout understanding input size
The symptom is a busy main thread and rising latency for unrelated clients. The fix is often to change the command shape, paginate, move work out of Redis, or redesign the data model.
2. Fork and copy-on-write
Background save and AOF rewrite can produce two distinct spikes:
- the
fork()itself takes time on a huge address space - later writes dirty pages and trigger copy-on-write copies
The first shows up as fork latency. The second shows up as RSS growth and sometimes allocator or kernel pressure. A write-heavy workload during BGSAVE can look stable in CPU metrics but still suffer because memory bandwidth and page copying are suddenly active.
3. Expiration and eviction pressure
Huge waves of expiring keys, or a server living right at maxmemory, add housekeeping cost to ordinary writes. A command that should have been a cheap SET can become “SET plus expiration work plus eviction sampling plus freeing old objects”.
This is especially visible in workloads that churn many small ephemeral keys, such as OTP codes, job leases, API rate limiter buckets, or short-lived session fragments.
4. Network and client buffer effects
Large replies, slow consumers, or heavy fan-out can make client output buffers expensive. Pub/Sub and streams can amplify this because the server may need to queue a lot of outbound bytes for clients that are not reading at the same pace the server is writing.
TLS can also move cost back toward the CPU, especially on older instances without hardware acceleration or with many small packets.
These are the cases where tools like SLOWLOG are necessary but not sufficient. SLOWLOG sees time spent executing the command logic, not time spent in network backpressure, fork side effects, or kernel scheduling. The deeper tools are the latency monitor, memory metrics, replication metrics, and the persistence section of INFO.
Commands and metrics that reveal what Redis is actually doing
The fastest way to build intuition is to tie the mental model to the server’s own telemetry. A few commands are disproportionately useful.
Memory shape
INFO memory
MEMORY STATS
MEMORY USAGE some:key
OBJECT ENCODING some:keyWhat to look for:
used_memoryversusused_memory_rss- fragmentation ratios that suggest allocator overhead or COW side effects
- unexpectedly expensive small keys
- object encodings that changed because collections grew past compact thresholds
A server with 18 GiB logical dataset and 28 GiB RSS is telling you something. Often it is fragmentation, recent fork activity, replica buffers, or client-side memory you forgot to count.
Latency sources
SLOWLOG GET 32
LATENCY LATEST
LATENCY DOCTORSLOWLOG catches expensive command execution. LATENCY LATEST and LATENCY DOCTOR can expose fork spikes, AOF fsync stalls, eviction pressure, and expire-cycle effects. When those events line up with application timeouts, the mechanism becomes much clearer.
Replication health
INFO replication
ROLEUseful fields include:
- primary or replica role
- replica offsets and lag
- backlog size and current history length
- whether replicas are online or resyncing
If replicas repeatedly fall behind the backlog window, you will often see a pattern of full resyncs and background save activity. That is not a random outage. It is the replication design telling you the history window is too short for the real network behaviour.
Persistence cost
INFO persistence
LASTSAVEWatch for:
rdb_bgsave_in_progressaof_rewrite_in_progresslatest_fork_usec- AOF delayed fsync events
These metrics answer a direct question: is the server currently paying persistence tax, and how large is it?
Command mix
INFO commandstatsThis tells you what the workload actually is, not what the application team thinks it is. Production surprises are common here. Teams swear they mainly do point lookups, then command stats show massive ZRANGE, large EVALSHA, or accidental full-collection reads.
The model worth keeping
Redis is fast because it keeps the main path short. Bytes arrive, RESP turns them into arguments, a single thread mutates in-memory structures, replies go back out, and the server avoids waiting whenever it can. That is the heart of the design.
Everything else is an attached cost or safety system.
- Persistence protects the dataset, but it introduces fork and fsync tradeoffs.
- Replication protects availability, but it introduces backlog sizing and lag tradeoffs.
- Expiration keeps volatile data honest, but it introduces sampling work.
- Eviction keeps the process alive under
maxmemory, but it adds extra work precisely when the server is already under pressure. - Compact encodings save memory, but they create thresholds where object behaviour changes.
- Clustering scales capacity, but it exposes the shard boundary to the client.
Once you see Redis in those terms, many production symptoms become ordinary engineering facts rather than mysteries. A memory spike during BGSAVE is copy-on-write. A replica full sync after a brief disconnect is a backlog that was too small. A tail-latency jump after a feature launch is often a command whose complexity no longer matches the data size. A cache node that looks lightly loaded in CPU but is still unstable may actually be fighting allocator fragmentation or huge client buffers.
That is the practical value of understanding how Redis actually works. It lets you stop reasoning from slogans like “Redis is in-memory so it is fast” or “Redis is single-threaded so it is simple”. Both are true, but neither is enough. The real behaviour comes from the specific machinery underneath: event loop, object encoding, expiry dictionaries, fork semantics, backlog windows, and the hard limit that one main command lane can only do one thing at a time.
If you respect those constraints, Redis is an unusually sharp tool. It gives you very low latency, direct data-structure operations, simple replication, and an operational model that is much easier to reason about than most heavier databases. If you ignore the constraints, Redis still tells you what is wrong. It tells you with fork spikes, backlog misses, blocked clients, memory cliffs, and commands that suddenly cost much more than they used to. The internals are close enough to the surface that the lesson is usually there if you know where to look.