Redis Cheatsheet
Strings and Counters
Use this Redis reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Command table
The string is the default Redis type. It holds any byte sequence up to 512MB, so a "string" is equally a JSON blob, a protobuf, a JPEG, a counter, or a bitmap.
| Command | What it does | Complexity |
|---|---|---|
SET key val [opts] | Set, with the option block below | O(1) |
GET key | Value, nil if absent, error if not a string | O(1) |
GETSET key val | Set and return the old value (deprecated, use SET ... GET) | O(1) |
GETDEL key | Return the value and delete the key atomically (6.2+) | O(1) |
GETEX key [EX s / PERSIST] | Return the value and reset the TTL (6.2+) | O(1) |
SETNX key val | Set only if absent (legacy form of SET ... NX) | O(1) |
SETEX key sec val | Set with a TTL (legacy form of SET ... EX) | O(1) |
MSET k v [k v ...] | Set many, atomically | O(n) |
MGET k [k ...] | Get many in one round trip | O(n) |
MSETNX k v [k v ...] | Set many, only if none exist | O(n) |
APPEND key val | Append, return the new length | O(1) amortized |
STRLEN key | Length in bytes, 0 if absent | O(1) |
GETRANGE key start end | Substring by byte offset, negatives count back | O(n) substring |
SETRANGE key offset val | Overwrite from an offset, zero-padding as needed | O(n) |
LCS k1 k2 [LEN] [IDX] | Longest common subsequence of two strings (7.0+) | O(n×m) |
MGET is the single cheapest optimization in Redis. One MGET of 100 keys costs one
round trip, 100 GET calls cost 100. Note that MSET is atomic but has no conditional
form per key, and MSETNX is all-or-nothing across every key named.
SET options
SET absorbed most of the older string commands. Learn this block and you can forget
SETNX, SETEX, PSETEX, and GETSET.
| Option | Effect |
|---|---|
NX | Only if the key does not exist |
XX | Only if the key does exist |
GET | Return the old value alongside setting (6.2+), nil if there was none |
EX seconds | TTL in seconds |
PX milliseconds | TTL in milliseconds |
EXAT unix-seconds | Absolute expiry, seconds |
PXAT unix-milliseconds | Absolute expiry, milliseconds |
KEEPTTL | Keep the existing TTL instead of clearing it (6.0+) |
IDLE / no option | Default, clears any TTL |
SET cfg:flags '{"beta":true}' # OK SET cfg:flags '{"beta":false}' KEEPTTL # value changes, TTL survives SET cfg:flags '{"beta":true}' XX GET # returns the previous value SET lock:job:9 "$token" NX PX 30000 # the lock primitive SET s v EXAT 4000000000 # dies at a fixed wall-clock time
A plain SET clears the TTL. NX plus GET is the one combination worth memorizing
beyond that: it either claims the key or hands you the current owner's value, in a single
atomic step.
Counters
Counters are strings that Redis parses as a 64-bit signed integer on every write. There
is no separate integer type, and OBJECT ENCODING reports int for one.
| Command | What it does | Complexity |
|---|---|---|
INCR key | Add 1, return the new value | O(1) |
DECR key | Subtract 1 | O(1) |
INCRBY key n | Add a signed integer | O(1) |
DECRBY key n | Subtract a signed integer | O(1) |
INCRBYFLOAT key f | Add a float, long-double precision, no DECRBYFLOAT exists | O(1) |
INCR hu:views:post:42 # 1, a missing key is treated as 0 INCRBY hu:views:post:42 10 # 11 INCRBYFLOAT hu:balance 12.50 # "12.5", returned as a string DECRBY hu:stock:sku9 3 # can go negative, there is no floor
Three gotchas:
- The value must parse as an integer or you get
ERR value is not an integer or out of range. That includes a value with a leading
space or a stored float.
- The range is int64. Overflow is an error, not a wrap.
- INCRBYFLOAT returns a string and there is no negative counterpart, so subtract by
passing a negative argument.
A rate-limit counter, and its race
The textbook fixed-window limiter is INCR plus EXPIRE. Written naively it is broken.
# BROKEN: if the client dies between the two commands, or the EXPIRE # errors, the key is immortal and that caller is banned forever. INCR hu:rl:ip:1.2.3.4 EXPIRE hu:rl:ip:1.2.3.4 60
The gap is real, not theoretical. Two fixes, in increasing order of correctness:
# 1. Set the TTL unconditionally with NX, so it is applied on every # request but only takes effect on the first one. Still two commands, # but now the second one is idempotent and order-independent (7.0+). INCR hu:rl:ip:1.2.3.4 EXPIRE hu:rl:ip:1.2.3.4 60 NX
-- 2. One Lua script, genuinely atomic. Returns {allowed, remaining_ms}. -- EVAL <script> 1 hu:rl:ip:1.2.3.4 100 60000 local n = redis.call('INCR', KEYS[1]) if n == 1 then redis.call('PEXPIRE', KEYS[1], tonumber(ARGV[2])) end if n > tonumber(ARGV[1]) then return {0, redis.call('PTTL', KEYS[1])} end return {1, n}
A third form avoids Lua entirely by making the first write create the key with its TTL:
SET hu:rl:ip:1.2.3.4 1 NX EX 60 # claims the window, or returns nil INCR hu:rl:ip:1.2.3.4 # only reached when the SET returned nil
All three are fixed window, so a caller can send the full quota at 0:59 and again at 1:00. Sliding-window and token-bucket variants that fix that live in the patterns topic.
Bitmaps
A bitmap is not a type, it is bit-level access to a string. At one bit per user id, a million daily-active users cost 125KB, which is why this is the standard trick for activity tracking.
| Command | What it does | Complexity |
|---|---|---|
SETBIT key offset 0/1 | Set one bit, return the old bit | O(1) |
GETBIT key offset | Read one bit, 0 past the end | O(1) |
BITCOUNT key [start end [BYTE/BIT]] | Count set bits, BIT ranges are 7.0+ | O(n) |
BITPOS key 0/1 [start end [BYTE/BIT]] | First offset holding that bit, -1 if none | O(n) |
BITOP AND/OR/XOR/NOT dst k [k ...] | Bitwise op across keys into dst | O(n) |
BITFIELD key ops... | Read and write packed integer fields | O(1) per op |
BITCOUNT on a missing key | 0, not an error | O(1) |
# Daily actives: one key per day, one bit per user id SETBIT hu:dau:2026-09-15 1042 1 SETBIT hu:dau:2026-09-14 1042 1 BITCOUNT hu:dau:2026-09-15 # today's active count # Retention: users active on BOTH days BITOP AND hu:dau:both 2 hu:dau:2026-09-15 hu:dau:2026-09-14 BITCOUNT hu:dau:both # Weekly actives: OR seven daily keys BITOP OR hu:wau:w37 7 hu:dau:2026-09-09 hu:dau:2026-09-10 hu:dau:2026-09-11 GETBIT hu:dau:2026-09-15 1042 # was this user active? 1 BITPOS hu:dau:2026-09-15 1 # lowest active id
SETBIT at a high offset allocates the whole string, so SETBIT k 4294967295 1
immediately costs 512MB. Bitmaps only work when your ids are dense and small. Sparse or
UUID ids want a set or a HyperLogLog instead.
BITFIELD packs several small counters into one string, with explicit overflow control.
BITFIELD bf SET u8 0 255 GET u8 0 INCRBY u8 0 10 OVERFLOW SAT INCRBY u8 0 10 # 1) (integer) 0 old value from SET # 2) (integer) 255 GET # 3) (integer) 9 default OVERFLOW WRAP wrapped 265 -> 9 # 4) (integer) 19 SAT would clamp at 255, here it just adds
Types are u1–u63 and i1–i64, offsets are bit positions or #n for the n-th field
of that width. OVERFLOW is WRAP (default), SAT (clamp), or FAIL (return nil).
When a string is the wrong choice
| Storing | Use instead | Why |
|---|---|---|
| An object you update field by field | Hash | Avoids read, decode, encode, write |
| A JSON blob you only ever read whole | String | Simplest, one round trip |
| Anything over a few MB | Split it | One 100MB value blocks the loop on every read |
| Millions of tiny keys | One hash per bucket | Per-key overhead dominates at ~50-100 bytes |
| A counter you need sorted | Sorted set | ZINCRBY gives you ranking for free |
| Unique-visitor counts | HyperLogLog | 12KB fixed, 0.81 percent error |
The 512MB ceiling is a hard limit, but the practical one is far lower. Redis is single threaded, so a single large value serializes every other client behind it. Keep values under roughly 100KB and the tail latency stays flat.