What is Redis
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.
Persistence, memory and eviction are Persistence, eviction and memory; replication and failover are Replication, Sentinel and Cluster.
Single-threaded, and why that is fine
Command execution is single-threaded. No locks, no race between two commands,
and every individual command is atomic — which is what makes INCR a
counter you can trust from a hundred workers.
It is fast because it never touches disk on the read path and never pays for coordination, not because it is parallel. Redis 6+ threads I/O (and Valkey threads more of it — see Valkey), but the command loop itself stays serial.
The consequence you must remember: one slow command blocks everything.
r.keys("user:*") # O(N) over the keyspace. Never.
r.scan_iter("user:*") # cursor-based, bounded work per callKEYS, FLUSHALL, a big SMEMBERS, an unbounded LRANGE — each stalls every
other client for its duration. This is the most common self-inflicted Redis
outage.
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 |
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")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 | 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, and the licence question (Redis 8 versus Valkey) matters more in 2026 than the Memcached comparison does.
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.
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.
Related
Interview angle 7
- “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 — Redis 6+ threads only I/O, and the command loop stays serial.
- “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 correctly: if Redis is down the application is slow rather than broken. Every key gets a TTL with jitter, or a synchronised expiry stampedes the database.
- “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.