skip to content

When would you use the Redis command `EXPIREAT` (or `PEXPIREAT`) instead of `EXPIRE`, and how does Redis represent a key's expiry internally with respect to time?

level: seniorimportance: should knowfreq 30%

answer

  1. expires dict stores absolute ms deadlines
  2. EXPIRE is converted to an absolute deadline on execution
  3. replicated/AOF-written as PEXPIREAT — no drift
  4. TTL survives restart; past deadlines are dropped on load
  5. EXPIRETIME/PEXPIRETIME (7.0) read the deadline back

basics

~20 s

EXPIREAT takes an absolute Unix timestamp rather than a relative duration. Redis stores every expiry as an absolute millisecond timestamp anyway — EXPIRE is converted internally — and replicates and persists it as PEXPIREAT, so deadlines survive restarts and never drift on replicas.

solid answer

~60 s

`EXPIRE key seconds` is relative; `EXPIREAT key unix-seconds` and `PEXPIREAT key unix-ms` are absolute. Internally Redis keeps only the absolute form: the `expires` dictionary maps a key to a millisecond Unix deadline, and a relative `EXPIRE` is converted to that at execution time. That internal choice has two visible consequences. First, expiries are **replicated and written to the AOF as `PEXPIREAT`**, never as the original relative command — otherwise the deadline would restart on every replica and after every AOF replay. Second, TTLs **survive restarts**: an RDB or AOF reload restores the same absolute deadlines, so a key with 10 minutes left still has about 10 minutes left after a restart. Use `EXPIREAT` when the deadline is a real point in time: expire at midnight, at the end of a billing period, at a token's `exp` claim, or to give a batch of keys one identical deadline computed once. Redis 7.0's `EXPIRETIME`/`PEXPIRETIME` read that absolute deadline back. Caveat: expiry follows the system clock, so a backwards clock step extends lifetimes.

code

text · 21 lines
text
# expire at a real instant (seconds)
> EXPIREAT daily:2026-08-14 1755129600
(integer) 1
> EXPIRETIME daily:2026-08-14
(integer) 1755129600

# millisecond variant for precision
> PEXPIREAT batch:job:9 1755129600000
(integer) 1

# a past timestamp is a delete, not a stored expiry
> EXPIREAT tmp:x 1000000000
(integer) 1
> EXISTS tmp:x
(integer) 0

# unit mix-up: ms value passed to the seconds command -> effectively immortal
> EXPIREAT oops 1755129600000
(integer) 1
> TTL oops
(integer) 1753374470400

go deeper

for a junior

Know that EXPIREAT takes an absolute Unix timestamp while EXPIRE takes a duration, and that TTLs survive a restart.

for a middle

Explain that Redis stores absolute millisecond deadlines internally and give concrete cases where an absolute API is the better fit.

for a senior

Connect the storage model to replication and AOF rewriting as PEXPIREAT, restart semantics, and the clock-skew and unit-mismatch failure modes.

for a principal

Reason about deadlines as data shared across systems — token expiry, batch cohorts, calendar boundaries — and about which clock is authoritative when application and datastore can drift.

## Two ways to say the same thing - `EXPIRE key seconds` / `PEXPIRE key milliseconds` — relative: "die this long from now". - `EXPIREAT key unix-time-seconds` / `PEXPIREAT key unix-time-milliseconds` — absolute: "die at this instant". All four return `1` when the timeout was set and `0` when it was not, and all four accept the `NX`/`XX`/`GT`/`LT` conditions from Redis 7.0. ## Redis only stores absolute deadlines Each Redis database holds two dictionaries: the keyspace (key → value) and `expires` (key → expiry). The value stored in `expires` is always an **absolute Unix timestamp in milliseconds**. A relative `EXPIRE key 300` is turned into `now + 300000` at the moment the command executes; the "300 seconds" is never retained. `TTL`/`PTTL` compute the remaining time by subtracting the current clock from that stored deadline, which is why they are cheap and why they always agree with reality. This single design decision explains most of the observable behavior around expiry. **Replication and AOF.** Redis rewrites expiry-setting commands as `PEXPIREAT` before propagating them to replicas and the append-only file. If it propagated the relative `EXPIRE 300`, a replica applying it a few milliseconds later — or an AOF replay hours later — would compute a different, later deadline, and the replica would keep keys the primary had already dropped. Propagating the absolute instant makes every copy agree on the same deadline regardless of when it processes the command. **Restarts.** RDB snapshots and the AOF carry absolute deadlines, so after a restart a key that had ten minutes left still has ten minutes left. Keys whose deadline already passed while the server was down are simply not loaded into the keyspace (on a primary), so a long downtime does not resurrect expired data. Contrast this with a hypothetical relative model, where every restart would silently renew every TTL. **Reading the deadline.** Redis 7.0 added `EXPIRETIME key` and `PEXPIRETIME key`, returning the stored absolute deadline (with the same `-1` / `-2` sentinels as `TTL`). Before that you could only get the remaining time and add it to your own clock, which is imprecise. ## When absolute is the right API 1. **Calendar boundaries.** A daily counter that must reset at 00:00 UTC, a promotional cache valid until the campaign ends, a leaderboard bucket for the current hour. Computing `EXPIRE` as `midnight - now` works but re-derives the boundary at every call site and drifts if the computation happens at slightly different moments. 2. **A deadline that already exists elsewhere.** A JWT carries an `exp` claim; a session record has a hard end; a signed URL has a validity instant. Passing that timestamp straight to `EXPIREAT` keeps Redis and the token from disagreeing. 3. **One deadline across many keys.** Writing 50 000 keys in a batch that must all die together: compute the instant once and `PEXPIREAT` each key with it. With relative TTLs the first and last key differ by however long the batch took. 4. **Idempotent re-application.** Re-issuing the same `PEXPIREAT` is a no-op in effect; re-issuing `EXPIRE 300` silently extends the lifetime each time — which is exactly the sliding-window bug people hit in rate limiters. Relative `EXPIRE` remains the better API for "N seconds of caching from now", where the duration, not the instant, is the meaningful thing. ## Clocks Expiry is evaluated against the server's system clock, so: - A **backwards** clock step (bad NTP, VM snapshot restore, manual correction) pushes deadlines further into the future — keys live longer than intended. - A **forwards** step expires a batch of keys at once, which can look like a cache stampede. - Prefer NTP **slewing** over stepping on Redis hosts, and remember that clients computing absolute timestamps for `PEXPIREAT` are using *their own* clock — skew between an application server and Redis becomes skew in the deadline. If the application and Redis clocks can diverge meaningfully, relative `EXPIRE` is more robust because only the server's clock participates. ## Edge cases - A timestamp in the past (or `EXPIRE` with a non-positive value) **deletes the key immediately** and propagates a `DEL`; it does not store an already-expired entry. - `EXPIREAT` takes seconds and `PEXPIREAT` milliseconds — mixing the units is the single most common bug here, and passing a millisecond value to `EXPIREAT` sets a deadline tens of thousands of years out, producing a key that never dies. Passing a seconds value to `PEXPIREAT` puts the deadline in 1970 and deletes the key instantly. Both failure modes are silent, so validate units at the client boundary.

  • Why does Redis propagate `PEXPIREAT` to replicas instead of the original `EXPIRE` command?
    Because a relative duration would be evaluated against the replica's own clock at the moment it applies the command, which is always later than the primary's execution. Every hop and every AOF replay would push the deadline further out, so replicas would keep keys the primary had already dropped and the datasets would diverge. Rewriting to an absolute millisecond instant makes every copy — replica, AOF replay, restarted node — agree on exactly the same deadline.
  • What happens to TTLs across a Redis restart?
    They survive, because RDB and AOF persist absolute deadlines rather than remaining durations: a key with ten minutes left still has roughly ten minutes left after the restart. Keys whose deadline passed during downtime are not loaded into the keyspace at all, so a long outage does not resurrect stale data. This is a direct consequence of the expires dictionary holding timestamps rather than countdowns.
  • How does system clock movement affect expiry, and what does that mean operationally?
    Expiry is evaluated against the server clock, so a backwards step makes keys live longer than intended and a forwards step expires a batch of keys at once — which downstream looks like a sudden cache stampede. Run NTP in slewing mode on Redis hosts rather than allowing steps, and be careful with VM snapshot restores. Note also that when the client computes the timestamp for `PEXPIREAT`, client-to-server clock skew becomes deadline error; relative `EXPIRE` avoids that because only the server clock is involved.

saying these in an interview costs you the question

  • Believing Redis stores a countdown and decrements it, rather than storing an absolute deadline.
  • Assuming TTLs restart or are lost on restart — absolute deadlines are persisted.
  • Thinking `EXPIRE` is replicated verbatim to replicas rather than rewritten as `PEXPIREAT`.
  • Mixing seconds and milliseconds between EXPIREAT and PEXPIREAT and not noticing the silent failure.
  • Expecting a past timestamp to store an already-expired entry rather than deleting the key immediately.

context