skip to content

EXPIRE & TTL Mechanics

You will learn the full TTL command surface — setting, inspecting, and clearing expirations, plus the conditional NX/XX/GT/LT flags — and the classic gotcha that overwriting a value silently drops its TTL. Interviewers use that gotcha as a quick screen for hands-on Redis experience.

part ofRedisoverview, primer and where to startread it →
on this pageshow

questions

5

What do the Redis commands `TTL` and `PTTL` return for a key that exists but has no expiry, versus a key that does not exist at all — and how do you remove an expiry without deleting the key?

level: juniorimportance: must knowfreq 58%

answer

  1. -1 = exists, no expiry
  2. -2 = key does not exist
  3. PTTL = same in milliseconds
  4. PERSIST → 1 removed / 0 nothing to remove
  5. EXPIRE with 0 or negative deletes the key

basics

~20 s

TTL returns the remaining seconds, -1 if the key exists with no expiry, and -2 if the key does not exist. PTTL is the same in milliseconds. PERSIST key removes the expiry and keeps the value, returning 1 if an expiry was removed, 0 otherwise.

solid answer

~50 s

`TTL key` returns a non-negative integer for the remaining seconds, and two sentinel values: - **`-1`** — the key exists but has no associated expiry (it is permanent). - **`-2`** — the key does not exist (either never created, or already expired and reclaimed). `PTTL` is identical but in milliseconds, which matters when TTLs are sub-second or when you need precision before re-applying one. The `-1` vs `-2` split exists since Redis 2.8; older versions returned `-1` for both, which is why some legacy client code treats "no TTL" and "missing key" as the same case. To drop an expiry without losing the value, use `PERSIST key`: it returns `1` if an expiry was removed and `0` if the key was missing or already permanent. `EXPIRE`/`PEXPIRE` similarly return `1` if the timeout was set and `0` if it was not (key missing, or a condition flag like `NX`/`GT` was not satisfied). A non-positive expiry deletes the key immediately rather than storing a past deadline.

code

text · 23 lines
text
> SET a 1
OK
> TTL a
(integer) -1        # exists, no expiry

> EXPIRE a 60
(integer) 1         # timeout was set
> TTL a
(integer) 60
> PTTL a
(integer) 59994     # millisecond precision

> PERSIST a
(integer) 1         # expiry removed, value kept
> PERSIST a
(integer) 0         # nothing left to remove

> TTL nosuchkey
(integer) -2        # key does not exist
> EXPIRE a -1
(integer) 1
> TTL a
(integer) -2        # a non-positive expiry deleted the key

go deeper

for a junior

Recite the three outcomes (remaining seconds, -1 no expiry, -2 no key) and that PERSIST clears an expiry while keeping the value.

for a middle

Add PTTL precision, the 1/0 return convention shared by EXPIRE and PERSIST, and that a non-positive expiry deletes the key.

for a senior

Use the -1 vs -2 distinction diagnostically — auditing a cache namespace for lost TTLs — and note the interaction with volatile eviction pools.

for a principal

Treat these as a contract the application layer should surface: distinguish miss from permanent-key in the client so TTL-loss bugs are observable rather than invisible.

## The return values `TTL key` answers "how long does this key have left?" and folds two exceptional cases into negative sentinels: | Return | Meaning | |---|---| | `n ≥ 0` | key exists and expires in about `n` seconds | | `-1` | key exists, **no expiry set** — it lives until deleted or evicted | | `-2` | **key does not exist** — never created, or already expired/deleted | `PTTL key` is the millisecond version with the same sentinels. Use it whenever you need precision: `TTL` rounds, so a key with 900 ms left reports `1`, and re-applying a TTL from a rounded `TTL` read slowly inflates or deflates deadlines. The distinction between `-1` and `-2` is genuinely useful in application code: `-1` on a key you believed was a cache entry is a bug signal (somebody overwrote it in a way that dropped the expiry), whereas `-2` is simply a miss. Conflating them hides the first case entirely. ## Historical note Before Redis 2.8, `TTL` returned `-1` for both "no expiry" and "no such key". Any code or tutorial that treats `-1` as "missing" is written against that old behavior. On any supported version today, check for `-2` to mean missing. ## Removing an expiry `PERSIST key` strips the expiry and leaves the value in place: - returns `1` when an expiry existed and was removed, - returns `0` when the key does not exist or had no expiry. This is how you promote a temporary key to a permanent one — for example, a draft that becomes a saved document, or a provisional lock entry you decide to keep. From Redis 6.2 you can also do it during a read with `GETEX key PERSIST`, which returns the value and clears the expiry atomically. Beware the interaction with eviction: making a key permanent removes it from the candidate pool of any `volatile-*` `maxmemory-policy`, so `PERSIST` on many keys can quietly shrink what Redis is allowed to evict. ## Related return conventions The expiry commands share a consistent reply style, which interviewers like to probe: - `EXPIRE key seconds` / `PEXPIRE` / `EXPIREAT` / `PEXPIREAT` → `1` if the timeout was set, `0` if not. "Not" means the key does not exist, or (Redis 7.0+) a supplied `NX`/`XX`/`GT`/`LT` condition was not met. - `PERSIST` → `1` / `0` as above. - `SET key value EX n` → the usual `OK`; it does not report anything about a previous TTL, which is exactly why the silent TTL-clearing behavior of plain `SET` surprises people. ## Non-positive expiries delete `EXPIRE key 0`, `EXPIRE key -1`, or an `EXPIREAT` with a timestamp already in the past does not store a weird deadline — Redis deletes the key straight away (propagating a `DEL` to replicas and the AOF). So computing a TTL as `deadline - now` and passing a negative result is a delete, not a no-op. Guard the arithmetic, or you will remove keys you meant to leave alone. ## Reading TTLs in practice `TTL`/`PTTL` are O(1) and safe to call in hot paths and in monitoring. Two common uses: 1. **Spot-checking cache hygiene.** `SCAN` over a prefix and `TTL` each key; any `-1` in a namespace that should be entirely volatile means some write path dropped the expiry. 2. **Debugging session or lock lifetime.** `PTTL lock:orders:42` tells you exactly how much lease a lock holder has left, which is far more useful than the second-rounded `TTL` when leases are short. One caution: `TTL` reflects the key's logical state. A key whose deadline has passed but which has not yet been physically reclaimed still reports `-2` and behaves as missing, because Redis checks expiry before answering. You never observe a negative-but-alive key.

  • When should you prefer `PTTL` over `TTL`?
    Whenever sub-second precision matters — short lock leases, sub-second caches, or any code that reads a remaining TTL and re-applies it elsewhere. `TTL` rounds to whole seconds, so repeatedly reading and re-applying it drifts the real deadline. `PTTL` also makes it possible to distinguish a key with 50 ms left from one with 950 ms, which `TTL` reports identically as 1.
  • What does `EXPIRE key 0` do?
    It deletes the key immediately rather than storing a zero or past deadline, and it returns 1 to indicate the operation applied. The same is true for any negative TTL or an `EXPIREAT` timestamp already in the past. This matters when a TTL is computed as `deadline - now`: if the deadline has passed, you are issuing a delete, so guard the arithmetic before sending the command.
  • You find keys in a cache namespace returning `TTL` of -1. What does that tell you?
    Those keys exist but carry no expiry, so something wrote them without one — most often a refresh path using a plain `SET`, which discards the previous expiry. They will never be reclaimed by expiration, and under a `volatile-*` eviction policy they are not even eviction candidates, so they grow the instance's floor permanently. The fix is at the write path: require an expiry in the cache client, or use `SET … KEEPTTL` on refresh.

saying these in an interview costs you the question

  • Reading `-1` as "key does not exist" — that is `-2` on any modern Redis.
  • Assuming `TTL` returns 0 for an expired key; expired keys report -2 and behave as missing.
  • Thinking `PERSIST` deletes the key instead of just removing its expiry.
  • Expecting `EXPIRE key 0` to be a harmless no-op rather than an immediate delete.
  • Round-tripping a TTL through the second-precision `TTL` command when milliseconds matter.

context

open as a page

In Redis, a key was given an expiry and the application later updates its value; sometimes the expiry survives and sometimes it silently disappears. What is the rule, and what does the `KEEPTTL` option of the `SET` command do?

level: middleimportance: must knowfreq 60%

basics

~20 s

Commands that replace the whole value — notably SET — clear the TTL, making the key permanent. Commands that modify a value in place (APPEND, INCR, LPUSH, HSET, SADD, ZADD) keep it. SET key value KEEPTTL (Redis 6.0+) replaces the value while retaining the existing expiry.

open as a page

Redis 7.0 added the `NX`, `XX`, `GT` and `LT` options to the `EXPIRE` command family. What does each one do, and which real problems do they solve that a plain `EXPIRE` cannot?

level: middleimportance: should knowfreq 34%

basics

~20 s

NX sets the expiry only if the key has none; XX only if it already has one; GT only if the new deadline is later than the current one; LT only if it is earlier. A key with no expiry counts as infinite, so GT never applies to it and LT always does.

open as a page

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%

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.

open as a page

How does Redis keep track of which keys carry an expiry, what does that bookkeeping cost, and can an individual field inside a hash expire on its own?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

Each database holds a second dictionary, expires, mapping keys that have a TTL to an absolute millisecond deadline — so a volatile key costs an extra hash-table entry. Expiry is per top-level key; Redis 7.4 added HEXPIRE for per-field hash TTLs, and before that you emulated it.

open as a page