Unix Timestamps in Caching and TTL — How Expiry Times Actually Work
If you've ever stared at a stale cache entry wondering why it's not expiring when it should, the answer is usually somewhere in a timestamp. Caching systems — from Redis to CDNs to HTTP headers — use Unix timestamps heavily, and getting the math wrong by even a few seconds can cause bugs that are difficult to reproduce.
Use the Unix Timestamp Converter to convert any cache expiry timestamp to a readable date, or to get the current timestamp for debugging TTL calculations.
What TTL Means and How It's Expressed
TTL stands for time-to-live. It's how long a cached value should be considered valid before it needs to be refreshed or discarded.
TTL can be expressed in two ways depending on the system:
Relative TTL: The number of seconds the cache entry should live from the time it was stored. Redis and Memcached both work this way by default. If you set a key with TTL of 3600, it expires 3600 seconds (one hour) from when it was set, regardless of what time that was.
Absolute expiry timestamp: The Unix timestamp at which the cached value expires. HTTP Cache-Control headers with max-age work as relative TTL, but Expires headers use an absolute date-time. CDNs and some application-level caches store an absolute expiry timestamp — the current time plus the TTL — and check whether now > expiry to determine staleness.
Both approaches translate to the same underlying logic: compare the current Unix timestamp to the stored expiry timestamp. The difference is where that arithmetic happens — at write time or at read time.
How Redis Handles Key Expiry
Redis is the most common in-memory cache, and its TTL behavior is worth understanding in detail.
When you set a key with EXPIRE key 3600, Redis records the absolute Unix timestamp at which the key should expire: current_time + 3600. You can see this with EXPIRETIME key, which returns the Unix timestamp of the expiry (Redis 7.0+). TTL key returns the remaining seconds.
Redis uses lazy expiry combined with periodic sweeps. A key isn't deleted the instant it expires — it's deleted when it's next accessed (and found to be expired) or when Redis's background sweep picks it up. This means a key with a TTL of 0 might still appear in DEBUG SLEEP scenarios or when the sweep hasn't run yet.
The practical implication: if your application reads a key, checks it's not nil, and then uses it — and there's even a small window between the GET and the TTL check — you might read a value that's just expired. In high-throughput systems, this can cause cache stampedes when many requests simultaneously find an expired key and all try to regenerate it.
HTTP Caching and Unix Timestamps
HTTP caching uses a mix of relative and absolute time values, and the interaction between them causes a lot of confusion.
Cache-Control: max-age=3600 tells a browser or CDN that the response is valid for 3600 seconds from when it was received. This is relative — each client tracks its own clock from when it got the response.
Expires: Thu, 17 Apr 2026 10:00:00 GMT is an absolute timestamp. It tells the client not to use the cached response after that point. The problem is that it requires the client's clock to be accurate. If a client's system clock is wrong by an hour, the cache behavior is off by an hour.
Age header: When a CDN has already had a response cached for 600 seconds, it sends Age: 600 to the client. Combined with max-age, the client knows the remaining fresh time is max-age - Age. If Age exceeds max-age, the response is already stale.
Last-Modified and ETag: These are validation headers — they let the client ask "is this still fresh?" rather than just using a TTL. The server returns a 304 Not Modified (no body, just headers) if the content hasn't changed, which is much faster than sending the full response.
CDN Expiry and Purge Logic
CDNs like Cloudflare, Fastly, and CloudFront cache responses at the edge and use TTL to determine how long to serve a cached copy without going back to the origin server.
CDNs typically store an absolute expiry timestamp computed at the time the response is cached: cached_at + max_age. On each request, they check current_time > expiry. If true, they fetch a fresh copy from origin.
The edge case that trips people up: a CDN might have cached a response at multiple edge locations at slightly different times. The CDN in Frankfurt cached the response 12 seconds before the one in Singapore. Their expiry timestamps differ by 12 seconds. If you do a "cache purge," it invalidates all of them simultaneously regardless of TTL. But if you're relying on TTL expiry alone, you might get a stale response from one edge location several seconds after another edge has already refreshed.
Debugging Stale Cache with Timestamps
When you suspect a cache is returning stale data, the first thing to do is get the raw timestamps:
1. Get the current Unix timestamp — use the Unix Timestamp Converter or run date +%s in your terminal.
2. Find the cached entry's expiry timestamp — in Redis, EXPIRETIME key; in HTTP responses, parse the Expires header or calculate from Date + max-age - Age; in your application cache, log the stored expiry value.
3. Compare them — if current_time > expiry, the entry should be expired. If it isn't being evicted, check your eviction policy, lazy vs eager expiry settings, and whether the entry is being refreshed before it expires.
4. Check for clock skew — if your application server and cache server have different system clocks, TTL calculations will be off. A common production bug is a cache server whose clock drifted 5 minutes behind. Entries that should have expired 4 minutes ago are still being returned as fresh.
NTP synchronization prevents clock drift, but doesn't eliminate it entirely. For critical expiry logic, use server-generated timestamps from a single source rather than trusting that all hosts agree on the current time.
Storing Expiry Timestamps in Your Application
When you build application-level caching — storing computed results in a database or key-value store with an expiry — you have to decide how to represent the expiry.
Option 1: Store the TTL (relative). The entry knows it should live for 3600 seconds, but not when it was created. You need a separate created_at field to compute whether it's expired.
Option 2: Store the absolute expiry timestamp. At write time, compute expires_at = current_unix_timestamp + ttl. At read time, check current_unix_timestamp > expires_at. Simpler to reason about — you only need one field.
The absolute timestamp approach is almost always cleaner. Storing expires_at: 1775000000 is clearer than storing ttl: 3600 separately from created_at: 1774996400. It also survives restarts cleanly — a relative TTL means nothing if you don't know when the entry was created.
Cache Stampede and How Timestamps Help Prevent It
Cache stampede happens when many requests simultaneously find that a cached value has expired and all try to regenerate it at once. For an expensive database query or external API call, 50 simultaneous regeneration requests can overwhelm the backend.
The timestamp-aware solution is probabilistic early expiry (also called cache warming): instead of expiring the entry exactly at expiry_time, start probabilistically refreshing it earlier. A simple version:
if current_time > (expiry_time - random_factor * remaining_ttl):
refresh the cache
Where random_factor is typically 0–1. This spreads out cache refreshes rather than clustering them at the exact expiry moment.
A simpler approach: add jitter to TTLs. Instead of setting every entry to expire in exactly 3600 seconds, set it to expire in 3600 + random(-300, 300) seconds. This desynchronizes expiry times across different entries and prevents them from all expiring simultaneously.
Signed vs Unsigned Timestamps and the 2038 Problem
Most caching systems store TTL values as 32-bit integers. For TTL values (relative durations), this is fine — no one is setting a cache TTL of 2 billion seconds. But for absolute expiry timestamps, a signed 32-bit integer overflows on January 19, 2038.
If your caching infrastructure stores absolute Unix timestamps in 32-bit integers, entries set to expire after January 2038 will have incorrect expiry values. This is an edge case today — most production TTLs are minutes or hours, not years — but systems that cache long-lived tokens, certificates, or configuration data with multi-year expiry could hit this.
Modern systems and languages handle this correctly with 64-bit integers. It's worth checking if you're working with older embedded systems, legacy C code, or any system that handles timestamps as int rather than int64 or long long.


