Time To Live
Time To Live (TTL) is the cross-domain pattern of attaching an expiration to a piece of cached or transient data so that, after a bounded interval, the data is treated as absent or stale and either re-fetched, re-validated, or discarded. It originates in the Internet Protocol header field of the same name (RFC 791, 1981) and the Domain Name System resource-record TTL (RFC 1035, 1987), but it now appears at every layer of every modern distributed system: HTTP
Cache-Control: max-agedirects browser and proxy caches; Content Delivery Networks (CDNs) like Cloudflare and Akamai expire each cached asset on its own TTL; key-value stores like Redis and DynamoDB let you tag entries with TTLs so they self-evict; session stores expire idle sessions for security; message queues use TTL-flavored visibility timeouts to redeliver unprocessed messages. Conceptually, TTL is the consistency-versus-availability dial at the cache layer — longer TTL means fewer origin requests but staler data, shorter TTL means fresher data but higher origin load. Its interview value comes from the operational problems it both solves and creates: thundering herds, cache stampedes, cookie/session lifetimes, DNS propagation delay, and the design choices around eager versus lazy expiration.
1. Plain-Language Definition
A piece of data has a TTL if, in addition to the data itself, it carries an expiration — either a wall-clock deadline (“valid until 2026-05-09 14:30:00 UTC”) or a relative duration (“valid for the next 300 seconds”). Anyone holding the data can use it freely until the deadline passes; afterward, the data is no longer to be trusted and must be either refreshed from a source of truth, marked as stale, or simply forgotten.
The pattern matters because most distributed systems are built around caching, and caches face a fundamental dilemma. Without an expiration, cached entries grow indefinitely and serve obsolete data when the underlying source changes — the famous “your DNS is cached for two days, please clear it” frustration. Without caching at all, every request hits the origin and the system collapses under load. TTL is the simplest mechanism that makes both bounds tractable: data lives in the cache for the TTL window (cheap, fast), then expires (correctness re-asserted at the cost of one origin fetch), then is re-cached.
The TTL itself is a parameter — the operator’s lever for trading off these forces. A 5-second TTL on session data keeps things fresh but bombards the origin; a 24-hour TTL gives huge cache hit rates but means a permission revocation takes up to 24 hours to take effect. Picking TTL is a recurring design decision in every caching layer of every large system, which is why it shows up across so many interview rubrics.
2. Mechanism
A TTL implementation has two responsibilities: recording when each entry expires, and enforcing the expiration. Recording is trivial — store either an absolute Unix timestamp or a relative duration alongside the value. Enforcement is the interesting part because it forces a choice between three implementation strategies.
flowchart TB PUT[client: PUT key=v ttl=300s] --> STORE[storage layer] STORE --> EXPMETA[record expires_at = now + 300] GET1[client: GET key, time=now+100s] --> CHECK{expires_at past?} CHECK -->|no| RETURN[return v] CHECK -->|yes| EXPIRE[mark expired<br/>delete or report miss] GET2[client: GET key, time=now+400s] --> CHECK SWEEP[background sweeper] -.->|periodic scan| EXPIRE
Diagram: the TTL lifecycle. On PUT we record the expiration; on subsequent GETs we check it and either return the value or treat the key as missing. A background sweeper optionally proactively reclaims storage even for entries no one is reading.
2.1 Eager (Active) Expiration
A background thread or periodic job sweeps the data store, finds entries whose deadline has passed, and deletes them. Memory is reclaimed promptly. The cost is constant CPU/IO overhead — even when nothing is being read or written, the sweeper is running. For a store with billions of keys, the sweeper itself can become an expensive component. If the sweep period is long, expired entries linger; if short, the sweeper monopolizes CPU.
2.2 Lazy (Passive) Expiration
The store does nothing in the background. Every read checks the entry’s TTL; expired entries are removed (or reported as miss) at read time. There is zero overhead for unaccessed keys, but if a key is set with a TTL and then never read, it occupies memory forever. For caches dominated by hot reads, this is fine — expired entries are cleared on the next access. For workloads with long-tailed access patterns (most keys read rarely), lazy expiration creates a slow, silent memory leak.
2.3 Hybrid (Lazy + Periodic Sweep)
Most production systems blend the two. Redis is the canonical example: every read goes through expireIfNeeded() (lazy), and a background activeExpireCycle() runs ~10 times per second, sampling 20 random keys with a TTL set, deleting any expired ones, and repeating until the fraction of expired-keys-among-samples falls below 25% (Redis docs — Expiration; see db.c). The randomized sampling avoids scanning all keys (which would be O(n) and crush large databases) while ensuring expired entries cannot accumulate indefinitely.
Memcached uses pure lazy expiration: keys are checked on read, and the slab allocator’s LRU eviction reclaims space if memory fills. The result: a Memcached instance under low read pressure can have many expired keys still consuming slabs, which is fine because slab memory is overwritten by LRU before it would be needed by new sets.
DynamoDB TTL is a special case: AWS scans tables in the background (typically delivering deletes within 48 hours) and fires DELETE events to streams (AWS DynamoDB TTL docs). The “TTL is best-effort” semantic surprises new users — an item is logically expired immediately past its expires_at, but the physical row may persist for tens of minutes. Reads must filter on expires_at > now themselves rather than trusting the table to be already-clean.
2.4 Why Hybrid Wins in Practice
Real systems converge on the hybrid approach for a specific reason: the failure modes of each pure strategy are unacceptable at scale. Pure eager expiration cannot handle a billion-key store because the sweep cycle becomes the dominant CPU consumer; pure lazy expiration cannot handle a workload where most keys are written-once-and-never-read because expired keys monopolize memory until OOM-eviction kicks in.
Redis’s tuned defaults illustrate the engineering nuance. The activeExpireCycle runs ten times per second, sampling 20 random keys with TTLs each invocation. If more than 25% of the sampled keys are expired, it samples another 20 and repeats; this loop is bounded at 25ms wall-clock per invocation to avoid blocking the main event loop (Redis source — db.c). The numbers were tuned over years of production feedback: small enough samples to be cheap, large enough to keep up with realistic write rates, with an explicit time budget so the sweeper never starves foreground work.
Memcached’s pure lazy strategy works because Memcached is a cache — the slab allocator’s LRU evicts the least-recently-used entries when memory fills, and most expired entries are also least-recently-used (you set them; they expire; nobody asked again). The slab eventually overwrites them. This works for cache use cases but not for the “use Memcached as the primary store of expiring sessions” use case where some keys live until expiration without ever being read.
2.5 Network and DNS TTL
Outside data stores, TTL serves a different role. The IP TTL field (RFC 791) is decremented at every router; when it reaches zero, the packet is discarded. This is a hop-count, not a wall-clock deadline — it prevents packets from looping forever in misrouted networks. (The name “Time To Live” was originally meant literally — IP designers envisioned TTL as seconds, decremented at each router with a delay-aware decrement, but in practice every router decrements by exactly 1 and it became a hop counter. The misnamed field survives.)
DNS Resource Record TTL (RFC 1035) is the wall-clock TTL that an authoritative name server attaches to records it returns. Recursive resolvers cache the record for at most that many seconds before re-querying. A www.example.com A record with TTL 300 means “cache for at most 5 minutes.” Lower TTL = faster propagation when records change but more queries to authoritative servers; higher TTL = better cache hit rates but slower propagation. Operators classically lower DNS TTL hours before a planned migration to keep the propagation tail manageable, then restore it after the cutover.
A subtle property of DNS TTL is that it is an upper bound, not a guarantee. Resolvers may cache for less time (e.g., they restart, or memory pressure forces eviction), and clients may cache longer than the record’s TTL specifies (some browsers and operating systems do, ignoring the protocol). The de facto propagation time after a DNS change is therefore roughly “the configured TTL plus some long tail of bad clients.”
This is why high-stakes DNS changes (cutting over the production load balancer) involve preparation: lower the TTL to 60 seconds 24 hours in advance, wait for the old high-TTL records to flush from caches, then make the change, then optionally restore a longer TTL after the migration is stable. Skipping the preparation step means the migration’s tail of stragglers can last hours.
2.6 HTTP and CDN TTL
HTTP’s caching model layers TTL on top of two header families. The original Cache-Control directive max-age=N (relative seconds) was added in HTTP/1.1 and is the modern primary; the legacy Expires: header (absolute date) is still supported but deprecated when max-age is also present. The shared-cache directive s-maxage=N overrides max-age only for proxies and CDNs, allowing the operator to say “browsers cache for an hour, the CDN caches for a day.”
CDNs add a third level: the origin’s Cache-Control headers are the suggestion, but the CDN’s own per-asset TTL configuration overrides them in many products. Cloudflare’s Edge Cache TTL settings, for example, can ignore the origin’s Cache-Control: no-cache and cache anyway — useful when the origin app’s developers haven’t been trained to set headers correctly. The interaction between origin headers and CDN policy is a recurring source of surprise: a header you set may not be the policy that’s actually applied.
3. Origins
The IP-layer TTL appears in RFC 791 (Postel, September 1981, §3.2), defined as an 8-bit field measured in seconds with a minimum decrement of 1 per router, intended to bound the lifetime of a packet so misrouted datagrams could not consume bandwidth indefinitely. The field’s reinterpretation as a pure hop-count happened by convention, not specification.
The DNS-record TTL appears in RFC 1035 (Mockapetris, November 1987, §3.2.1), defined as a 32-bit unsigned integer of seconds that bounds how long a non-authoritative server may cache the record. The semantics are explicit: 0 means do not cache, larger values are upper bounds. DNS TTL is what made the entire DNS hierarchy scale — without per-record caching the root name servers would have collapsed in the early 1990s under direct-query load.
HTTP gained its caching semantics in RFC 2068 (1997, the original HTTP/1.1) and refined them in RFC 2616 (1999), then again in RFC 7234 (2014), and most recently in RFC 9111 (2022). The TTL-flavored directives are Cache-Control: max-age=<seconds> (relative) and Expires: <date> (absolute), with max-age winning when both are present. The s-maxage directive applies only to shared (proxy/CDN) caches, allowing different TTLs for browser caches versus edge caches.
In key-value stores, Memcached added per-key expiration in its earliest releases (~2003); Redis added it in 2009 with the EXPIRE, PEXPIRE, and later SETEX commands. DynamoDB added native TTL in February 2017, treating it as a first-class table feature with its own column. The general pattern of “tag an entry with a deadline; reclaim asynchronously” was already established practice by then; what was new was the cloud-native packaging.
4. Worked Example
Consider a CDN-cached product page on an e-commerce site. The product description rarely changes, but stock and price update frequently. A reasonable design caches the rendered HTML for 5 minutes (Cache-Control: public, max-age=300) and bypasses the cache for the price-and-stock fragment, fetching it via a separate API call.
Imagine the product page is requested by 100 users distributed evenly over a 5-minute window. The cache-hit timeline:
t=0s user_1 request → cache MISS → origin (200ms) → cache stores entry, expires at t=300
t=0.5s user_2 request → cache HIT (5ms response)
t=1.5s user_3 request → cache HIT
...
t=299s user_99 request → cache HIT
t=300s user_100 request → entry now expired → cache MISS → origin → cache stores entry, expires at t=600
99 of 100 requests hit the cache, costing the origin 1 fetch instead of 100. Edge response latency for cached requests is the CDN’s PoP-to-user latency (~10ms) plus the cache lookup (~1ms). The origin sees one request per 5-minute window per CDN PoP — a simple multiplicative reduction of the origin load.
Now imagine the same product page is on Reddit’s front page and ten million users hit it simultaneously, each routed to one of fifty CDN PoPs. Each PoP sees an independent stream of ~200,000 requests/5-minute window. On the first request to a given PoP, the cache misses; the PoP fetches from origin; the next 199,999 requests within the 5-minute window are cache hits at that PoP. The origin sees 50 fetches per 5 minutes. Excellent.
But consider the moment of expiration. At t=300, every PoP’s cached entry expires simultaneously (because they all populated at roughly t=0 from the original content release). The next request at each PoP misses; each PoP fetches from origin. The origin sees 50 simultaneous fetches at t=300. This is the thundering herd at the per-PoP scale.
It gets worse intra-PoP. Within one PoP, between t=300 (expiration) and t=300.2s (when the origin response returns), the PoP receives, say, 1000 concurrent requests for the same key. Naively, each of those 1000 sees a cache miss and triggers a separate origin fetch. The origin now sees 50,000 fetches, not 50. This is the cache stampede and is the canonical TTL pitfall.
Mitigations include:
- Single-flight / request coalescing at the cache: only one in-flight request per key; the other 999 wait for that one’s response. Cloudflare calls this “concurrent stream limit”; nginx calls it
proxy_cache_lock. It is the single most important stampede defense. - Stale-while-revalidate (RFC 5861): once an entry is past its TTL, serve the stale copy to incoming requests for a configured grace window while a single background fetch refreshes it. Users see the stale page for ~200ms; the origin sees one fetch.
- Jittered TTL: instead of
max-age=300, randomize each entry’s TTL between 270 and 330 seconds. Entries that populated together no longer expire together; the herd is smeared across a 60-second window.
Now imagine a different scenario: the price update on a hot product. The CDN holds a 5-minute-stale price for ~5 minutes. If the merchant changes the price at t=10, customers see the old price until t=300 at the earliest. If the change is “we sold out, price is N/A,” up to 5 minutes of customers may complete checkout against stale data, leading to oversold inventory. Mitigation: route price-sensitive fragments through a much shorter TTL (10s) or no cache at all, accepting the higher origin load for that subset.
This worked example illustrates the TTL design tension: every change is one knob trading against another. Picking sensible TTLs requires knowing the access pattern, the rate of change, and the cost of staleness.
4.1 Cache Stampede Mitigation: Concrete Algorithms
The thundering-herd / stampede problem is sufficiently important to warrant detailed treatment of the canonical mitigations.
Single-flight (request coalescing). When N concurrent requests arrive for an expired or missing key K, only the first one fetches from origin; the other N-1 wait on a per-key lock until the first finishes, then receive its response. This is implemented at every level: nginx’s proxy_cache_lock, Cloudflare’s “concurrent stream limit,” Go’s golang.org/x/sync/singleflight, the sync.Once pattern in many languages. The mitigation reduces origin load from N to 1 per key per expiration.
Stale-while-revalidate. Defined in RFC 5861 (Nottingham 2010). The directive Cache-Control: max-age=600, stale-while-revalidate=86400 means “cache for 10 minutes; after that, serve stale for up to a day while a single background fetch refreshes.” Users see the stale value with low latency; the origin sees one fetch per cache, not one per concurrent request. The technique is now widely supported in browsers, Cloudflare, Fastly, Akamai, Varnish.
Probabilistic early refresh. Instead of refreshing exactly at expiration, refresh probabilistically as expiration approaches. The XFetch algorithm (Vattani et al. 2015, Optimal Probabilistic Cache Stampede Prevention) uses expiry - delta * beta * ln(rand()) as the effective expiration; entries refresh with probability that grows toward expiration. The result: refreshes are spread over a window rather than synchronized at one instant, even without explicit locking.
Jittered TTL. When setting TTLs, randomize them within a range — instead of all entries having max-age=300, use 300 + random(-30, 30). Entries that populated together no longer expire together; the expiration herd is smeared. Combined with single-flight, this reduces both the per-key concurrency and the cross-key concurrency at the origin.
In production, all four mitigations are typically used together. They are layered: jittered TTL spreads expirations; stale-while-revalidate serves through the brief miss window; single-flight prevents per-key concurrency on the actual fetch; probabilistic refresh further smooths the load.
5. Variants and Implementations
| Domain | TTL mechanism | Typical values |
|---|---|---|
| IP / TCP packets | IP header TTL (8 bits, hop count) | Default 64 (Linux), 128 (Windows), 255 (some BSDs) |
| DNS records | RR TTL (32-bit seconds) | A/AAAA: 300–3600s typical; root NS: 86400–172800s |
| HTTP responses | Cache-Control: max-age=<s>, Expires: <date> | Static assets: 86400–31536000s; HTML: 0–300s |
| CDN edge cache | Per-asset TTL with manual purge override | Cloudflare default: respects origin Cache-Control; configurable Edge Cache TTL 0–365d |
| Browser cache | Same HTTP Cache-Control | Often the same TTL as CDN; ignores s-maxage |
| Cookies | Max-Age=<s> or Expires=<date> | Session: 0; persistent: hours to years |
| Redis | EXPIRE, PEXPIRE, SETEX, EXPIREAT, PERSIST | Sessions: 900–1800s; rate-limit windows: 60s; cache: 60–3600s |
| Memcached | set <key> <flags> <exptime> <bytes> | Same ranges as Redis |
| DynamoDB TTL | Per-item attribute storing Unix timestamp | Anything; deletion is async (best-effort, may take 48h) |
| SQS visibility timeout | Per-queue or per-message | 30s default; 0–43200s |
| Kafka topic retention | retention.ms, retention.bytes | Hours to forever; not strictly TTL but conceptually related |
| JWT (JSON Web Token) | exp claim (Unix timestamp) | 5min–24h depending on token type |
| Kerberos tickets | Lifetime + renewable lifetime | TGT: 8–10h; service ticket: shorter |
The diversity of implementations highlights that TTL is fundamentally a protocol-level concept — wherever you have a producer and a consumer separated by a cache or a network hop, you have a place where TTL is the right mechanism for bounding staleness.
A useful mental model: TTLs are usually expressed as either absolute deadlines (Expires: 2026-05-09T14:30:00Z, DynamoDB’s stored Unix timestamp) or relative durations (max-age=300, Redis EXPIRE key 300). Absolute is more correct in distributed systems where clocks drift, because the deadline is well-defined globally; relative is friendlier for clients but requires the client and store to agree on what time it is. Most modern systems use relative-at-write, then translate to absolute internally.
5.05 Comparison Table — TTL Mechanisms by System
| System | Sweep | Storage | Min granularity | Best-effort? |
|---|---|---|---|---|
| Redis | Hybrid (lazy + active-expire-cycle) | Separate expires hashtable | Millisecond (PEXPIRE) | No (deterministic) |
| Memcached | Pure lazy + LRU | Per-entry exptime field | Second | No (but LRU may evict early) |
| DynamoDB | Background sweep service | Per-item attribute | Second | Yes — best-effort, may take 48h |
| Browser HTTP cache | Lazy (per-request) | Per-entry header parsing | Second (max-age) | No |
| CDN edge cache | Lazy + per-asset purge support | Per-object metadata | Second | No (but per-PoP variance possible) |
| DNS resolver | Lazy + bounded by RR TTL | Per-record cache entry | Second | No |
| Kafka topic retention | Background segment deletion | Per-segment timestamp | Configured retention.ms | Yes — segments retire in chunks |
| etcd lease | Server-side with grant/keepalive | Per-lease object | Second | No (deterministic) |
The diversity of “sweep” strategies in the second column reflects that different access patterns (Redis: in-memory, frequent; DynamoDB: massive table scan needed) make different choices appropriate. Best-effort vs deterministic in the last column is the most operationally important distinction — applications must read the docs to know whether their TTL is a guarantee or a heuristic.
5.1 Choosing TTL: A Decision Framework
Picking a sensible TTL requires reasoning about three questions, in order:
1. What is the cost of staleness? If the cached data being one minute out of date is acceptable, TTL ≥ 60s is permissible. If even a one-second window of stale data is unacceptable (financial transactions, security tokens after revocation), TTL alone is insufficient — push-based invalidation or aggressive single-digit-second TTL is required. The cost-of-staleness question is fundamentally a product question, not a technical one.
2. What is the cost of cache miss? If origin fetches are cheap (1ms latency, plentiful capacity), short TTLs are fine and you trade off in favor of freshness. If origin fetches are expensive (100ms+ latency, scarce capacity), longer TTLs are mandatory and you trade off in favor of cache hit rate.
3. What is the rate of change? If the underlying data changes every 30 seconds, a TTL of 5 minutes is wasteful — most cached entries will be served as stale. If the data changes once an hour, a TTL of 5 minutes is paranoid. TTL roughly tracks the rate of change with a safety factor.
The interaction of the three: TTL is bounded above by acceptable staleness (Q1) and bounded below by origin capacity (Q2), with rate-of-change (Q3) as the natural sweet spot. When the bounds are inconsistent (acceptable staleness < what the origin can handle), the design must change — add a CDN, add async invalidation, push freshness via WebSockets — TTL alone cannot solve it.
6. Real-World Examples
DNS root server TTLs. The 13 root server NS records have TTL of 6 days (518400 seconds); the root TLD records (e.g., com., org.) have TTL of 2 days. These long TTLs are essential — without them every recursive resolver in the world would be hammering the root servers continuously, which they cannot handle. Resolver-side caching of long-TTL root data is what lets DNS scale to billions of queries per day with fewer than 1000 root-server instances (root-servers.org statistics).
CDN TTL strategy. Cloudflare’s default behavior is to cache static assets (.css, .js, .png, .jpg) for several hours and HTML for 0 seconds (no cache by default), then offer page rules letting operators override per-path (Cloudflare docs). For dynamic content with a known invalidation event, the typical pattern is “long TTL + manual purge” — set a 1-day TTL for cache-hit-rate, and call the CDN’s purge API when the underlying content actually changes. This is dramatically more efficient than a short TTL but only works when you can detect changes.
Session TTL. Web frameworks default to ~15–30 minutes of idle session lifetime, refreshed on every authenticated request. Banking and financial sites use shorter (5–15 minutes) to bound the window for shoulder-surfing or unattended-machine attacks. SaaS products often use longer (hours to days) since user friction is the dominant concern. Session TTL is the security/usability trade in compact form.
Redis cache aside. A typical pattern: read from Redis; on miss, read from the database and SETEX the result with TTL=300. The TTL caps both staleness (data refreshes every 5 minutes max) and memory growth (entries automatically retire). This is so common Redis ships dedicated commands for it (Redis docs — EXPIRE).
Cookie Max-Age. Browsers respect the Max-Age directive on Set-Cookie (browser-side TTL), deleting the cookie automatically when it elapses. For session cookies, no Max-Age is set — the cookie expires when the browser closes. For “remember me” cookies, Max-Age=2592000 (30 days) is typical. Modern security practice favors short-lived access cookies (15min) plus long-lived refresh tokens, the duration split serving different threat models.
Kafka retention. Not strictly TTL but the same idea: each topic has a retention.ms setting that bounds how long messages are kept. Default is 7 days. A Kafka cluster’s disk usage is bounded roughly by (produce_rate × retention) + replication_overhead, making retention.ms a major capacity-planning parameter. CDC topics often run with longer retention (30 days or compaction-only) to support consumer recovery.
Memcached’s “30 day” gotcha. Memcached treats TTL values < 30 days as relative durations and >= 30 days as absolute Unix timestamps. Setting exptime=2592001 (one second over 30 days) is interpreted as “expire at Unix time 2592001,” which is January 1970, meaning the value expires immediately. This is a famous footgun documented in the Memcached wiki.
Facebook’s TAO. TAO is Facebook’s read-optimized graph cache, fronting MySQL. It uses TTL for cache freshness in combination with explicit invalidation messages from the write path. The TTL is the safety net: even if invalidation messages are lost, the cache cannot serve permanently stale data. Described in the 2013 USENIX paper TAO: Facebook’s Distributed Data Store for the Social Graph (Bronson et al. 2013).
Discord’s session expiration. Discord uses Redis-backed sessions with a TTL of one week, refreshed on every authenticated request. The choice is friendlier than typical 30-minute session TTLs because Discord users are session-resident for hours-to-days at a time; aggressive timeouts would degrade UX. The trade is increased session-store memory and a longer window for stolen-token attacks. Discord’s engineering blog has discussed the design.
7. Tradeoffs
The dominant tradeoff is staleness versus origin load, modulated by TTL length. Doubling the TTL halves origin requests (for a steady-state read workload) but doubles the upper bound on how stale cached data can be. The shape of the cost curve depends on workload: if reads are bursty around fresh content (news, social feeds), short TTLs are mandatory; if reads are evenly distributed against slowly-changing data (product catalog), long TTLs are nearly free.
A second tradeoff is memory cost of pending expirations. Eager expiration (full sweep) reclaims memory promptly but costs CPU; lazy expiration costs no CPU but lets memory grow until reads or external eviction reclaim it. The hybrid Redis approach is a tunable middle: more frequent active-expire-cycle iterations cost CPU, fewer iterations let expired keys linger.
A third tradeoff is propagation latency for invalidations. Long TTLs mean changes take long to propagate. The mitigation is explicit invalidation (CDN purge API; cache delete; DNS update) on top of TTL, but this couples the producer to the cache (the producer must know to purge), which TTL was supposed to avoid. Engineers often run with “long TTL + purge on known events + short TTL fallback” as a defense-in-depth pattern.
A fourth tradeoff is between absolute and relative TTLs. Absolute TTLs (“expires at Unix time 1715260983”) are unambiguous across distributed systems and survive clock skew gracefully — once the deadline arrives in real time, every node agrees the entry is expired. Relative TTLs (“expires 300 seconds from now”) are simpler for clients to specify but require the store and the client to agree on what “now” is, which is fragile under clock skew. Most modern systems convert relative-at-write to absolute internally for storage, then back to relative-as-needed for client API ergonomics.
A fifth tradeoff is memory locality of expiration metadata. Storing expires_at as an extra column on every key costs memory; some systems compress it (delta encoding, shared epoch), others don’t. Redis stores expirations in a separate hash table indexed by key, only for keys that have expirations set, which costs an extra pointer-chase on TTL’d reads but avoids the per-key memory overhead for non-TTL’d keys.
7.1 The Cookie / JWT / Session Triple
A particularly dense corner of TTL practice is web-session lifetime, where three different mechanisms with different semantics interact. A typical login flow produces:
- A session cookie stored in the browser, marked HttpOnly and Secure, with
Max-Age=900(15 minutes); the browser automatically attaches it to every same-origin request. - A JWT access token with
exp900 seconds, sent in theAuthorizationheader for API requests; verified by checking signature +exp. - A refresh token, longer-lived (7 days) and stored more carefully (HttpOnly cookie or secure storage); used to obtain new access tokens.
Each of these has a TTL, but they protect different threat models. The cookie’s Max-Age defines how long the browser sends the cookie; the JWT’s exp defines how long the server accepts the token; the refresh token’s exp defines how long the user can stay logged in without reauthenticating.
The recurring design question is: when the user “logs out,” what happens? If logout deletes the session cookie but the JWT was cached by a Service Worker, the JWT may continue to work for exp - now seconds. If logout invalidates the refresh token but the access token is still valid, the user can keep using the application until the access token expires. Production OAuth implementations typically use very short access-token TTLs (5 minutes) and rely on refresh-token revocation as the primary “logout” mechanism — accepting up to 5 minutes of post-logout access in exchange for never having to maintain a server-side access-token blocklist.
8. Pitfalls
-
Thundering herd (synchronized expiration). When many cache entries expire simultaneously, every refresh hits the origin in parallel, potentially overloading it. This happens organically when cache entries were populated together (mass cache pre-warm; restart event; new hot content). Mitigation: jittered TTL (randomize within ±10%); stale-while-revalidate; serving stale on origin failure.
-
Cache stampede (single-key concurrency). When a single hot key expires and many concurrent requests all observe the miss, each fires its own origin fetch. The origin sees N parallel fetches for the same key. Mitigation: single-flight (request coalescing) at the cache; probabilistic early refresh (refresh before expiration with probability
(elapsed/ttl)^β). -
TTL longer than the underlying data’s volatility. Setting a 1-hour TTL on data that changes every 10 seconds means users see hour-stale data routinely. The fix is not “longer TTL”; it is “shorter TTL or push-based invalidation.” Invariant: TTL should be ≤ acceptable staleness, not “as long as the cache will tolerate.”
-
Clock skew between writer and store. Absolute-deadline TTLs assume the writer’s clock and the store’s clock agree. In a distributed system with NTP drift on the order of seconds, an entry may be considered expired immediately after a write or, worse, never expire. Mitigation: prefer relative-duration TTLs, which the store interprets in its own time; for absolute TTLs, ensure clock sync via NTP or use a logical-clock approach.
-
DynamoDB TTL is best-effort, not real-time. Items past
expires_atcan persist for tens of minutes (the AWS SLA is 48 hours upper bound). Applications cannot trust queries to be already-filtered; they must filter onexpires_at > nowthemselves. Many production bugs trace to forgotten read-side filters. -
Cookie
Max-Age=0to delete. SettingSet-Cookie: name=; Max-Age=0; Path=/is the canonical pattern to delete a cookie. ForgettingPath=/(or specifying the wrong path) means the deletion silently fails — the original cookie persists because it was set with a different path. The security implication on logout flows is real: a logout that fails to delete the session cookie leaves the session reusable. -
TTL on negative cache entries. A cache that records “no such entry” (negative result) with a TTL can serve incorrect “not found” answers if the entry is later created. DNS classically caches
NXDOMAINresponses with the SOA’s minimum TTL; if a domain didn’t exist when first queried, it’s effectively unreachable for the cached duration even after registration. Mitigation: short negative-cache TTLs (DNS’s recommendation is to cap NXDOMAIN caching at 3 hours per RFC 2308). -
JWT
expand the offline-revocation problem. A JWT (JSON Web Token) withexp1 hour is, by design, valid for that full hour even if the user is “logged out” or has their permissions revoked — the token is self-contained and not consulted against any server-side state. This is the JWT-vs-session-cookie debate: shortexp(5 min) plus a refresh-token mechanism approximates revocation; longexpwith no revocation is dangerous. Mitigation: keep access-token TTL short (≤15min) and validate refresh against a server-side store.
8.1 The Negative Cache Pitfall, Elaborated
A subtler version of pitfall 7 deserves its own treatment. Negative caching — caching the absence of a result — is necessary for performance: without it, a million requests for a non-existent key would each hit the origin. With it, only the first request hits; subsequent requests are served “not found” from cache.
But the negative cache must expire faster than the positive cache, because creating a previously-absent entry is a transition the cache cannot observe locally. DNS solves this with the SOA record’s “minimum TTL” field, which caps NXDOMAIN caching to typically 1-3 hours per RFC 2308. HTTP solves it with Cache-Control: max-age=0, must-revalidate for 404 responses. Application caches often forget the issue entirely and either don’t negative-cache (hammering the database for every miss) or cache too long (creating phantom 404s).
The fix is to negative-cache with a short TTL (10-60 seconds is typical for application caches) and to have an out-of-band signal that creates the entry on the producer side, allowing the negative cache to flush. For DNS, the out-of-band signal is the registrar update + zone reload; for HTTP, the application can send a cache-purge after creating the resource.
9. Common Interview Discussion Points
“Pick a TTL for ___” is a recurring sub-question. Expect to have a defensible reason. For a session cookie: tens of minutes idle, refreshed on activity, balanced against the security cost of an unattended browser. For a DNS record: minutes for a record undergoing migration, hours for stable records, days for pure infrastructure (root servers). For a CDN-cached static asset: long (1 day to 1 year) with cache-busting via filename hashing. For a price/inventory cache: very short (seconds to a minute) or push-based invalidation.
Cache stampede is a near-universal follow-up. Anyone who has run a high-traffic cache has been bitten by stampedes. The strong answer mentions single-flight (request coalescing), stale-while-revalidate, and probabilistic early expiration, and explains why each helps. Bonus credit for naming proxy_cache_lock (nginx), groupcache (Go library), srand-style early-refresh from Vattani et al. 2015.
TTL versus active invalidation. TTL is a bound on staleness, not a guarantee of freshness. For data that must be current immediately on change (price updates, permission revocations), TTL alone is insufficient — you need explicit invalidation (purge, pub/sub invalidation messages, write-through). Most production systems combine both: TTL as the safety net for missed invalidations.
Why DNS uses TTL but HTTP also has Cache-Control. Both implement the same idea at different layers. DNS TTL is on resource records, with caching by recursive resolvers; HTTP’s Cache-Control: max-age is on response bodies, with caching by clients and proxies. The history is that HTTP/1.0 (1996) had an explicit Expires: header (absolute), HTTP/1.1 (1997) added Cache-Control: max-age (relative), and modern guidance is to use both for compatibility.
The “stale-while-revalidate” pattern. Originally RFC 5861 (2010), now widely adopted. The directive Cache-Control: max-age=600, stale-while-revalidate=86400 means “cache for 10 minutes; after that, serve stale for up to a day while a background refresh runs.” This decouples user-visible latency from origin-fetch latency entirely — users always get a fast cached response; freshness happens lazily.
TTL versus LRU as eviction policies. Pure TTL evicts only on expiration; pure LRU evicts only when memory is full. Most production caches combine them: TTL governs when an entry becomes invalid, LRU governs which invalid (or even valid) entries to evict when memory is tight. Redis exposes a maxmemory-policy setting (volatile-lru, allkeys-lru, volatile-ttl, allkeys-lfu, etc.) that lets the operator pick the eviction strategy when both signals are available. Knowing the matrix of options and when to choose each is a sign of operational depth.
Why DNS uses min(TTL) for combined records. When a recursive resolver caches multiple resource records that came together (e.g., a CNAME chain), it caches each at its own TTL but treats the bundle as expiring at the minimum. This prevents partial-bundle staleness where, e.g., the CNAME has expired but the underlying A record has not. The rule appears in RFC 1034 and is one of those subtle DNS behaviors that explain otherwise-mysterious propagation timings.
Memory management: eager vs lazy. Knowing that Redis uses lazy + sampled active expiration, and explaining why (the alternative O(n) full scan would not scale to millions of keys), is a standard intermediate question. Naming the activeExpireCycle algorithm earns extra points.
Connection to other topics. TTL is intimately related to Caching Strategies (cache-aside, write-through, write-behind), Content Delivery Network System Design (the CDN’s whole job is to serve cached responses with TTL), Domain Name System Design (TTL drives the propagation model), and Session Management. A good interview answer connects to at least one of these neighbors.
8.5 The Visibility Timeout in Message Queues
Message queues use a TTL-flavored mechanism called the visibility timeout (or “lock duration”) that deserves its own treatment because it appears in nearly every distributed-system interview involving queues.
When a consumer receives a message from SQS / RabbitMQ / Kafka with manual commits, the message is not yet deleted from the queue — it is marked invisible to other consumers for the visibility timeout period. The consumer is expected to process the message and then explicitly delete it (commit). If the consumer crashes before committing, the visibility timeout expires and the message becomes visible again to other consumers.
This is a TTL on a lease, not a TTL on data. The mechanics are the same — record an expiration, check on read — but the semantic is “if I don’t hear back by time T, return ownership to the pool.” Picking the timeout right matters: too short, and slow-but-correct processing causes duplicate delivery; too long, and a crashed consumer’s messages are stuck for the entire timeout window.
SQS exposes the timeout per-queue or per-message (default 30 seconds, max 12 hours); RabbitMQ uses consumer-timeout (default infinite); Kafka uses max.poll.interval.ms. The pattern is universal: distributed locks, leader election, leases — all rely on TTL as the failsafe for participants that crash without releasing.
9.1 TTL Across Layers — A Unifying View
Reflecting on the breadth of TTL applications surfaces a useful unification. Every place TTL appears, the underlying problem is the same: a producer of authoritative data and a consumer of cached or transient data are separated, and the consumer needs a bound on how out-of-date its view can be. TTL is the simplest solution to that problem — encode the bound as part of the data itself, let the consumer enforce it locally.
The alternatives to TTL are all more complex. Push-based invalidation (the producer notifies the cache when data changes) requires a reliable channel from producer to cache, and the producer must know about every cache. Versioned cache keys (cache-key includes a content hash, so updates produce a different key) requires the producer to know the cache exists and update keys at the right time. Lease-based caching (the consumer holds a lease that the producer can revoke) requires bidirectional coordination.
TTL’s appeal is that it requires no producer involvement. The producer doesn’t know the cache exists; the cache doesn’t know the producer’s identity. They communicate only through the data carrying its expiration. This is eventual consistency by construction — the system converges within at most one TTL window after any change.
The price is the latency of convergence. If you cannot tolerate up to one TTL window of staleness, you cannot use pure TTL; you need one of the more complex mechanisms (or a combination, like “TTL + push-purge for known events”).
9.15 The Connection to Eventual Consistency
The TTL pattern is fundamentally a consistency model: an eventually-consistent cache, with the consistency window bounded by the TTL. From the CAP-theorem perspective, cached data with TTL is the canonical AP system — it sacrifices consistency (in the strict sense) to retain availability, with TTL parameterizing how stale “eventually” can be.
Production systems that need stronger consistency than TTL provides have to push the consistency boundary elsewhere. Read-through caches that block on the origin if the cache misses move toward CP at the cost of latency. Read-your-own-writes consistency requires bypassing the cache for recently-written data, typically via a “dirty bit” or recently-modified set. Linearizability requires routing all reads through a consensus layer.
TTL is the simplest, fastest, weakest member of the consistency family. The right design move is often to start with TTL (because it is so easy to reason about and operate) and tighten only the data paths that genuinely need stronger guarantees.
9.2 The Mathematics of Cache Hit Rate Under TTL
For a workload where each cached key is accessed at rate λ requests/second and underlying data changes at rate μ updates/second, the relevant probabilities under a TTL of T seconds are:
- Probability a key is accessed during its TTL window: roughly
1 - exp(-λ × T)(for Poisson-distributed accesses). - Probability of cache hit:
(λT - 1)/(λT)for hot keys, dropping toward 0 as λT decreases. - Probability of staleness:
1 - exp(-μ × T)per access (the chance that data has changed since the cached version was fetched). - Origin request rate: roughly
1/Tper key for hot keys (one fetch per TTL window).
Practical implication: hit rate scales with λT (access rate × TTL), staleness scales with μT (change rate × TTL). The ratio λ/μ — how often a key is read versus written — is the natural quality factor for TTL caching. Keys with λ/μ ≫ 1 (read-heavy) cache well even under short TTLs; keys with λ/μ ≈ 1 (read-write balanced) cache poorly because most cached versions are stale before they’re read.
This explains a recurring engineering observation: TTL caching is great for “popular and slowly-changing” data (product catalogs, user profiles, configuration) and bad for “rarely-read or fast-changing” data (per-user inventory states, real-time prices, individual order status). The decision of whether to cache is not “should we use TTL?” but “is this data in the read-heavy regime where TTL works?“
9.3 What “Stale” Actually Means
A frequently-glossed point: “stale” is a misleading binary framing. In production, cached data is on a continuous spectrum from “moments-old and almost certainly correct” to “many-TTLs-old and almost certainly wrong.” The TTL boundary is a hard cutoff that doesn’t reflect this gradient.
Some advanced systems track softer expiration: an entry is “fresh” until time T1, “stale-but-usable” until T2, “stale-and-do-not-serve” thereafter. The HTTP stale-while-revalidate directive is a coarse approximation of this; some application caches generalize it further. The model lets the system make context-dependent decisions: serve fresh in normal traffic, serve stale-but-usable during origin failures, hard-error only when the data is truly unusable.
The lesson for designers: even when TTL is the right primitive, recognizing that “stale” is a continuum, not a binary, often reveals a useful design tweak. Two TTLs (a soft and a hard one) capture more of the underlying reality than one.
10. See Also
- Distributed Caching Architecture — TTL is the primary tuning knob in cache design
- Content Delivery Network System Design — edge cache TTL governs hit rates and origin load
- Domain Name System Design — DNS TTL is the original, longest-lived application of the pattern
- Message Queue System Design — visibility timeout is a TTL-flavored mechanism for at-least-once delivery
- Distributed Key Value Store System Design — Redis, DynamoDB, Memcached all implement TTL natively
- Distributed Log System Design — Kafka retention.ms is a TTL for log segments
- Rate Limiting — TTL on counters is how sliding-window rate limiters expire stale buckets
- ACID Transactions — TTL is fundamentally an eventual-consistency mechanism, antithetical to ACID
- Multi-Version Concurrency Control — old row versions in MVCC are conceptually expired by transaction-visibility rules, a structural cousin to TTL
- Write-Ahead Log — WAL retention has a TTL-like dimension (how long to keep segments before recycling)
- Change Data Capture — CDC consumers can fall behind and create the same “long-lived hold” problem TTL is supposed to bound
- SWE Interview Preparation MOC
- Major System Designs MOC