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.

CommandWhat it doesComplexity
SET key val [opts]Set, with the option block belowO(1)
GET keyValue, nil if absent, error if not a stringO(1)
GETSET key valSet and return the old value (deprecated, use SET ... GET)O(1)
GETDEL keyReturn 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 valSet only if absent (legacy form of SET ... NX)O(1)
SETEX key sec valSet with a TTL (legacy form of SET ... EX)O(1)
MSET k v [k v ...]Set many, atomicallyO(n)
MGET k [k ...]Get many in one round tripO(n)
MSETNX k v [k v ...]Set many, only if none existO(n)
APPEND key valAppend, return the new lengthO(1) amortized
STRLEN keyLength in bytes, 0 if absentO(1)
GETRANGE key start endSubstring by byte offset, negatives count backO(n) substring
SETRANGE key offset valOverwrite from an offset, zero-padding as neededO(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.

OptionEffect
NXOnly if the key does not exist
XXOnly if the key does exist
GETReturn the old value alongside setting (6.2+), nil if there was none
EX secondsTTL in seconds
PX millisecondsTTL in milliseconds
EXAT unix-secondsAbsolute expiry, seconds
PXAT unix-millisecondsAbsolute expiry, milliseconds
KEEPTTLKeep the existing TTL instead of clearing it (6.0+)
IDLE / no optionDefault, 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.

CommandWhat it doesComplexity
INCR keyAdd 1, return the new valueO(1)
DECR keySubtract 1O(1)
INCRBY key nAdd a signed integerO(1)
DECRBY key nSubtract a signed integerO(1)
INCRBYFLOAT key fAdd a float, long-double precision, no DECRBYFLOAT existsO(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.

CommandWhat it doesComplexity
SETBIT key offset 0/1Set one bit, return the old bitO(1)
GETBIT key offsetRead one bit, 0 past the endO(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 noneO(n)
BITOP AND/OR/XOR/NOT dst k [k ...]Bitwise op across keys into dstO(n)
BITFIELD key ops...Read and write packed integer fieldsO(1) per op
BITCOUNT on a missing key0, not an errorO(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

StoringUse insteadWhy
An object you update field by fieldHashAvoids read, decode, encode, write
A JSON blob you only ever read wholeStringSimplest, one round trip
Anything over a few MBSplit itOne 100MB value blocks the loop on every read
Millions of tiny keysOne hash per bucketPer-key overhead dominates at ~50-100 bytes
A counter you need sortedSorted setZINCRBY gives you ranking for free
Unique-visitor countsHyperLogLog12KB 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.