Caching interview questions

Table of Contents

Thundering herd problem

The thundering herd problem occurs when many clients or servers react to the same event simultaneously, causing a sudden spike in load on a downstream dependency. One of the most common causes is cache expiration.

Consider a cache entry with a fixed TTL of one hour. While the cache is valid, requests are served directly from memory and the database receives little or no traffic for that key. However, once the TTL expires, every server that needs the value will observe a cache miss at roughly the same time.

Imagine a fleet of 50 application servers. If the cached value expires, each server may independently issue a query to rebuild the cache. Instead of one database query, the system suddenly generates 50 identical queries. If the cached data is used by hundreds or thousands of incoming requests, the situation becomes even worse. Before any server manages to rebuild and repopulate the cache, thousands of requests may reach the database for the exact same piece of data.

Databases are typically provisioned for steady-state traffic rather than large synchronized bursts. Although the system may comfortably handle 10,000 requests per second under normal conditions, a sudden spike of identical requests can exhaust database resources, increase latency, and trigger request timeouts.

Timeouts often make the situation worse. Many retry mechanisms use the same retry interval for every client. If thousands of requests fail simultaneously and all retry after the same delay, they create a second synchronized wave of traffic. This feedback loop can prolong outages and significantly increase recovery time.

This phenomenon is known as the thundering herd problem, and in caching systems it is often referred to as a cache stampede. The core issue is not the overall request volume, but rather the fact that many requests become correlated and arrive at the same moment because they were all triggered by a single event, such as cache expiration.

Request coalescing (singleflight)

Request coalescing ensures that only one request performs the expensive operation required to rebuild a missing cache entry. When multiple requests encounter the same cache miss, the first request fetches the data while the remaining requests wait for the result. Once the cache has been populated, all waiting requests use the newly cached value. This prevents duplicate work and dramatically reduces load on the database.

Distributed locking

In a distributed environment, request coalescing must work across multiple servers. A common solution is distributed locking. One server acquires a lock, rebuilds the cache entry, and then releases the lock when the update is complete. Other servers either wait for the lock to be released or temporarily serve stale data while the cache refresh is in progress.

Facebook introduced a lease mechanism in Memcache to prevent large numbers of servers from simultaneously regenerating the same cache entry. The idea is similar to distributed locking, where only one server is allowed to repopulate the cache while others wait or continue serving stale data.

Jittered retries

Retry logic can unintentionally create another thundering herd. If 100 clients all fail at the same time and each retries exactly two seconds later, they will generate another synchronized burst of traffic.

To avoid this, systems introduce random jitter into the retry delay. Instead of retrying at a fixed interval, each client waits for a randomly chosen amount of time. This spreads retries over a larger time window and reduces the likelihood of another traffic spike.

1delay = random(0, min(cap, base * 2^attempt))

In this formula, base * 2^attempt implements exponential backoff, capdefines the maximum retry delay, and random(...)ensures that retries are distributed across time rather than occurring at the same instant.

Modern distributed systems often combine request coalescing, distributed locking, stale cache serving, and jittered retries to mitigate thundering herd scenarios. Together, these techniques help maintain system stability during cache expirations, traffic spikes, and partial service failures.

Redis

Durability

Redis is primarily an in-memory data store, meaning the main copy of the data lives in RAM. However, Redis provides persistence mechanisms to recover data if the Redis process or machine crashes. The two main mechanisms are RDB snapshots and the AOF (Append Only File).

RDB (Redis Database Snapshot)

RDB periodically creates a snapshot of the entire dataset and saves it to disk. The important thing to understand is that when Redis performs an RDB snapshot, it looks exclusively at the current in-memory representation of all keys and values. It does not replay or read the AOF to construct the snapshot.

For example, suppose Redis currently contains:

1user:123 -> "Kenneth"
2user:456 -> "Alice"
3counter -> 42

When an RDB snapshot is created, Redis takes the current state of these keys in memory and writes that state into an RDB file on disk. The snapshot therefore represents a point-in-time copy of the dataset.

Because RDB stores a compact representation of the dataset rather than every individual write operation, it is generally efficient and useful for backups and faster restarts.

However, RDB snapshots are periodic. If Redis takes a snapshot at 12:00 and crashes at 12:05, any writes that happened between 12:00 and 12:05 may not exist in the RDB file. Therefore, RDB alone can result in some recent writes being lost.

RDB is therefore a good choice when you want efficient persistence, backups, and fast recovery, and you can tolerate losing some writes since the most recent snapshot.

AOF (Append Only File)

AOF takes a different approach. Instead of periodically writing the entire dataset to disk, Redis records the write operations that modify the dataset.

For example, if an application performs these operations:

1SET user:123 "Kenneth"
2SET user:456 "Alice"
3INCR counter

Redis can record these commands in the append-only log. The log might conceptually look like:

1SET user:123 "Kenneth"
2SET user:456 "Alice"
3INCR counter

The purpose of the AOF is to provide a durable record of the changes made to the dataset. If Redis crashes and restarts, it can replay the commands in the AOF to reconstruct the in-memory dataset.

Unlike RDB, AOF can provide much finer-grained durability because Redis can control how frequently AOF data is flushed to disk. For example, Redis can be configured to synchronize the AOF on every write, once per second, or allow the operating system to decide when to flush it.

This creates a trade-off between durability and performance. Flushing every write provides stronger durability but can introduce additional I/O overhead, while flushing less frequently improves performance but means more recent writes could be lost if the machine crashes.

RDB vs AOF

The key difference is that RDB stores a snapshot of the current state, while AOF stores a history of write operations.

1RDB:
2In-memory state
34Periodic snapshot
56RDB file
7
8AOF:
9Write command
1011Append to log
1213AOF file

They can also be used together. For example, Redis can use an RDB file to quickly load an initial dataset and then use the AOF to recover more recent changes that occurred after the snapshot.

In short, RDB is mainly about efficiently storing the state of the database at a point in time, while AOF is mainly about recording changes so that the state can be reconstructed after a failure.

Redis as a Cache

Redis is commonly used as a cache when the application can tolerate some degree of staleness or inconsistency. The cache does not necessarily need to contain the latest version of the data because the underlying database remains the source of truth.

This is especially useful in systems that can tolerate eventual consistency. For example, if a cached value is slightly out of date for a short period of time, the system can still function correctly.

Hot Keys

A hot key is a key that receives a disproportionately large number of requests. Even if Redis is distributed across many instances, all requests for the same key may be routed to the same Redis instance, creating a bottleneck.

One possible solution is to replicate the hot value across multiple keys. For example, instead of storing a value under user:123, we could use keys such as user:123:1, user:123:2, and so on. Requests can then be distributed across these keys, spreading the load across multiple Redis instances.

TTL (Time To Live)

A TTL sets an expiration time on a Redis key. Once the TTL expires, the key is no longer available. This is useful for temporary data such as sessions, cache entries, verification codes, and rate-limiting counters.

LRU Eviction

Redis can be configured with a maximum memory limit and an eviction policy. When Redis reaches this limit, it can automatically remove keys according to the configured policy. With an LRU (Least Recently Used) policy, Redis preferentially evicts keys that have not been accessed recently.

Rate Limiting

Redis can be used to implement a rate limiter because operations such as incrementing a counter can be performed atomically.

For example, suppose we want to allow a user to make at most N requests per minute. We can maintain a Redis key containing the request count:

1INCR rate_limit:user123
2EXPIRE rate_limit:user123 60

If the counter exceeds the configured limit, we reject the request. After the key expires, the counter is removed and the user gets a fresh window.

This simple fixed-window approach does not guarantee fairness. For example, a user could make N requests at the end of one window and another N requests immediately after the next window begins.

Redis Streams

Redis Streams provide an append-only sequence of messages. Each entry receives a unique ID, typically based on a timestamp, and contains one or more key-value fields.

Streams can be used to implement asynchronous job queues. For example, producers can add jobs to a stream and workers can consume them.

A consumer group allows multiple workers to process messages from the same stream. Each message is assigned to one consumer within the group, allowing the workload to be distributed across multiple workers.

If a worker fails while processing a message, the message remains associated with that worker and can later be claimed by another consumer. This allows failed or abandoned work to be recovered.

Redis Streams provide at-least-once delivery, not exactly-once processing. For example, if a worker processes a message but loses network connectivity before acknowledging it, another worker may eventually process the same message. Therefore, consumers should ideally make their processing logic idempotent.

Sorted Sets

A sorted set is a Redis data structure that stores multiple unique members, with each member associated with a numeric score. Redis automatically keeps the members ordered by their scores.

It is useful for things such as leaderboards, rankings, and priority queues where we need to efficiently find the highest- or lowest-ranked items.

A sorted set has a Redis key, which identifies the sorted set itself. Inside that sorted set are multiple members, and every member has a numeric score.

For example, suppose we want to build a leaderboard for tweets based on the number of likes. We could create a sorted set calledtweet_leaderboard:

1ZADD tweet_leaderboard 150 "tweet:123"
2ZADD tweet_leaderboard 300 "tweet:456"
3ZADD tweet_leaderboard 200 "tweet:789"

Here, tweet_leaderboard is the Redis key. It identifies the entire sorted set. The strings tweet:123, tweet:456, andtweet:789 are the members of the sorted set. The numbers 150, 300, and200 are their respective scores.

In this example, the score represents the number of likes that each tweet has received:

1Redis key: tweet_leaderboard
2
3Member       Score
4-------------------
5tweet:123     150
6tweet:456     300
7tweet:789     200

So tweet:456 is currently ranked first because its score is 300. tweet:789 is ranked second with 200 likes, andtweet:123 is ranked third with 150 likes.

Notice that the tweet itself is the member, not the Redis key. The Redis key identifies the collection of tweets, while each tweet is an individual member inside that collection. The score is the value Redis uses to determine the member's ordering.

We can then retrieve the top 5 most-liked tweets using:

1ZREVRANGE tweet_leaderboard 0 4 WITHSCORES

ZREVRANGE returns members from highest score to lowest score. This makes retrieving the top-ranked items efficient without having to sort all the tweets ourselves.

One important distinction is that a sorted set does not have a traditional "key → value" relationship like a Redis string. Instead, it is more like: Redis key → set of members, where each member has a score.