skip to content

Strings & Bitmaps

You will learn the workhorse string type — atomic counters, flags with NX/XX, binary-safe values — and the bit-level view that turns one key into millions of booleans. Interviewers ask because INCR-based counters and bitmap presence tracking are the two cheapest tricks in the Redis playbook.

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

questions

6

The Redis SET command accepts NX, XX, EX/PX, KEEPTTL and GET options. What does each of them do, and why is `SET key value NX EX 30` preferred over calling SETNX and then EXPIRE?

level: juniorimportance: must knowfreq 72%

answer

  1. NX = create-only, XX = update-only, never both
  2. plain SET wipes the TTL — KEEPTTL keeps it
  3. EX/PX relative, EXAT/PXAT absolute
  4. SETNX+EXPIRE = immortal key on client crash
  5. SET ... GET replaces GETSET; GETDEL/GETEX are 6.2

basics

~20 s

NX writes only if the key is absent, XX only if it already exists, EX/PX attach a TTL in seconds/milliseconds, KEEPTTL keeps the current TTL, GET returns the previous value. A single SET is one atomic command; SETNX then EXPIRE can leave a key with no TTL if the client dies between them.

solid answer

~50 s

`SET` overwrites unconditionally by default and **discards any existing TTL**. The options change that: - **NX** — set only if the key does not exist; **XX** — only if it does. They are mutually exclusive. - **EX s / PX ms** (and **EXAT / PXAT** for absolute unix times) — set value and expiry in one step. - **KEEPTTL** — write the new value but retain the remaining TTL. - **GET** — return the old value (or nil) alongside the write, replacing GETSET. When the condition fails, SET returns nil and writes nothing. `SET k v NX EX 30` is a single command, so it is atomic on Redis's execution loop: either the key is created *with* its TTL or nothing happens. `SETNX` + `EXPIRE` are two round trips; a crash, timeout or failover between them leaves an immortal key — the classic "my lock never expired" bug. The same reasoning is why blindly re-SETting a cached value without KEEPTTL silently makes it permanent.

code

text · 16 lines
text
> SET job:42 running NX EX 30
OK
> SET job:42 running NX EX 30      # already there → no write
(nil)
> TTL job:42
(integer) 27
> SET job:42 finished             # value replaced AND ttl dropped
OK
> TTL job:42
(integer) -1                       # -1 = no expiry, -2 = no key
> SET job:42 finished KEEPTTL      # would have preserved it
OK
> SET job:42 archived GET          # write + return previous value
"finished"
> GETEX job:42 EX 60               # read and (re)arm the TTL
"archived"

go deeper

for a junior

Be able to state what NX, XX, EX/PX and KEEPTTL each do and that a plain SET wipes the TTL.

for a middle

Explain the atomicity argument: one command executes to completion, so SETNX+EXPIRE has a crash window that SET NX EX does not.

for a senior

Connect it to real failure modes — immortal lock keys, caches that stop expiring after a refresh path was added — and to the modern GETEX/GETDEL/SET GET replacements for legacy commands.

for a principal

Frame it as an API-design rule: prefer server-side atomic primitives over client-side command sequences, and treat any multi-command read-modify-write as requiring a script or transaction by policy.

## The string is the default type A Redis string is just a binary-safe byte array up to 512 MB — text, JSON, a serialized object or raw bytes. `SET key value` stores it, `GET key` reads it back. Two behaviours of the bare form surprise people: it **overwrites any existing value regardless of type**, and it **removes any TTL the key had**. Everything below is about controlling those two behaviours in a single atomic command. ## The option matrix **Existence conditions.** `NX` ("not exists") performs the write only when the key is absent; `XX` performs it only when the key already exists. They cannot be combined — Redis replies with a syntax error. When the condition is not met, the command performs no write and replies with a nil, which is how you distinguish "I created it" from "someone else already had it". **Expiry.** `EX seconds` and `PX milliseconds` set a relative TTL; `EXAT` / `PXAT` set an absolute unix timestamp (seconds / milliseconds). Without any of them the key is written with no expiry. `KEEPTTL` means "replace the value but leave the existing expiry ticking" — useful when you refresh a cached payload and want the original deadline honoured rather than extended. **Reading while writing.** The `GET` option makes SET return the previous value (nil if there was none) while still applying the write, so a read-and-replace is one round trip. This supersedes the legacy `GETSET`. Its siblings are `GETDEL` (read and remove atomically) and `GETEX` (read and, in the same command, set/extend a TTL or `PERSIST` it away). ## Why one command beats two Redis executes each command to completion before starting the next, so any single command is atomic with respect to other clients. Two commands are not: another client's commands can interleave, and — more dangerously — **your own process may never issue the second one**. The failure sequence with `SETNX lock 1` followed by `EXPIRE lock 30` is: SETNX succeeds, the client crashes / the TCP connection drops / a GC pause outlives the socket timeout, and the key now exists forever with no expiry. Every subsequent attempt to acquire fails, and nothing ever cleans it up. `SET lock token NX EX 30` cannot land in that state: the key is created with the expiry or not created at all. The same atomicity argument applies to `GET` + `SET` used as read-modify-write: the read and write are separate, so two clients can both read the old value and both write. For that you either use a purpose-built atomic command (INCR family, SET with GET) or a WATCH/Lua construct — never a client-side round trip pair. ## The TTL-discard trap Because plain SET clears the TTL, a cache-refresh path written as `SET user:42 <fresh json>` turns an entry that used to expire into a permanent one. On a small key this is invisible; across millions of keys it is an unbounded memory growth that only stops when the eviction policy starts throwing things out (or the instance hits its limit and starts erroring). Either re-state the TTL (`EX 300`) or use `KEEPTTL` deliberately. Conversely, commands that only *modify* a value — INCR, APPEND, SETRANGE, GETSET's replacement `SET ... GET` — differ here: INCR and APPEND leave the TTL alone, while a full SET resets it. Knowing which commands preserve expiry is the practical takeaway. ## Return values worth memorising - `SET` without conditions → simple string `OK`. - `SET ... NX` / `XX` that did not fire → nil. - `SET ... GET` → old value or nil, whether or not the conditional write fired. - `SETNX` → 1 or 0 (integer), which is why old code reads awkwardly next to modern SET. ## Practical guidance Treat `SETNX`, `SETEX`, `PSETEX` and `GETSET` as legacy spellings kept for compatibility; write everything as one `SET` with options. Reach for `KEEPTTL` when refreshing content under a fixed deadline, for `EXAT` when the deadline comes from business time rather than from now, and for `SET ... GET` when you need the displaced value (for example to detect who held a slot before you). If your logic genuinely needs several keys changed together, that is a Lua script or a transaction — not a sequence of SETs.

  • What happens to an existing TTL when you SET the same key again without KEEPTTL?
    The TTL is discarded and the key becomes persistent. Redis treats a full SET as creating a brand-new value, so any expiry set earlier is dropped. This is a common cause of caches that quietly stop expiring; either restate EX on every write or pass KEEPTTL. Note that INCR, APPEND and SETRANGE do not clear the TTL — only a full SET does.
  • How do you atomically read a value and extend its lifetime in one round trip?
    Use GETEX: `GETEX session:abc EX 900` returns the value and re-arms the TTL to 900 seconds in a single command. `GETEX key PERSIST` removes the expiry instead, and `GETDEL key` reads and deletes atomically. Before Redis 6.2 you needed GET followed by EXPIRE, which is two commands and therefore not atomic.
  • How would you tell whether your SET with NX actually performed the write?
    By the reply: a conditional SET that did not fire returns nil, while a successful one returns OK. Do not re-read the key to check — between your write and your read another client may have changed it, so the reply is the only trustworthy signal. If you also need the value that was there before, add the GET option.

saying these in an interview costs you the question

  • Assuming SET preserves the key's existing TTL
  • Claiming SETNX followed by EXPIRE is safe because the two commands are fast
  • Combining NX and XX in one SET and expecting 'either' semantics
  • Reading EX as milliseconds and PX as seconds
  • Thinking a failed conditional SET raises an error rather than returning nil

context

open as a page

Why is Redis's INCR the correct way to implement a counter shared by many clients, and what happens if the key is missing, holds non-numeric text, or is incremented past the 64-bit range?

level: middleimportance: must knowfreq 68%

basics

~20 s

INCR performs the read-modify-write inside Redis as one command, so concurrent clients cannot lose updates. A missing key is treated as 0 and created. A non-numeric or out-of-range value returns an error rather than resetting. Values are signed 64-bit; overflow errors instead of wrapping. INCR keeps any existing TTL.

open as a page

Redis has no dedicated bitmap type — SETBIT, GETBIT and BITCOUNT operate on ordinary strings. How does that work, how much memory does a daily-active-user flag for 50 million user ids cost, and what is the trap when the bit offsets are sparse?

level: middleimportance: should knowfreq 42%

basics

~20 s

A bitmap is just a string addressed bit by bit. SETBIT key offset 0|1 sets a bit and returns the old one; BITCOUNT counts set bits. 50 million bits is about 6 MB. The trap: setting a high offset allocates every byte below it, so a single sparse id can allocate hundreds of MB.

open as a page

Redis strings can be modified in place with APPEND, SETRANGE and read partially with GETRANGE. How do these commands behave — including what SETRANGE does when you write past the end of the value — and what are the limits of treating a Redis string as a growable buffer?

level: middleimportance: should knowfreq 36%

basics

~20 s

A Redis string is a mutable byte array. APPEND adds bytes to the end and returns the new length. SETRANGE overwrites bytes at an offset, zero-filling any gap. GETRANGE returns a byte slice and accepts negative indexes. Max length is 512 MB, and rewriting huge values gets expensive.

open as a page

Redis's BITFIELD command lets you treat a string as an array of arbitrary-width integers with u8, i5 or similar types. When is that worth using instead of ordinary keys, and what do the OVERFLOW WRAP, SAT and FAIL modes control?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

BITFIELD packs many small integers into one string, addressed by bit offset and width (u1..u63, i1..i64). It runs GET/SET/INCRBY operations atomically in one command. OVERFLOW chooses what happens on wrap: WRAP wraps around, SAT clamps at the limit, FAIL returns nil and skips the write.

open as a page

You keep one Redis bitmap per day with a bit set for every active user. How would you use BITOP and BITPOS to answer questions like 'how many users were active on all seven days of last week', and what does that computation cost on the server?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

BITOP AND/OR/XOR/NOT combines bitmaps into a destination key, then BITCOUNT gives the answer: AND of seven day keys counts users active every day. It is O(total bytes) and writes a full-size result key, so batch it off the hot path, give the result a TTL, and mind memory.

open as a page