Backend / Caching / Redis / 01_what_is_redis.md

What is Redis

Updated 9 interview angles 9 min read source
On this page9
  1. Single-threaded, and why that is fine
  2. The structures, and what each is actually for
  3. When Redis is the wrong answer
  4. Against Memcached
  5. Caching patterns
  6. Where it breaks in production
  7. The trade not taken
  8. Related
  9. Interview angle

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

text
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 steps

While 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:

text
DEL of a 2,000,000-member set      179 ms, server blocked
UNLINK of the same                 0.3 ms, freed on a background thread

lazyfree-lazy-user-del is no by default, so DEL frees inline; use UNLINK for anything large.

Round trips, not threads, are the usual limit

text
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/s

Pipelining 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:

python
# 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

text
hash with 512 fields     listpack     4,525 B
hash with 513 fields     hashtable   22,009 B

Small 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.

python
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 user

Two 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.

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: KEYS over a million keys stalled a concurrent PING for 249 ms; DEL of a 2M-member set blocked 179 ms where UNLINK took 0.3 ms.
    • Follow-up: “Cheapest throughput win?” - pipelining: 50,000 INCRs took 9.68 s one by one and 0.38 s pipelined.
  • “What does single-threaded cost you?” - one slow command blocks every client. KEYS on a large keyspace, FLUSHALL, or an unbounded range read is a self-inflicted outage; use SCAN and bound every range.
  • “Which data structure would you use for a leaderboard?” - a sorted set. ZADD to write, ZREVRANGE for the top N and ZREVRANK for 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: HSET rewrites 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 PX lock 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.