Compare a fixed expiry (TTL set once when the value is written to Redis) with a sliding TTL that is extended on every cache hit. When is each the right choice, and what does the sliding version cost?
answer
- Fixed = max age; sliding = max idle
- GETEX key EX n = read + extend, one trip
- Sliding makes reads writes: AOF, replication, no replica reads
- Hot key + sliding = stale forever
- Hybrid: sliding under an absolute cap
basics
~20 sFixed TTL bounds staleness: the entry dies at a known time no matter how often it is read. Sliding TTL (EXPIRE or GETEX on read) keeps popular entries alive but lets a continuously read key stay stale forever. Use sliding for idle timeouts, fixed for data freshness.
solid answer
~60 s**Fixed expiry** — set the TTL when you write the value and never touch it again — gives a hard staleness bound: every entry is refetched at most `T` seconds after it was cached, no matter how hot it is. That predictability is what a cache of database-derived data needs. **Sliding TTL** — `EXPIRE key T` or `GETEX key EX T` on each hit — means the deadline measures *idleness*, not age. A key that is read every second never expires, so its value can be arbitrarily old. That is exactly right when the semantics you want are an idle timeout: session state, a login token's inactivity window, a draft form, a rate-limit-free "keep while in use" cache of expensive-to-rebuild objects. Costs of sliding: every read becomes a **write** — it propagates to replicas and to the AOF, cannot be served by a read-only replica, and adds a round trip unless you use `GETEX` (6.2+). It also weakens the safety-net property, so an invalidation bug can persist indefinitely. A common hybrid: sliding within a hard absolute cap, by storing the creation timestamp in the value and refusing to extend past `created_at + max_age`.
code
text · 11 lines# fixed expiry: read does not touch the deadline
> SET page:home "<html>" EX 300
> GET page:home # TTL keeps counting down
# sliding: one round trip, deadline resets on every hit
> SET sess:ab12 "{...}" EX 1800
> GETEX sess:ab12 EX 1800 # value returned AND idle window restarted
# pre-6.2 equivalent (pipelined; note the read is now a write)
> GET sess:ab12
> EXPIRE sess:ab12 1800go deeper
Say clearly that fixed TTL counts from the write and sliding TTL restarts on every read, and give one example of each: cached query result versus login session.
Add the failure mode — a constantly read key under a sliding TTL never refreshes — and that extending on read is a write command.
Discuss the operational cost (replication and AOF amplification, no replica reads) and propose the idle-plus-absolute-cap hybrid, naming GETEX for the single round trip.
Treat the choice as a per-key-prefix contract enforced by the shared cache client, so 'bounded staleness' is a property of the platform rather than of whichever helper a team happened to call.
## Two different meanings of 'expiry' A fixed TTL answers **"how old may this value get?"** A sliding TTL answers **"how long may this value go unused?"** They look similar because both are implemented with the same Redis expiry mechanism, but they encode different contracts, and picking the wrong one is a real production bug. ## Fixed expiry Write once with `SET key value EX 300` (or `SET ... KEEPTTL` on subsequent writes to keep the original deadline) and leave it. Properties: - **Bounded staleness.** The value is at most `T` seconds behind the source of truth. This is the property you can state in a design document and defend in an incident review. - **Predictable origin load** — roughly `keys / T` refetches per second. - **Reads stay reads.** They can be served by replicas, they do not touch the AOF or replication stream, and they are safe to retry. - Downside: hot keys are refetched on schedule even though they are in constant use, so you pay refill cost for data you obviously want cached. That refill on a very hot key is what makes stampede handling a separate concern. ## Sliding (refresh-on-read) TTL On each hit, extend the deadline: `GETEX key EX 1800` in one round trip, or `GET` followed by `EXPIRE` in a pipeline. Properties: - **Idle-timeout semantics.** The entry survives as long as it is used and dies `T` after the last use. Sessions are the canonical case: a user active for eight hours should not be logged out at a fixed six-hour mark, and an idle user should be logged out after thirty minutes. - **Usage-shaped residency.** In effect you are approximating an LRU with expiry, keeping the working set warm. - **Unbounded staleness.** The failure mode: a value read by traffic every few seconds is never re-read from the origin, so a missed invalidation is permanent. For anything derived from a mutable source of truth this is dangerous. - **Every read is a write.** `EXPIRE`/`GETEX ... EX` are write commands. Consequences: they are rejected on a read-only replica, they enter the replication stream and the AOF (so they add I/O and inflate the AOF, and the periodic rewrite has to absorb it), and they force reads through the primary in a read-scaled setup. - **Extra latency** unless combined into `GETEX` — two round trips per read otherwise, or a pipeline. ## The hybrid people actually ship Sliding within an absolute cap. Store `created_at` (or a hard deadline) inside the cached value; on each hit, extend the Redis TTL only while `now < created_at + max_age`. The entry then behaves as an idle timeout up to a ceiling, giving both liveness and a bounded worst-case age. For sessions the same shape appears as "30 minutes idle, 12 hours absolute", which is also the standard security posture for session lifetimes: an absolute cap limits how long a stolen session identifier is useful. An alternative hybrid keeps a **fixed** TTL for freshness and lets `maxmemory` eviction, not expiry, handle residency of hot keys — you get bounded staleness and still keep the working set resident because hot keys are re-populated immediately after each expiry. ## Choosing - Data derived from a database, where correctness depends on age → **fixed**. - State whose lifetime is defined by user activity — sessions, inactivity logout, in-progress workflows, temporary locks held while a job runs → **sliding**, usually with an absolute cap. - Expensive-to-rebuild derived objects that are also immutable for their key (content-addressed) → sliding is harmless, since staleness is not a concept. - Anything security-relevant → fixed and short, or invalidated on the event. Be explicit about which contract each key prefix uses; mixing them by accident (a `GETEX ... EX` sneaked into a helper) turns a bounded-staleness cache into an unbounded one without any visible symptom until a bad value gets pinned.
- Why can a sliding TTL break a setup that serves reads from Redis replicas?Extending a TTL uses EXPIRE or GETEX ... EX, which are write commands. A replica is read-only by default and will reject them, so the read path has to go to the primary, losing the read-scaling benefit. It also means each cache hit produces replication traffic and an AOF entry, which raises write amplification on a read-heavy workload.
- How do you keep sliding behaviour but still bound staleness?Store an absolute deadline or creation timestamp inside the cached value and extend the Redis TTL on read only while that ceiling has not been passed. The entry then expires either after the idle window or at the hard cap, whichever comes first — the same 'idle timeout plus absolute timeout' pattern used for sessions.
saying these in an interview costs you the question
- Applying a sliding TTL to database-derived data and still claiming a bounded staleness guarantee
- Not realising EXPIRE/GETEX are write commands that replicate and hit the AOF
- Implementing sliding as GET plus EXPIRE without noticing the extra round trip when GETEX exists
- Assuming a sliding TTL gives 'LRU for free' with no cost or risk
- Using a fixed TTL for a session and logging active users out mid-work