What is Redis
Article.
An in-memory data structure server. Not “a cache with extra features” — the data structures are the product, and choosing the right one is most of what separates a Redis answer from a memcached answer.
Redis is single-threaded, so what do I/O threads buy, and what still blocks every client? Measurements below are from Redis 8.10.2 in Docker, driven by redis-py from a second container. Persistence, memory and eviction are Persistence, eviction and memory; replication and failover are Replication, Sentinel and Cluster.
Single-threaded, and why that is fine
Commands execute one at a time on one thread, from an event loop over all
client sockets. No locks, no race between two commands, and every
individual command is atomic, which is what makes INCR a counter a hundred
workers can trust.
It is fast because it never touches disk on the read path and never pays for
coordination, not because it is parallel. I/O threads (io-threads, 1 by
default, so off) read, parse and write sockets on other threads; commands
still run serially on the main one. Background threads handle lazy freeing
(UNLINK, FLUSHALL ASYNC) and AOF fsync. Valkey pushes I/O threading
further (Valkey).
One slow command blocks everyone
1,000,000 keys; a second client sends PING in a loop
KEYS user:* 1,363 ms in total; PING p50 0.19 ms, worst 249 ms
SCAN user:* 1.6 s in total, but ~1,000 small stepsWhile KEYS ran on the server, every other client waited a quarter of a
second. SCAN does more work in total and never stalls anyone for long. The
same applies to HGETALL/SMEMBERS on huge keys, unbounded LRANGE, a long
Lua script, and deleting a big key:
DEL of a 2,000,000-member set 179 ms, server blocked
UNLINK of the same 0.3 ms, freed on a background threadlazyfree-lazy-user-del is no by default, so DEL frees inline; use
UNLINK for anything large.
Round trips, not threads, are the usual limit
50,000 INCRs, one round trip each 9.68 s
the same, pipelined 0.38 s
redis-benchmark SET, 50 clients 66,689 req/s
the same with -P 16 (pipelined) 731,529 req/sPipelining sends many commands per round trip; it gave 25× from Python and
11× in the benchmark, far more than I/O threads would. Batch with pipelines,
MGET/MSET, or a Lua script before reaching for threads or a cluster.
The structures, and what each is actually for
| Type | Reach for it when |
|---|---|
| String | a cached blob, a counter, a flag |
| Hash | an object you update field by field |
| List | a queue or a capped recent-items feed |
| Set | membership and set algebra |
| Sorted set | anything ranked, or a time-ordered window |
| Bitmap | dense boolean per id, at scale |
| HyperLogLog | approximate cardinality, fixed 12 KB |
| Stream | an append-only log with consumer groups |
| JSON, search, vectors | built in since Redis 8.0 |
The two that win interviews are the sorted set and HyperLogLog, because they replace work people otherwise do in application code:
# Leaderboard: ranked reads without sorting anything yourself.
r.zadd("scores", {"ada": 99, "bob": 71})
r.zrevrange("scores", 0, 9, withscores=True) # top 10
r.zrevrank("scores", "bob") # their position
# Unique visitors per day, 12 KB regardless of volume.
r.pfadd("visitors:2026-08-18", *user_ids)
# ~0.81% error
r.pfcount("visitors:2026-08-18")Redis 8.0 (May 2025) moved JSON documents, the Query Engine (secondary indexes and full-text search), TimeSeries, probabilistic types and Vector Sets from separate modules into Redis itself. Valkey ships JSON and search as modules instead, so the choice between them now depends on features as well as licence.
Encodings: small collections are compact, until they are not
hash with 512 fields listpack 4,525 B
hash with 513 fields hashtable 22,009 BSmall hashes, sets and sorted sets are stored as one packed array; past a
threshold (hash-max-listpack-entries, 512 on this server; 128 for sorted
sets) they convert to a real hash table and take almost five times the
memory. Millions of small hashes kept under the threshold are one of Redis’s
cheapest storage patterns; one field too many per key multiplies the bill.
A hash beats a string per field when you update one attribute of an object:
HSET user:1 name Ada rewrites one field, where a JSON blob in a string means
read, parse, mutate, serialise, write — and loses a concurrent update.
When Redis is the wrong answer
- As the system of record. It is memory-first; persistence is a recovery aid, not a durability guarantee, and a failover can lose recent writes.
- For a queue you cannot lose. Lists are a queue until a consumer crashes mid-item. Streams with consumer groups and acknowledgement are the honest version — Pub/sub and Streams.
- For a working set larger than RAM. The cost curve is memory, and the answer is usually a smaller working set, not a bigger instance.
- For querying by value. No secondary indexes in core Redis; you either maintain the index yourself or you wanted a database.
Against Memcached
| Redis | Memcached | |
|---|---|---|
| Data types | eight-plus | strings only |
| Persistence | RDB / AOF | none |
| Replication | yes | no |
| Scripting | Lua, functions | no |
| Multi-threaded | I/O only, off by default | fully |
| Memory per key | higher | lower |
Memcached is still marginally better at exactly one job — a large, purely volatile, string-only cache on a many-core box. For anything else Redis wins on capability.
The licence question matters more in 2026 than Memcached does. Redis moved to source-available licences (RSALv2/SSPLv1) in March 2024, the Linux Foundation forked the last open version as Valkey, and Redis 8.0 (May 2025) added AGPLv3 as a third option. “Redis is not open source” is therefore out of date; the real questions are whether AGPL is acceptable for your use and whether you need Redis 8’s built-in JSON, search and vector types.
Caching patterns
| Pattern | Who writes the cache |
|---|---|
| Cache-aside | the application, on a miss |
| Write-through | the cache, synchronously with the DB |
| Write-behind | the cache, asynchronously — risks loss |
| Refresh-ahead | a background job, before expiry |
Cache-aside is the default and the one to describe, because it degrades correctly: a cache outage becomes a slow application rather than a broken one, if the database can absorb a 100% miss rate. If it was sized for the 1% that missed, the outage takes the database down too; that defence is in Cache Stampede and Mitigation Patterns.
def get_user(uid: int) -> User:
if (hit := r.get(f"user:{uid}")):
return User.model_validate_json(hit)
user = db.get(uid)
r.setex(f"user:{uid}", 300, user.model_dump_json())
return userTwo things that must be there: a TTL on every key — an entry with no expiry is a leak waiting for a deploy — and jitter on that TTL, or everything cached in the same second expires in the same second. That failure has its own note: Cache Stampede and Mitigation Patterns.
Where it breaks in production
KEYS in a maintenance script
Someone runs KEYS session:* against production to count sessions. Every
client stalls for the length of the scan, timeouts cascade through the
services using Redis, and the incident looks like a network problem. Rename or
disable KEYS with ACLs, and use SCAN.
A lock that two clients hold
SET lock NX PX 30000 on a primary with an asynchronous replica: the primary
acknowledges, dies before replicating, the replica is promoted without the
key, and a second client acquires the same lock. Use the lock only for
efficiency, never correctness, or pair it with a fencing token checked by the
resource (Redlock and the Kleppmann Critique).
Memory doubling at fork
BGSAVE and AOF rewrites fork the process; pages written during the save are
copied, so a write-heavy instance can briefly need up to twice its memory.
Without headroom (and with vm.overcommit_memory not set to 1), the fork
fails or the OOM killer takes the instance
(Persistence, eviction and memory).
Big keys and hot keys
One sorted set with ten million members, or one key every request reads:
the first blocks on every full read and delete, the second pins one CPU and,
in a cluster, one shard. redis-cli --bigkeys and --hotkeys find them;
split them by id.
The trade not taken
Redis’s design, one thread executing every command against memory, buys atomic commands and predictable latency at the price of one CPU per instance for execution and a dataset bounded by RAM. Scaling past one core means sharding (Redis Cluster) rather than threads; staying under RAM means TTLs and eviction.
A multi-threaded store (Dragonfly, or KeyDB) uses more cores per instance but gives up some of the simplicity that makes every Redis command atomic for free.
Related
Interview angle 9
- “Why is Redis fast?” - in-memory with no disk on the read path, and single-threaded command execution so there is no locking or coordination. Not because it is parallel: I/O threads (off by default) only parse and write sockets.
- Follow-up: “What still blocks every client?” - any slow command:
KEYSover a million keys stalled a concurrentPINGfor 249 ms;DELof a 2M-member set blocked 179 ms whereUNLINKtook 0.3 ms. - Follow-up: “Cheapest throughput win?” - pipelining: 50,000 INCRs took 9.68 s one by one and 0.38 s pipelined.
- Follow-up: “What still blocks every client?” - any slow command:
- “What does single-threaded cost you?” - one slow command blocks every client.
KEYSon a large keyspace,FLUSHALL, or an unbounded range read is a self-inflicted outage; useSCANand bound every range. - “Which data structure would you use for a leaderboard?” - a sorted set.
ZADDto write,ZREVRANGEfor the top N andZREVRANKfor one user’s position, all without sorting anything in application code. - “How would you count unique visitors at scale?” - HyperLogLog. Fixed 12 KB per key regardless of cardinality, roughly 0.81% error. A set would be exact and unboundedly large.
- “String or hash for an object?” - a hash when you update fields independently:
HSETrewrites one field, where a JSON string means read-modify-write and loses concurrent updates. A string is fine for a blob you always replace whole. - “Which caching pattern and why?” - cache-aside, because it degrades to the database: if Redis is down the application is slow rather than broken, provided the database can take the full read load or misses are shed. Every key gets a TTL with jitter, or a synchronised expiry stampedes the database.
- “Your
SET NX PXlock runs on a primary with a replica. What breaks on failover?” - replication is asynchronous, so the promoted replica may not have the lock and a second client acquires it. Use a fencing token checked by the protected resource. - “Redis or Valkey in 2026?” - “Redis is not open source” is stale since 8.0 added AGPLv3. Choose Redis 8 for its built-in JSON, search and vector types if AGPL is acceptable; Valkey for a BSD licence and a community-governed project.
- “When would you not use Redis?” - as a system of record, for a queue that cannot lose messages (use Streams with acknowledgement), when the working set exceeds RAM, or when you need to query by value.