Redis usage among developers climbed from roughly 20 percent to 28 percent between the 2024 and 2025 Stack Overflow Developer Survey, making it the fifth most-used database overall and the fastest riser inside the top five (Stack Overflow, 2025). That's not a small move for a database category that's been mature for over a decade. It shows up in interviews too: more teams now assume you've touched Redis before you walk in, so the questions skip past "what is a key-value store" and go straight to the parts that actually break in production.
One opinion here, and it might not hold up: I think Cluster mode gets more prep time than it deserves, and eviction policy gets less. Most teams running Redis in 2026 are a single primary with one or two replicas, not a sharded cluster, and a wrong maxmemory-policy has quietly caused more production incidents I've come across than a bad hash slot migration ever has.
This page covers Redis interview questions across eight areas: why the single-threaded design is fast instead of slow, the core data types and when each one earns its keep, RDB versus AOF persistence, key expiration and eviction policies, pub/sub versus Streams alongside transactions and Lua scripting, pipelining with replication and Sentinel, Redis Cluster, and the caching patterns, plus the cases where Redis is the wrong call. Every command example runs through redis-cli, the tool most people actually reach for when debugging a Redis problem live.
What Redis actually is, and why single-threaded makes it fast
Every loop starts here, warm-up or not. A shaky answer on why Redis is fast sets a bad tone before the real questions show up.
Easy questions
15Redis is an in-memory data structure store used as a cache, a database, a message broker, and a few other things depending on which data type you reach for. Everything lives in RAM by default, so reads and writes come back in well under a millisecond, and the trade-off is that your dataset has to fit in the memory budget you're willing to pay for, not on cheaper disk.
Postgres and MySQL are built around disk-backed tables, complex joins, and strict transactional guarantees on the full dataset. Redis is built around a fixed set of data structures and simple, fast operations on top of them. Most production setups run both, not one instead of the other, Redis sitting in front of Postgres as a cache rather than replacing it.
Strings hold text, numbers, or binary blobs up to 512MB and cover simple key-value lookups plus atomic counters (INCR, DECR). Lists are ordered and push or pop from either end, a natural fit for a queue or a recent-activity feed. Sets hold unique members with no order at all, useful for tag membership or "has this user done X" checks.
Sorted Sets add a floating-point score to each member and keep everything ordered by that score, the structure behind leaderboards and rate limiters. Hashes store field-value pairs under one key, so a user record becomes one Redis key instead of five separate String keys. Streams, added in Redis 5.0, are an append-only log with per-entry IDs and consumer groups, closer to a lightweight Kafka than to any of the other five types.
Two mechanisms run at the same time. Passive expiration checks a key's TTL the moment something touches it, a GET, an EXISTS, whatever, and deletes it right then if it's expired before returning anything, so an expired key never gets served back to a client. Active expiration doesn't wait for a client to ask: a background cycle runs roughly 10 times a second, sampling 20 random keys carrying a TTL from each database, deleting whatever's expired, and immediately repeating the sample if more than a quarter of those 20 turned out to be expired.
That combination means a key that's never touched again after expiring still gets cleaned up eventually. It doesn't sit there forever waiting for someone to ask for it.
A client subscribes to a channel, another client publishes a message to that channel name, and Redis fans that message out to every currently-subscribed client, live, in memory, with no persistence involved anywhere in the path. If zero clients are subscribed when the publish runs, the message just disappears.
There's no queue, no buffer, no catching up on what you missed. Pub/Sub is fire-and-forget by design, closer to a live radio broadcast than a mailbox.
Pipelining batches several commands into one network write and reads all the responses back in one pass, instead of paying a full round trip for every single command. On a connection with 1ms of round-trip latency, sending 100 commands one at a time costs roughly 100ms just in network waiting; pipelined, that same 100 commands can finish in a few milliseconds because the network wait only happens once.
It's not a transaction. Pipelining says nothing about atomicity or isolation, another client's commands can still interleave between yours on the server, it's purely a client-side batching trick to cut round trips. Wanting both isolation and the round-trip savings means wrapping the batch in MULTI/EXEC and pipelining that.
Cache-aside puts the application in charge: on a read, check Redis first, and on a miss, read from the source of truth, usually a relational database, then write that value into Redis before returning it, so the next read for the same key hits cache. Writes go straight to the database, and the app deletes or updates the corresponding cache key so it doesn't serve stale data.
It's the default because it's the least invasive option. The database schema and write path don't change at all, Redis sits entirely on the read side, and a Redis outage degrades performance, every read falls through to the database, instead of breaking writes.
Redis Lists are ordered collections of strings, implemented internally as a linked structure (quicklist encoding once they get large). You push onto either end with LPUSH or RPUSH and pop with LPOP or RPOP, so they naturally support FIFO or LIFO order, and push/pop at the ends run in constant time.
That makes them a natural fit for simple job queues: a producer does LPUSH job onto a list, and workers call BRPOP, a blocking pop, to wait for the next item without hot-looping on empty checks. The tradeoff is that a List gives you at-most-once delivery. If a worker pops a job and crashes before finishing it, that job is just gone, which is why anything needing reliable processing tends to move to Streams with consumer groups instead.
INCR reads the current value, adds one, and writes it back, all inside a single command Redis runs without interleaving any other client's command in the middle, since Redis processes commands one at a time. Two clients calling INCR page:views at the same instant can't both read 41 and both write 42, you're guaranteed to land on 43.
If you tried the same thing in application code with a GET followed by a SET, you'd have a classic race condition: two clients read the same value, both add one, and one increment silently disappears. INCR, DECR, INCRBY, and HINCRBY push that read-modify-write step into the server itself, so the atomicity guarantee comes from Redis, not from locking you'd otherwise have to write yourself.
EXPIRE sets a TTL on an existing key, telling Redis to delete it automatically after some number of seconds (PEXPIRE does the same in milliseconds). Once set, TTL keyname tells you how many seconds remain, -1 if the key exists with no expiration, or -2 if the key doesn't exist at all.
PERSIST does the opposite: it strips whatever TTL is attached to a key so it lives forever, without touching the value. A common gotcha is that a plain SET wipes out an existing TTL as a side effect, so updating a value while keeping its expiration needs SET with KEEPTTL, or a fresh EXPIRE call afterward.
Both remove a key, but DEL reclaims the memory synchronously, on the main thread, before it returns. If that key holds a huge value, say a hash with millions of fields, the free() call can take a real chunk of time and block every other connected client during that window.
UNLINK unlinks the key from the keyspace immediately and hands the actual deallocation to a background thread, so the command returns fast without stalling anyone else. For small keys the difference is negligible, but once you're deleting large collections in production, UNLINK is the safer default.
KEYS * walks the entire keyspace in one shot, and because Redis is single-threaded, that whole scan runs to completion before Redis can serve anything else. On a database with a few hundred keys that's instant. On one with tens of millions, it can block every other client for seconds, which in a live system means timeouts cascading through whatever's calling Redis.
SCAN fixes this by walking the keyspace incrementally: you call it with a cursor starting at 0, it returns a small batch plus a new cursor, and you keep calling with that cursor until it comes back as 0 again. It's not perfectly consistent (a key added or removed mid-scan might or might not show up), but it never blocks the server for more than the time it takes to return one small batch.
A single Redis instance has 16 logical databases by default, numbered 0 through 15, and you switch between them with SELECT n. They're just namespaces inside the same process, sharing the same memory, the same persistence file, and the same single thread, so it's not isolation and definitely not sharding.
In practice most teams treat this as a footgun more than a feature. Connection pooling libraries and Redis Cluster don't support SELECT at all, so if you use multiple DBs to separate, say, your cache from your session store, you'll hit a wall the moment you move to Cluster. The more common pattern is keeping everything in DB 0 and separating concerns with key prefixes, like cache: and session:.
GEOADD stores a set of locations, each with a longitude, latitude, and name, and beneath that it's actually a Sorted Set where the score is a geohash packed into a 52-bit integer. GEODIST gives you the distance between two members, and GEOSEARCH, the modern replacement for the older GEORADIUS, lets you ask for everything within a given radius or bounding box of a point.
Because it's built on a Sorted Set, you still get every normal Sorted Set operation on the same key, ZRANGE and ZSCORE both work fine against it. It's a solid fit for "find nearby drivers" or "find nearby stores" features when you don't want a dedicated geospatial database just for that.
SET key value NX only sets the key if it doesn't already exist, short for "not exists." If the key is already there, the command does nothing and returns nil instead of overwriting it, giving you an atomic set-if-absent check in one round trip, instead of a GET then a SET in your app, which would race between the check and the write.
It's the building block behind the simplest form of a distributed lock. SET lock:order:123 workerA NX EX 10 either grabs the lock and sets a 10 second safety expiration, or fails instantly if someone already holds it. It's also handy for things like "only send this welcome email once," where you SET NX a marker key before sending.
Redis listens on port 6379 by default. The quickest sanity check is redis-cli ping, which opens a connection and expects PONG back if the server is alive and accepting connections.
From there, redis-cli -h host -p port drops you into an interactive shell where you can run any command directly, and redis-cli info gives you a quick health and config dump without needing a monitoring stack. In production you'd also want auth configured, requirepass or, better, Redis 6+ ACLs, since Redis has no authentication and no external network binding by default.
redis-cli -h 127.0.0.1 -p 6379 ping
# PONGMedium questions
25The core event loop that actually runs your commands is single-threaded, which means two commands never race on the same piece of data at the same time. That single fact is why operations like INCR or HSET don't need locks, mutexes, or compare-and-swap logic anywhere in your app code, Redis just handles one command at a time and the next one waits its turn.
It sounds like it should be slow. It isn't, because Redis commands are almost all O(1) or O(log N), memory access is fast, and there's no disk I/O or lock contention eating time the way a multi-threaded database would spend coordinating. A single instance regularly pushes past 100,000 operations a second on ordinary hardware precisely because it isn't paying the coordination tax multi-threading requires.
Redis Ltd. moved Redis from the permissive BSD-3 license to source-available RSALv2/SSPLv1 starting with Redis 7.4 in March 2024, restricting commercial-hosting redistribution rather than restricting normal application use. The Linux Foundation, backed by AWS, Google Cloud, and Oracle, responded by forking the last BSD-licensed version into Valkey the following month. Redis Ltd. reversed course in May 2025 and shipped Redis 8.0 with AGPLv3 added back as a license option (Redis, 2025).
For almost anyone just running Redis inside their own app, none of this changes anything day to day, you were never redistributing Redis as a hosted service in the first place. The part worth knowing for an interview: know that Valkey exists, know roughly why it exists, and know that the two projects stay close enough that most client libraries and commands work against either one interchangeably. I wouldn't be surprised if an interviewer asks this exact question just to see if you've kept up.
A Set answers "is this thing a member" and nothing else, membership checks and set math (SINTER, SUNION, SDIFF) are its whole job, with no ordering, not even insertion order. A Sorted Set adds a score to every member and keeps the entire structure ordered by that score at all times, backed internally by a skip list plus a hash table, so writes are O(log N) and reading a slice of the ordering doesn't require sorting on the fly.
You need the sorted version the moment ranking matters: a leaderboard where scores change constantly and you need "top 10 right now," a sliding-window rate limiter keyed by timestamp, or a delayed-job queue where the score is a due time and a worker pulls whatever's due next.
Memory and atomicity, mostly. Storing a user as three separate String keys, one for name, one for email, one for age, means three round trips to read the full record and three times the per-key overhead Redis charges for storing a key at all. A Hash collapses that into one key with three fields, and one HGETALL reads the whole thing. Small hashes, under the configured listpack threshold, also get stored in a compact encoding that uses noticeably less memory than the equivalent String keys would.
The other real win: HINCRBY on one field of a Hash is atomic the same way INCR is on a String, so you can bump a view counter that lives alongside a dozen other fields without any extra locking.
Bitmaps aren't a separate data type, they're bit-level operations (SETBIT, GETBIT, BITCOUNT, BITOP) that run against a plain String. Tracking "did user N show up today" as one bit per user in a single key uses a fraction of the memory a Set of user IDs would, a million daily-active flags fit in about 125KB as a bitmap against megabytes as a Set of string IDs.
HyperLogLog solves a different problem, counting unique things rather than storing which things, using PFADD and PFCOUNT to estimate cardinality within roughly 0.81 percent standard error while capping memory at about 12KB no matter whether you've added a thousand items or a hundred million. I don't reach for either one often. When I do, it's almost always "how many unique visitors hit this page today" or "which of these million users were active in the last 24 hours," where a Set would cost 50 to 100 times the memory for the same answer.
RDB takes a full point-in-time snapshot of the dataset and writes it to a single binary file at intervals you configure, fast to load on restart because it's reading one compact file back into memory. Anything written after the last snapshot is gone if the process dies before the next one fires.
AOF instead logs every write command as it happens and replays that log on startup to rebuild the dataset, which means far less data loss, down to about one second with the default fsync policy, at the cost of a larger file and a slower restart, since replaying thousands of individual commands takes longer than loading one snapshot (Redis Docs, Persistence). Redis has supported running both together since version 4.0, and for most production setups that's the actual answer: AOF for durability during normal operation, RDB snapshots for fast backups and restores.
BGSAVE forks a child process, and that fork shares the parent's memory pages through copy-on-write instead of duplicating gigabytes of RAM upfront. The child walks its private, frozen view of memory and writes it to disk while the parent keeps serving reads and writes normally. Only pages the parent actually modifies during the dump get copied, one page at a time, so the memory overhead scales with how much write traffic happens during the snapshot, not with total dataset size.
On a mostly-read workload that overhead is tiny. On a write-heavy instance mid-snapshot, that copy-on-write memory can spike hard enough to be the actual reason an instance runs out of memory, not the dataset growing on its own.
Once memory usage crosses maxmemory, every new write triggers Redis to first free up space according to whatever maxmemory-policy is configured. noeviction, the default, just rejects writes with an error and leaves reads working, the safest choice if you're using Redis as a database and would rather fail loudly than lose data silently.
The rest all evict something: allkeys-lru and allkeys-lfu can remove any key in the dataset, using recency or an approximated access-frequency counter; volatile-lru and volatile-lfu only touch keys that have a TTL set, leaving keys without an expiry untouchable; volatile-ttl evicts whichever key is closest to expiring anyway (Redis Docs, Key Eviction). Picking an allkeys policy when your dataset is a mix of cache data and data you actually need to keep is the single most common eviction misconfiguration I've come across.
Streams solve the durability problem Pub/Sub doesn't. Every write appends to a persisted, ordered log with a unique ID, consumer groups track exactly which entries each consumer has acknowledged, and a consumer that disconnects for ten minutes can reconnect and read everything it missed instead of losing it. That durability isn't free, Streams cost more memory and more latency per message than a raw publish.
My take: Pub/Sub is the right call for something like a live cache-invalidation signal, where missing a message occasionally is fine because the next write just re-triggers it. Streams are the right call the moment losing a message is an actual problem, an order-processing pipeline, a job queue, anything downstream that treats every entry as something that has to be handled exactly once.
Everything queued between MULTI and EXEC runs back-to-back with no other client's commands interleaved in the middle, which gives you isolation, not atomicity in the SQL sense. If one queued command fails at runtime, a wrong-type error because a key held the wrong type, say, Redis still executes every other command in the queue. There's no rollback of the commands that already ran.
The only case EXEC aborts the whole transaction outright is a syntax error caught while queuing, before EXEC ever runs. Candidates who answer "it's like a SQL transaction" without that caveat are working from the wrong mental model entirely, and it shows the moment a follow-up question probes a runtime failure instead of a syntax one.
A Lua script runs entirely inside the single-threaded event loop as one atomic unit, no other command executes in the middle of it, which is a stronger guarantee than MULTI/EXEC provides and cuts out every network round trip between the individual commands, since the whole read-then-decide-then-write logic happens server-side in one call.
The classic example is a rate limiter: check a counter, compare it against a limit, increment it, and set an expiry, all as one script instead of four separate round trips where another client could sneak a request in between your check and your increment. Redis 7 added Functions as a more manageable way to register and version these scripts server-side instead of shipping raw Lua strings from application code, but plain EVAL is still what most existing codebases run today.
Asynchronous by default. A write returns success to the client the moment the primary applies it locally, and the replica gets that write streamed to it afterward, on its own schedule, not as part of the same operation. If the primary crashes in the gap between "wrote locally" and "replica received it," that write is gone from the replica's point of view, even though the original client already got an OK back.
The WAIT command can make a specific write block until N replicas confirm they've received it, which narrows that window, but it's still not the same guarantee as a synchronous multi-node commit. It just tells you how many replicas had it at a point in time, and a replica can still fail right after acknowledging.
Every key gets hashed with CRC16 and mapped into one of 16,384 fixed hash slots, and each slot is assigned to exactly one primary node, plus that node's replicas, when the cluster is set up. A client asking a node for a key that lands in a slot the node doesn't own gets a redirect pointing at the node that does, rather than the cluster silently proxying the request on your behalf, there's no central router sitting in front.
Rebalancing the cluster means migrating slots, not individual keys directly, moving ownership of a slot range from one node to another while the keys inside it get copied across in the background.
Write-through writes to Redis and the database in the same request, synchronously, so the cache is never stale relative to the database and a cache read is always safe to trust. Write-behind, also called write-back, writes to Redis immediately and returns success to the caller, then flushes that write to the database asynchronously on a delay or in a batch.
The upside is lower write latency for the caller, since the database round trip isn't on the critical path. The real cost: if the Redis node holding that unflushed write dies before the batch job runs, the write is gone, it never landed in the database at all. That makes write-behind a bad fit for anything where losing an acknowledged write is unacceptable, and a reasonable fit for something like high-volume analytics counters where losing the occasional increment doesn't matter.
When the dataset genuinely doesn't fit in memory at a cost you're willing to pay. Redis charges you RAM for every byte stored, and RAM is meaningfully more expensive per gigabyte than disk, so a multi-terabyte dataset that needs to stay fully queryable is usually a worse fit than a disk-backed database with a smaller Redis cache layered in front of the hot subset.
Same goes for anything that genuinely needs complex relational queries, multi-table joins, ad hoc filtering across many columns, full-text search across large documents. Redis's data structures are fast specifically because they're simple, and forcing relational-style querying onto them usually means fighting the tool instead of using it. My honest opinion: a decent share of "let's add Redis" tickets I've seen are really "we're missing an index" tickets in disguise, and Redis just papers over the slow query instead of fixing it.
A consumer group is a named cursor over a stream shared by multiple consumers. Each consumer reads with XREADGROUP GROUP mygroup consumer1, which hands out entries and marks them as delivered-but-unacknowledged in a pending entries list, or PEL. That PEL is the key difference from a plain List.
If consumer1 crashes after reading a message but before finishing it, the message doesn't just disappear, it sits in the PEL tied to that consumer's name. Another process can call XPENDING to see what's stuck, then XCLAIM it to reassign ownership to a healthy consumer, and only once that consumer calls XACK does the entry actually leave the pending list. That's at-least-once delivery with explicit acknowledgment, exactly what a job queue or event log needs, and something BRPOP on a List can't give you since a popped item is gone the instant it's popped whether or not the worker finishes.
Left alone, yes, an append-only file keeps growing since it logs every write command that ever touched the dataset. Redis handles that with BGREWRITEAOF, which forks a child process the same way BGSAVE does, and that child writes a brand new, compact AOF by walking the current in-memory dataset and generating the minimal set of commands needed to recreate it, rather than replaying the whole write history.
So if you SET a key ten thousand times, the rewritten file just has the one command reflecting its final value. While the child writes that new file, the parent keeps appending live writes to a rewrite buffer, and once the child finishes, those buffered writes get flushed onto the new file before it atomically replaces the old one. Redis also triggers this automatically based on auto-aof-rewrite-percentage, kicking off a rewrite once the file has grown by some percentage since the last one, so in a well configured system you rarely call BGREWRITEAOF by hand.
Redis picks a different internal representation for the same logical type depending on its size, purely to save memory, and OBJECT ENCODING keyname shows which one you're getting. A small Hash, List, or Sorted Set gets stored as a listpack, a compact block of memory with no pointers, cheap to allocate and cache-friendly.
Once that collection crosses a size threshold, like hash-max-listpack-entries (default 128 fields) or hash-max-listpack-value (default 64 bytes per value), Redis silently converts it to a full hash table, which uses more memory per entry but gives O(1) lookups instead of the O(n) scan a listpack needs. If your data model keeps hashes just under those thresholds by convention, you're paying for hash table overhead you didn't need, and conversely, one outlier record that blows past the threshold jumps to a much larger footprint with no warning, it just happens silently.
SLOWLOG GET is the first stop. It keeps a ring buffer of the last N commands that took longer than slowlog-log-slower-than microseconds (10ms by default), along with the command, when it ran, and how long it took. That tells you if the problem is app-side, someone running SMEMBERS on a set with two million members, or a KEYS * from a script someone forgot was still scheduled.
Alongside that, LATENCY HISTORY and LATENCY LATEST break down where time is going at the event level, fork time from a BGSAVE, command execution, expire cycles. That matters because a slow command in SLOWLOG and a fork-related latency spike look identical from the client's point of view but need completely different fixes. If neither shows anything obvious, a live rolling latency view correlated against deploys and traffic spikes, plus a careful MONITOR session used sparingly given its overhead, usually catches whatever's left.
A big key is any single key whose value is disproportionately large, a Hash with five million fields, a Sorted Set with a million members, or a String holding a 200MB blob. The danger isn't really the memory, it's that a lot of the commands you'd run against that key, HGETALL, SMEMBERS, even DEL, are O(n) on the number of elements and run on Redis's single command-processing thread, so one oversized key can stall every other client for the duration of that command.
redis-cli --bigkeys does a full keyspace scan and reports the largest key it finds per data type, a decent first pass on a smaller dataset, though on a huge keyspace it's itself an expensive scan you'd want to run against a replica rather than the primary. A related but separate problem is a "hot key," one key getting hammered with reads or writes far more than anything else, which in a clustered setup can overload a single node even though the cluster as a whole has plenty of headroom, since Redis Cluster shards by key, not by request volume, and there's no way to split load for one key across nodes.
Keyspace notifications turn Redis's internal write operations into Pub/Sub events, once you enable them with CONFIG SET notify-keyspace-events, off by default since publishing an event for every write has real overhead. You configure which categories you want, KEA for everything, or Ex for just expired-key events, then subscribe to channels like __keyevent@0__:expired to get notified the moment a specific key expires.
A common use is cache invalidation across services: if service A deletes or updates a cached object, other services subscribed to that keyspace channel can react immediately, evicting their own local caches, instead of relying on a fixed TTL and serving stale data until it lapses. The tradeoffs are that notifications are fire-and-forget same as regular Pub/Sub, so a disconnected subscriber just misses the event, and you're adding load to Redis for every qualifying operation, so you'd scope notify-keyspace-events narrowly rather than turning on every category.
The naive version does INCR request_count:user123, checks whether the result is over the limit, and if it's the first request, calls EXPIRE request_count:user123 60 so the counter resets every minute. The bug sits between those two commands: if the process crashes or a network blip happens right there, that EXPIRE never runs, and now you've got a counter key with no TTL that increments forever and permanently locks that user out.
The clean fix pushes both operations into one atomic unit, a small Lua script that does the INCR and conditionally sets the expiration only when the count equals 1. EVAL guarantees the whole script runs as a single atomic step with no other client's commands interleaving in the middle, which is exactly the guarantee correctness needs here.
local count = redis.call('INCR', KEYS[1])
if count == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return countIntroduced with RESP3 and the tracking feature in Redis 6, client-side caching lets a client keep its own local copy of values it recently read, and have Redis proactively tell it when one of those keys changes on the server, so it knows to invalidate its local copy instead of serving stale data indefinitely. Without this, caching client-side yourself means either polling to check freshness, which defeats a lot of the point, or accepting some staleness window and hoping it's short enough.
With tracking enabled, Redis maintains a table mapping keys to the clients that have read them, and pushes an invalidation message over the same connection the moment one of those keys is written, using out-of-band push messages that RESP3 supports but RESP2 doesn't. The tradeoff is memory and bookkeeping on the Redis side, since it has to track who read what, so it's usually scoped with BCAST mode and prefix filters rather than tracking every key any client happens to touch.
Before Redis 6, requirepass gave you exactly one password shared by every client, meaning any process that could authenticate could run FLUSHALL just as easily as GET. ACLs let you create named users, each with their own password, and restrict what that user can actually do, which commands they're allowed to call, which key patterns they can touch, and even which Pub/Sub channels they can subscribe to.
A read replica used by a reporting service, for example, can get a user allowed only GET, MGET, and SCAN on keys matching report:*, so a compromised credential there can't be used to write data or run admin commands. You manage users with ACL SETUSER, and ACL WHOAMI and ACL LIST let you audit who's configured and what they can do, which matters once you've got more than one team or service sharing the same instance.
When a new replica connects for the first time, the primary needs to send it a full copy of the dataset before incremental replication takes over. By default that means the primary forks, writes a full RDB file to disk, then streams that file over the network to the replica. repl-diskless-sync flips this around: instead of writing to disk first, the forked child streams the RDB data directly over the socket to the replica as it's generated, skipping the disk write entirely.
On a primary where disk I/O is the bottleneck, or where you're running on ephemeral or network-attached storage with mediocre write throughput, that avoids a large sequential write you didn't otherwise need. The tradeoff is that if the replica is slow to consume the stream, or the connection is flaky, you can't just retry from a file sitting on disk, you have to redo the whole generation from scratch, so it behaves worse under a slow or unreliable network than disk-based sync does, which is why it's opt-in rather than default.
Hard questions
12Only for network I/O, not for running your commands. Redis 6.0 (2020) added optional threads that read requests off the socket and parse them in parallel, then hand the actual command execution back to the single main thread. Writing the response back to the client got threaded too.
The part that touches your data, the actual GET, SET, ZADD, whatever, still runs on one thread, one command at a time, with the same no-lock guarantee it always had. Background jobs like an RDB fork, AOF rewrites, and lazy key deletion also run off separate processes or threads, but none of them touch live data while a command is executing. I/O threading buys you more throughput on a fat multi-core box under heavy pipelined load. It doesn't change the atomicity story at all.
That's RDB-only persistence with a save point like save 60 10000 that hadn't fired yet when the crash happened. RDB by design only captures state as of its last completed snapshot, so anything written since is gone. Switching to AOF with appendfsync everysec, the default AOF fsync policy, caps the loss window at about one second instead of a full snapshot interval, because a background thread flushes the AOF buffer to disk on that cadence instead of waiting for the next scheduled snapshot.
CONFIG SET appendonly yes
CONFIG SET appendfsync everysec
BGREWRITEAOFappendfsync always fsyncs on every single write and gets you closer to zero loss, but the latency cost is real, every write now waits on a disk fsync before returning, a bad trade for most workloads and a fine one for something like a payment queue where losing even one write is unacceptable.
volatile-lru only considers keys that have a TTL set as candidates for eviction. If a chunk of your keyspace was written without an expire, session data left permanent by accident, a cached value some code path forgot to set a TTL on, those keys are now completely exempt from eviction, no matter how stale or unused they are.
Memory pressure still has to go somewhere, so Redis evicts more aggressively from the smaller pool of TTL-carrying keys instead, which pushes out entries that were actually still getting hit and tanks your hit rate. The fix isn't usually "switch back," it's auditing which keys are missing a TTL and deciding whether that's intentional or a bug someone shipped without noticing.
WATCH marks one or more keys before a MULTI block starts, and if any watched key changes between the watch and the exec, whether from this client or a completely different one, exec fails and returns null instead of running the queued commands. That's optimistic locking: instead of blocking other clients out of a key while you decide what to write, you read the value, decide what you want to write, and only commit if nothing else touched it in between. If something did, your app just retries the whole read-decide-write cycle.
WATCH inventory:sku_1234
GET inventory:sku_1234
MULTI
DECRBY inventory:sku_1234 1
EXECIt's the pattern behind "decrement this counter only if it hasn't changed since I last read it," without ever taking an actual lock that could get stuck if a client crashes mid-hold.
Sentinel processes continuously ping the primary and its replicas, and when enough Sentinels, the quorum count you configure, independently agree the primary is unreachable, they hold a leader election among themselves to pick one Sentinel to run the actual failover. That Sentinel promotes the best-positioned replica to primary, reconfigures the remaining replicas to follow it, and publishes the new topology so clients using Sentinel-aware libraries pick it up automatically.
Running just one or two Sentinels defeats the purpose: with two, a network partition can leave each Sentinel unable to reach a majority, so neither can safely declare the primary down without risking a split-brain call. Three Sentinels spread across separate failure domains is the practical minimum for a quorum that can actually reach a majority decision when one Sentinel, or one whole zone, disappears.
Commands touching multiple keys, an MGET, a Lua script reading two keys, a transaction across keys, only work if every key involved hashes to the same slot. Otherwise the cluster returns an error instead of guessing which node should coordinate the operation.
Hash tags fix this: wrapping the part of the key you want hashed in curly braces, user:{1000}:profile and user:{1000}:sessions, tells Redis to hash only the "1000" substring for slot assignment, so both keys land on the same slot even though the full key strings differ.
redis-cli -c SET "user:{1000}:profile" '{"name":"jordan"}'
redis-cli -c SET "user:{1000}:sessions" 3
redis-cli -c MGET "user:{1000}:profile" "user:{1000}:sessions"It's a deliberate design trade-off, not a bug: Redis Cluster gives up automatic cross-node multi-key operations in exchange for not needing a coordinator node in the write path at all.
Redlock's idea is that a single Redis instance is a single point of failure for a lock, so you run N independent instances, 5 is the usual recommendation, and to acquire a lock you try to SET the same key with the same unique value and a TTL on a majority of them, timing the attempt against a much shorter window than the lock's TTL. Get a majority within that window and you're considered to hold the lock; to release, you delete the key on every instance you managed to lock.
The core criticism, laid out by Martin Kleppmann in his widely cited response to the algorithm, is that Redlock still assumes a synchronous system with bounded clock drift and bounded processing delays, neither of which you actually get in the real world. His example: a client acquires the lock, then a long GC pause or a slow network hiccup happens right before it does the protected operation, its lock TTL expires while it's paused, a second client acquires the lock and starts working, and the first client wakes up and finishes its operation, now with two clients both believing they hold exclusive access.
Redlock protects against a single Redis node's failure, but not against the deeper problem of assuming a paused or delayed process can't come back and act. The fix Kleppmann suggests, a fencing token that downstream systems check is monotonically increasing, addresses the actual correctness gap. Redlock alone doesn't.
Redis Cluster splits the keyspace into 16384 hash slots. A MOVED redirect means a slot has permanently moved to a different node, telling the client to update its local slot map and always send that slot there from now on. ASK is different and only shows up mid-migration: while a slot is actively being moved key by key from source to destination, a key that's already migrated lives on the destination, but the slot as a whole is still officially owned by the source.
If a client asks the source for a key that's already moved, the source returns ASK pointing at the destination, but the client is only supposed to treat that as a one-off redirect for this single request, not update its permanent slot map, because most other keys in that slot are still on the source. To actually use the redirect correctly, the client has to send ASKING immediately before the retried command on the destination node, telling it "serve this one key even though this slot isn't officially mine yet." Get this wrong, say by caching the ASK redirect like a MOVED one, and you'll start sending unrelated keys in that same slot to the wrong node, which is why hand-rolling cluster support instead of using a cluster-aware client library is a common source of subtle production bugs.
When a replica disconnects and reconnects, Redis tries a partial resync first. The primary keeps a replication backlog, a fixed-size, in-memory circular buffer of the most recent write commands, and if the replica's last known offset is still inside that buffer, the primary just streams the missing commands from where the replica left off. That's cheap, no fork, no full dataset transfer.
The problem shows up when the primary is under heavy write load and the blip lasts long enough that the writes from during the gap get overwritten in that circular buffer before the replica reconnects. At that point the primary has no choice but to fall back to a full resync, forking and sending the entire dataset again, which on a large, busy dataset is expensive: a fork under write load, a big RDB transfer, and the replica unavailable or serving stale reads for however long that takes.
The fix is sizing repl-backlog-size generously enough to cover your realistic reconnect windows given actual write throughput, since the default of 1MB is fine for a quiet dataset but gets blown through in seconds on a busy one. It's a config people don't think about until a routine network hiccup unexpectedly turns into a multi-minute full resync during peak traffic.
Replicas don't run their own active expiration cycle for keys with a TTL, and they don't lazily delete a key on read either, on purpose, to keep the replica's dataset a consistent, deterministic copy of whatever the primary has. Instead, when a key expires on the primary, whether the active expire cycle caught it or a client read triggered a lazy delete, the primary generates an explicit DEL and propagates it to every replica, same as any other write.
So logically a key can be expired as far as TTL math goes, PTTL on the replica shows a negative number, but the key can still physically exist in the replica's memory until that DEL arrives over the replication link. If your app reads with a plain GET, Redis still checks the TTL server-side and returns nil regardless of whether the DEL has arrived, so a straightforward GET is safe.
Where teams get bitten is tooling that doesn't do that same live TTL check, using DEBUG OBJECT to inspect a key directly, or scripts that walk the raw keyspace and report counts. That kind of tooling reports keys as present on a replica well after they've logically expired, which looks like a bug in Redis but is actually the expected propagation-lag behavior of read replicas.
An empty SLOWLOG rules out any single command taking too long, so the next suspects are things that block the main thread without technically being a slow command in Redis's own accounting, or that happen outside command execution entirely. Fork time is the classic one. BGSAVE, BGREWRITEAOF, or a new replica doing a full sync all fork the process, and while fork() itself is fast on Linux, on a host with a large dataset and Transparent Huge Pages enabled, the kernel does extra work copying page tables that can turn a normally sub-millisecond fork into hundreds of milliseconds of the whole process being frozen, none of which shows up in SLOWLOG since it isn't a Redis command, it's happening in the kernel.
LATENCY HISTORY fork and LATENCY LATEST will actually show this if you check them, since Redis instruments fork time separately from command execution time. The other common cause that slips past SLOWLOG is expired-key cleanup: Redis deletes expired keys in small batches to avoid blocking too long in one go, but if a huge number of keys with the same TTL expire in the same window, say your app set a fixed 3600 second TTL on millions of keys around the same time, that cleanup work adds up across many short cycles without necessarily registering as one long-running command.
The general move is to check LATENCY HISTORY for fork and expire event categories, correlate the spike timing against your BGSAVE or AOF rewrite schedule and against whatever set a wave of same-length TTLs, and if it is fork-related, disable Transparent Huge Pages on the host, which resolves it for good rather than just working around symptoms.
A ZADD call adds or updates a player's score in one write, and Redis re-sorts that member into the right position automatically, no separate sort step and no re-reading the whole set. ZREVRANGE pulls the top slice back already sorted in one round trip. ZRANK or ZREVRANK gets a specific player's current position without scanning anything at all.
ZADD leaderboard:global 4820 "player:492"
ZREVRANGE leaderboard:global 0 9 WITHSCORES
ZREVRANK leaderboard:global "player:492"The reason this beats rolling your own with a List or a database ORDER BY query on every score change: a Sorted Set keeps the order maintained incrementally as an invariant of the structure itself. A leaderboard with 50,000 active players updating scores every few seconds doesn't need a full re-sort on any single write, since each update is O(log N) against the current size, not O(N log N) against a full re-sort.
How to prepare for a Redis interview in 2026
Skip another slide deck on the eight data types and just run redis-cli against a local instance for twenty minutes. Start it with a tiny maxmemory (CONFIG SET maxmemory 1mb), set allkeys-lru, and load in more keys than that ceiling until you watch eviction kick in. Kill a replica mid-write and see what WAIT actually reports back. Force a cross-slot error on a two-node cluster by running MGET on two keys without a hash tag, then fix it with braces and watch it succeed. None of that takes more than half an hour, and it sticks in a way re-reading a list of Redis interview questions never does.
Across mock interviews run through LastRoundAI tagged backend or platform, the eviction-policy question trips up more candidates than the transactions question does, even though MULTI/EXEC gets more airtime in most prep guides. My guess is that eviction feels like boring config trivia right up until an interviewer asks why cache hit rate dropped after a policy change, the exact scenario in this page's eviction section. We don't have a clean percentage to put on that pattern, only that it comes up often enough in review to flag here.
LastRound data
Debug your own answers before an interviewer does it for you
Reading an answer is not the same as defending it once an interviewer changes one number on you, drops maxmemory, kills the primary mid-write, adds a second cluster node. LastRoundAI's mock interview mode runs backend and platform rounds with follow-up questions that adapt to what you actually said instead of a fixed script, and the free plan includes 15 credits a month that reset monthly rather than piling up. Starter is $19/mo if fifteen sessions a month isn't enough runway.
If the slower part of the job hunt right now is finding enough backend or platform roles that actually mention Redis, rather than passing the interview once you land one, Auto-Apply queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.
Questions about either product go to contact@lastroundai.com. That's the only inbox we check.
How this list was built
Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.
What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.
If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.
Frequently asked questions
What Redis topics come up most often?
Interviewers concentrate on the parts that cause production incidents rather than the parts that are pleasant to learn. Expect the fundamentals to be assumed and the follow-up questions to sit one layer below what a tutorial covers.
Do I need hands-on Redis experience to pass?
It shows quickly either way. Textbook answers hold up until the interviewer asks what you did when it broke, and that is usually the question that separates candidates. A small real project you can discuss honestly beats a longer list of familiarity claims.
Is Redis still worth learning in 2026?
For interview purposes the question is really whether the teams you are targeting use it, which is worth checking against their actual job postings rather than general popularity rankings. Where it is in use it tends to be deeply embedded and slow to replace.
Should I memorise Redis syntax for the interview?
Rarely worth it. Most interviewers care that you know what to reach for and why, and will not fail you for forgetting an exact flag. Being confidently wrong about behaviour costs far more than admitting you would check the documentation.
What is the most common mistake in Redis interviews?
Answering the question that was asked and stopping there. The strongest candidates add the trade-off or the failure mode without being prompted, which is what signals real use rather than revision.

