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?
answer
- NX = only if no TTL; XX = only if a TTL exists
- GT = later only; LT = earlier only
- no TTL == infinite ⇒ GT never sets, LT always does
- GT for sliding sessions and lease renewal
- NX for fixed-window rate limits (INCR + EXPIRE NX)
basics
~20 sNX 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.
solid answer
~60 s`EXPIRE key seconds [NX | XX | GT | LT]` (also on PEXPIRE/EXPIREAT/PEXPIREAT, Redis 7.0+) makes the expiry write conditional, returning `1` if applied and `0` if the condition failed. - **NX** — set only when the key currently has **no** expiry. - **XX** — set only when an expiry already exists. - **GT** — set only if the new deadline is **greater** (later) than the current one. - **LT** — set only if it is **less** (earlier). The key subtlety: a key without an expiry is treated as having an **infinite** TTL. So `GT` never sets an expiry on a persistent key, while `LT` always does. `NX` is mutually exclusive with the other three; `GT` and `LT` cannot be combined. What they buy you is atomicity. `GT` extends a lock lease or session without ever shortening it — no read-then-compare race where a slow client rolls the deadline backwards. `NX` sets a fixed rate-limit window exactly once, so concurrent `INCR` callers cannot keep resetting it. Previously both required Lua or WATCH.
code
text · 13 lines> SET s:42 "payload" EX 1800
OK
> EXPIRE s:42 600 GT # 600s is earlier than the current 1800s
(integer) 0 # rejected: would have shortened the session
> EXPIRE s:42 3600 GT # later deadline
(integer) 1
> SET perm "x" # no expiry at all
OK
> EXPIRE perm 60 GT
(integer) 0 # no TTL is treated as infinite
> EXPIRE perm 60 LT
(integer) 1 # any finite deadline is less than infinitego deeper
Know that the four flags make an expiry write conditional and that the command returns 1 when applied, 0 when skipped.
Explain each flag precisely, including the treatment of a missing expiry as infinite, and give the rate-limit and session-refresh use cases.
Argue from atomicity: name the races these flags remove, the Lua scripts they replace on 6.x, and the new ambiguity in the 0 reply.
Position them as server-side invariants — the deadline monotonicity a lease or session needs is enforced at the data store rather than trusted to every client.
## The commands and the flags Since Redis 7.0 all four expiry-setting commands — `EXPIRE`, `PEXPIRE`, `EXPIREAT`, `PEXPIREAT` — accept one optional condition: ``` EXPIRE key seconds [NX | XX | GT | LT] ``` The reply is the usual `1` (timeout was set) or `0` (it was not). Before 7.0, `0` meant only "the key does not exist"; now it also means "the condition was not satisfied", so code that interprets the reply must account for both. - **NX** — apply only if the key currently has *no* expiry. - **XX** — apply only if the key *already has* an expiry. - **GT** — apply only if the new expiry is strictly later than the existing one. - **LT** — apply only if the new expiry is strictly earlier than the existing one. `NX` cannot be combined with `XX`, `GT` or `LT`; `GT` and `LT` are mutually exclusive. `GT`/`LT` may be combined with `XX` ("only if it already has an expiry, and only if later/earlier"). ## The infinity rule The one thing candidates get wrong: **a key with no expiry is treated as if its TTL were infinite.** Follow that through: - `EXPIRE key 60 GT` on a persistent key → `0`, nothing set. Nothing is greater than infinity. - `EXPIRE key 60 LT` on a persistent key → `1`, the expiry is set. Any finite deadline is less than infinity. This makes `GT` safe as a pure "extend, never shorten" operation and makes `LT` a "cap the lifetime" operation that also bounds previously-permanent keys. If you want "extend, but also give a permanent key its first deadline", you need `GT` plus a separate `NX` call, or a small script — one flag cannot express both. ## What they replace Every one of these was previously a read-modify-write across two round trips, which is racy under concurrency: **Sliding session that must never shrink.** Two application nodes both refresh a session; node A computes a 30-minute deadline, node B a 25-minute one, and whichever lands last wins — so a user can be logged out early. `EXPIRE session:42 1800 GT` makes the shortening physically impossible: the server compares deadlines atomically and ignores the smaller one. **Lock lease extension.** A holder renewing its lease must extend it, never contract it, and must not resurrect a lease it has already lost. `PEXPIRE lock:orders 30000 XX GT` renews only while an expiry still exists and only forwards. (Correctness of distributed locking overall is a broader topic; the point here is that the renewal step itself is now a single atomic command.) **Fixed-window rate limiting.** The classic bug is `INCR key` followed by an unconditional `EXPIRE key 60`: every request pushes the window forward, so a steady stream of traffic keeps the counter alive forever and the limit never resets. Using `EXPIRE key 60 NX` sets the window exactly once, when the counter is first created; subsequent requests leave it alone and the counter dies on schedule. Before 7.0 this needed either a check of `INCR`'s return value being 1 (still two round trips and racy on the failure path) or a Lua script. **Capping a TTL.** Policy says nothing in this namespace may live longer than an hour. `EXPIRE key 3600 LT` shortens anything longer and, thanks to the infinity rule, also puts a deadline on keys that had none. ## Pipelining and scripting Because each flag makes a decision the server evaluates atomically, these commands are safe to pipeline: you no longer need the response of a read before deciding what to write. That removes a round trip from hot paths such as per-request session refresh, and it removes the Lua script many teams carried for exactly this purpose. If you are on Redis 6.x or earlier, the equivalent is a two-line Lua script comparing `PTTL` against the desired deadline — atomic, but more code and a script cache to manage. ## Reply handling Because `0` is now ambiguous between "no such key" and "condition not met", pair the call with a `TTL`/`EXISTS` check when the distinction matters — for instance, a lock renewal returning `0` might mean the lock was lost (key gone) or that a longer lease already exists, and those warrant different behavior.
- Why does `EXPIRE key 60 GT` return 0 on a key that currently has no expiry?Redis treats a key without an expiry as having an infinite TTL, and `GT` applies the new deadline only if it is strictly greater than the current one. Nothing finite is greater than infinity, so the condition fails and nothing is written. If you need "extend if present, otherwise set a first deadline", combine a `GT` call with an `NX` call or use a short Lua script.
- How did teams implement 'extend but never shorten' before Redis 7.0?Either a `PTTL` read followed by a conditional `PEXPIRE`, which is racy because another client can change the deadline in between, or a two-line Lua script that reads `PTTL` and calls `PEXPIRE` only when the new deadline is larger — atomic because the script runs as a single unit. The 7.0 `GT` flag replaces both with one round trip and no script cache to manage.
- What ambiguity does the `0` reply introduce, and how do you handle it?Before 7.0, `0` from EXPIRE meant only that the key did not exist; with condition flags it also means the condition was not satisfied. For a lock renewal those cases differ sharply — a missing key means the lease was lost and the holder must stop working, whereas a rejected `GT` means a longer lease already exists and is harmless. Follow up with `EXISTS` or `PTTL` when the distinction drives behavior.
saying these in an interview costs you the question
- Expecting `GT` to set an expiry on a key that currently has none.
- Believing `NX` means 'set only if the key does not exist' — it is about the expiry, not the key.
- Combining `NX` with `GT` or `XX` and expecting Redis to accept it.
- Reading a `0` reply as 'the key is gone' when it may just mean the condition failed.
- Claiming these flags exist in Redis 6.x, or that `SET`'s NX/XX behave the same way.