Redis is an in-memory data-structure server that can accelerate reads and coordinate bounded operations. A production design must define the source of truth, consistency window, capacity policy, failure behavior, and ownership of every key. Persistence and replication improve recovery options; they do not give a cache the transactional invariants of its source database.
Cache-Aside and Consistency Windows #
Cache-aside leaves read and write policy in the application:
The application reads Redis, loads a miss from the database, stores the result with a TTL, and returns it. A source write must invalidate or refresh affected keys. Because database commit and cache mutation are not one atomic operation, readers can observe stale data between those events. Define the acceptable window and use a transactional outbox or change-data stream when cache invalidation must survive process failure.
Cache penetration occurs when repeated requests for absent identifiers always reach the database. Store a short-lived negative sentinel after authoritative absence is confirmed, but never let that sentinel outlive the interval in which creation must become visible. A Bloom filter can reject definitely absent values before lookup; false positives still reach the backing systems.
Stampedes and Refresh #
When a popular key expires, concurrent misses can duplicate an expensive load. Mitigations include:
- adding randomized TTL jitter so related keys do not expire together;
- allowing one bounded refresher to rebuild a key while others use stale data;
- coalescing in-process requests for the same key;
- refreshing hot keys before their hard expiration.
A refresh lock needs a lease and failure path. It must not turn a cache miss into indefinite waiting, and the backing service still needs overload controls because Redis can be unavailable.
Expiration, Eviction, and Memory Pressure #
Set value and expiry atomically:
SET session:42 '{"user":42}' EX 1800Redis removes expired keys passively when accessed and actively by sampling keys with TTLs. Expiration therefore defines logical visibility, not the exact instant at which memory is reclaimed. TTL selection should include freshness, recomputation cost, object size, and synchronized-expiry risk.
maxmemory bounds memory available to data, and maxmemory-policy selects the
response to pressure. Policies include noeviction, all-key or volatile-key
LRU/LFU approximations, random eviction, and volatile TTL-based eviction.
Volatile policies consider only keys with an expiry and can behave like
noeviction when no eligible keys remain. Monitor used memory, fragmentation,
evictions, expirations, rejected writes, latency, and cache hit ratio rather
than assuming a configured TTL guarantees residency.
Distributed Locks and Fencing #
Acquire a single-instance Redis lock with a unique ownership token and bounded lease:
SET lock:invoice:42 4d1c9f... NX PX 30000Release only if the token still matches, using one atomic script:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
end
return 0The lease limits abandonment but does not prove exclusive access. A process can pause beyond its TTL, lose the lock, resume, and write concurrently with a new holder. When the downstream resource can be corrupted, issue a monotonically increasing fencing token and require that resource to reject tokens older than the greatest one already observed.
Replication is asynchronous, clocks and pauses affect lease reasoning, and a network partition can hide ownership changes. Prefer a database constraint, conditional write, or consensus coordinator when it already protects the invariant. A Redis lock should guard bounded, retryable work whose failure model has been documented.
Fixed-Window Rate Limits #
A fixed window stores one counter for each subject and time bucket. Increment, set the initial expiry, and decide atomically:
local count = redis.call("INCR", KEYS[1])
if count == 1 then
redis.call("EXPIRE", KEYS[1], tonumber(ARGV[1]))
end
if count > tonumber(ARGV[2]) then
return {0, count}
end
return {1, count}Fixed windows need constant state, but traffic at the end of one bucket and the start of the next can create a burst close to twice the nominal limit. Derive bucket identity consistently; client clocks can disagree, so use server time in the script or route one subject through a consistent clock domain when boundary precision matters.
Sliding Windows and Buckets #
A sorted-set sliding log stores one unique member per request with its timestamp as the score. One script removes entries before the window, counts remaining entries, conditionally adds the request, and sets a TTL. The algorithm enforces a rolling count but consumes memory proportional to admitted events. Members must remain unique even when multiple requests share a timestamp.
A token bucket stores a token balance and last-refill time. Each request refills according to elapsed time up to a capacity, then consumes a token if available. It enforces a long-term rate while permitting a configured burst. A leaky bucket instead drains queued work at a fixed rate, smoothing output but requiring bounded queueing and an explicit rejection policy.
Lua scripts and Redis Functions make each decision atomic on one Redis node. Keep scripts deterministic and bounded; a long script blocks other command execution. Return limit, remaining capacity, and reset or retry timing only when those values accurately reflect the chosen algorithm.
Cluster Placement, Retries, and Observability #
Redis Cluster requires every key touched by one transaction, script, or function
to belong to the same hash slot. Use a hash tag such as
rate:{account-42}:write to colocate related keys without forcing unrelated
accounts into one slot. One hot subject can still overload its slot.
Timeout does not prove that a mutating command failed. Retrying INCR, XADD,
or a rate-limit script blindly can duplicate effects, so attach an operation ID
or make the script detect repeated requests when exact-once accounting matters.
Bound retries and expose Redis errors rather than silently replacing a strict
limit with unlimited access; security-sensitive systems must choose fail-open or
fail-closed deliberately.
Record script latency, errors, rejected and allowed counts, hot keys, slot distribution, replica lag, failovers, memory pressure, and clock skew. These signals distinguish policy behavior from Redis saturation or topology changes.
Comments
Read or comment on GitHub (sign-in required to comment)