Skip to content

Redis CLI 치트시트

In-memory data structure store - CLI commands and usage.

01

Getting Started

Connect to Redis Server

redis-cli is the standard command-line client. Default connects to 127.0.0.1:6379. Use -h for host, -p for port, -a for password (or set REDISCLI_AUTH env var to avoid leaking secrets in shell history). TLS needs --tls plus cert/key. URL scheme redis:// is supported in Redis 6+, rediss:// for TLS.

redis-cli
# connect to local server (default port 6379)
redis-cli

# connect to a remote host with custom port
redis-cli -h redis.example.com -p 6380

# connect with authentication
redis-cli -a "yourpassword"
redis-cli -h host -p 6379 -a "yourpassword"

# connect over TLS
redis-cli --tls --cert client.crt --key client.key -h host -p 6379

# connect using a URL (Redis 6+)
redis-cli redis://user:password@host:6379/0
redis-cli rediss://user:password@host:6379/0  # TLS

Connection Options & Mode

redis-cli supports both interactive and one-shot modes. Passing a command as arguments runs it and exits. Pipe mode reads RESP/text commands from stdin — great for scripting and bulk loads. -n selects the logical database. --stat and --latency are built-in monitoring helpers. --raw outputs values without type prefixes, useful for piping.

redis-cli
# select a specific database index (0-15)
redis-cli -n 1            # use database 1

# run a single command and exit (non-interactive)
redis-cli GET mykey
redis-cli -h host PING

# pass command with arguments
redis-cli SET counter 100
redis-cli INCR counter

# read commands from stdin (pipe mode)
echo "SET k1 v1" | redis-cli
cat commands.txt | redis-cli

# repeat a command every second (watch mode)
redis-cli --stat
redis-cli --latency

# pretty-print JSON output (Redis 6+)
redis-cli --raw GET user:json

PING & Connectivity Test

PING is the simplest health check — a working Redis returns PONG. --latency continuously measures round-trip time (useful for diagnosing network issues). --latency-dist shows a histogram. --latency-history logs rolling stats. Use these to baseline your connection quality before performance tuning.

redis-cli
# basic connectivity test
redis-cli ping
# PONG

# ping with a message (echoed back)
redis-cli PING "hello"
# "hello"

# test auth + connection in one shot
redis-cli -a password PING

# check round-trip latency (continuous)
redis-cli --latency
# output: min: 0, max: 1, avg: 0.27 (3421 samples)

# latency histogram
redis-cli --latency-dist

# latency history (rolling, every 15s by default)
redis-cli --latency-history -i 1

Interactive Mode Tips

Interactive mode is a REPL for Redis commands. HELP <command> or HELP @<group> shows built-in docs (no internet needed). CONNECT switches servers in-session. CLEAR clears the screen. The CLI auto-detects multi-line input for quoted strings. Prefix ':' to run CLI-specific commands (like :raw) vs Redis commands.

redis-cli
# start interactive mode
redis-cli

# inside the CLI:
127.0.0.1:6379> HELP SET       # command help
127.0.0.1:6379> HELP @string   # help for a command group
127.0.0.1:6379> SCAN 0         # any Redis command

# clear the screen
127.0.0.1:6379> CLEAR

# connect to a different server without restarting
127.0.0.1:6379> CONNECT host 6380

# exit
127.0.0.1:6379> EXIT
127.0.0.1:6379> QUIT

# multi-line input: end with a backslash
127.0.0.1:6379> SET mykey "this is \
a multi-line value"

# toggle raw output mode
127.0.0.1:6379> :raw

Database Selection & Basics

Redis databases are logical namespaces (0-15) sharing the same memory — not isolated schemas. Most apps use only db 0. FLUSHDB clears the current db; FLUSHALL clears everything (be careful!). Multiple databases are discouraged in favor of separate instances or key prefixes, since they share a single thread.

redis-cli
# Redis has 16 logical databases (0-15) by default
SELECT 0     # switch to database 0
SELECT 15    # switch to database 15

# move a key to another database
MOVE mykey 1

# flush the current database (DESTRUCTIVE)
FLUSHDB

# flush ALL databases (VERY DESTRUCTIVE)
FLUSHALL

# get the number of keys in the current db
DBSIZE

# swap two databases (Redis 4.0+)
SWAPDB 0 1

Help & Command Discovery

COMMAND introspects the server's command table — useful for discovering capabilities and arity. COMMAND DOCS (7.0+) returns structured documentation. The CLI's HELP command shows offline docs grouped by category (@string, @hash, @server, etc.). Run 'redis-cli --help' for all CLI flags and options.

redis-cli
# list all commands (huge output)
COMMAND

# get details of a specific command
COMMAND INFO GET
COMMAND INFO SET HSET LPUSH

# structured documentation (Redis 7+)
COMMAND DOCS SET
COMMAND DOCS @string

# count available commands
COMMAND COUNT

# CLI help
redis-cli --help
redis-cli --version

# inside interactive mode
HELP
HELP @server
HELP @string
HELP @hash
HELP GETSET
02

Strings

Basic SET & GET

SET/GET are the fundamental string operations. NX = set if not exists (used for locks); XX = set if exists. EX/PX/EXAT/PXAT set expiration atomically (no race between SET and EXPIRE). GETSET returns the previous value while setting a new one. Strings are binary-safe up to 512MB.

redis-cli
# set and get a key
SET mykey "Hello"
GET mykey               # "Hello"

# set only if not exists (NX) or only if exists (XX)
SET mykey "new" NX      # set only if key does NOT exist
SET mykey "new" XX      # set only if key DOES exist

# set with expiration (seconds / milliseconds)
SET token "abc" EX 3600    # expires in 3600s
SET token "abc" PX 60000   # expires in 60000ms

# set with expiration at a Unix timestamp
SET token "abc" EXAT 1700000000

# get and set atomically (returns old value)
GETSET counter 0        # returns old value, sets new

# delete a key
DEL mykey

Numeric Counters (INCR/DECR)

INCR/DECR are atomic — safe for concurrent access without locks. This makes Redis ideal for counters, rate limiting (INCR + EXPIRE), and generating sequence IDs. INCRBYFLOAT supports decimals but uses double precision (watch for rounding). If the value isn't an integer, INCR returns an error.

redis-cli
# increment a numeric string atomically
SET counter 100
INCR counter            # 101
INCRBY counter 10       # 111

# decrement
DECR counter            # 110
DECRBY counter 5        # 105

# floating point increment
SET price "10.50"
INCRBYFLOAT price 0.25  # "10.75"
INCRBYFLOAT price -1.00 # "9.75"

# all increment ops are atomic and concurrency-safe
# use for: counters, rate limiting, sequence IDs, stats

# INCR on a non-integer value returns an error
SET mystr "hello"
INCR mystr              # ERROR: value is not an integer

APPEND & String Operations

APPEND extends a string (creates it if missing). STRLEN reports length. GETRANGE/SETRANGE operate on substrings (binary-safe). GETDEL (6.2+) atomically returns and deletes — useful for one-time-read patterns. GETEX atomically returns the value and sets a TTL — handy for refreshing session expiration on read.

redis-cli
# append to a string value
SET msg "Hello"
APPEND msg " World"     # "Hello World"

# get string length
STRLEN msg              # 11

# get substring
GETRANGE msg 0 4        # "Hello"
GETRANGE msg 6 -1       # "World"

# overwrite a substring (grows if needed)
SETRANGE msg 6 "Redis"  # "Hello Redis"

# get and delete atomically
GETDEL mykey            # returns value AND deletes key
# get and set expiration atomically (6.2+)
GETEX mykey EX 3600     # returns value, sets TTL

# append only if key exists
APPEND existing_key "more"

MSET & MGET (Batch Operations)

MSET/MGET batch operations into one round-trip — essential for performance (Redis is single-threaded, so per-command overhead matters). MSETNX is atomic: either all keys are set or none. For many small key-value pairs, consider grouping them into a hash (HSET) for better memory efficiency via listpack encoding.

redis-cli
# set multiple keys at once (atomic)
MSET k1 "v1" k2 "v2" k3 "v3"

# get multiple keys at once
MGET k1 k2 k3           # 1) "v1" 2) "v2" 3) "v3"

# set multiple only if ALL keys are new
MSETNX k1 "v1" k4 "v4"  # 1 if all set, 0 if any existed

# MSET/MGET reduce round-trips — much faster than
# separate SET/GET calls in a loop

# get the length of a string value
STRLEN k1               # 2

# set multiple fields of different keys is not possible;
# for per-field updates use hashes (HSET)

# pipeline alternative in CLI:
printf '%s\n' "SET k1 v1" "SET k2 v2" "GET k1" | redis-cli --pipe-mode

Bitmaps (String-based)

Bitmaps store boolean flags for up to 4 billion users in ~512MB — extremely space-efficient. BITOP AND/OR/XOR/NOT combine bitmaps (e.g., users active on multiple days). BITCOUNT counts active users. BITFIELD packs multiple small counters into one string. Position = user ID. Use cases: daily active users, feature flags.

redis-cli
# bitmaps are strings operated on at the bit level
# great for boolean flags on a large user base

# set bit at position (0 or 1)
SETBIT user:1:active 7 1    # user 1 active
GETBIT user:1:active 7      # 1

# count set bits
BITCOUNT user:1:active      # total bits set to 1

# bitwise operations between strings
SETBIT users:d1 5 1
SETBIT users:d2 5 1
BITOP AND active-both users:d1 users:d2
BITCOUNT active-both        # users active on both days

BITOP OR  active-either users:d1 users:d2
BITOP XOR diff         users:d1 users:d2

# find the first set bit
BITPOS user:1:active 1      # position of first 1-bit

# bitfield: multiple counters in one string
BITFIELD mycnt SET u8 0 100
BITFIELD mycnt INCRBY u8 0 10

String Expiration & TTL

TTL returns -2 for non-existent keys and -1 for keys without expiration. SET with EX is atomic (no race between SET and EXPIRE). KEEPTTL (6.0+) lets you update a value while preserving its TTL — useful for refreshing cached values without resetting the expiration window. Expiration is lazy (checked on access) plus periodic background sweeps.

redis-cli
# set a key with expiration in one atomic command
SET session "data" EX 3600    # 1 hour
SET cache "value" PX 60000    # 60 seconds (ms)

# set expiration on an existing key
EXPIRE mykey 60               # 60 seconds
PEXPIRE mykey 60000           # 60 seconds (ms)
EXPIREAT mykey 1700000000     # Unix timestamp

# view remaining time to live
TTL mykey          # seconds (-1 = no expiry, -2 = no key)
PTTL mykey         # milliseconds

# remove expiration (make key persistent)
PERSIST mykey

# TTL with KEEPTTL option (6.0+): set value, keep existing TTL
SET mykey "newval" KEEPTTL
03

Lists

Push & Pop Operations

Lists are double-ended: LPUSH+RPOP = queue (FIFO), LPUSH+LPOP = stack (LIFO). BLPOP/BRPOP block until an item is available — the foundation of Redis queues (no polling needed). Always set a timeout on BLPOP to handle disconnects. Lists are O(1) for head/tail operations but O(N) for middle access.

redis-cli
# lists are ordered sequences of strings (linked lists)
LPUSH mylist "a" "b"      # list: [b, a]
RPUSH mylist "c"          # list: [b, a, c]

# pop from head or tail
LPOP mylist               # "b" (removes from head)
RPOP mylist               # "c" (removes from tail)

# pop multiple (6.2+)
LPOP mylist 2             # pop 2 from head

# get length
LLEN mylist               # 0

# blocking pop (waits if empty, up to timeout)
BLPOP queue:tasks 30      # blocks up to 30 seconds
BRPOP queue:tasks 0       # blocks forever

# push only if the list exists (don't create new key)
LPUSHX mylist "x"         # 0 if key doesn't exist

LRANGE & Indexing

LRANGE 0 -1 returns the whole list. LINDEX is O(N) for middle elements — lists are optimized for end access. LSET modifies by index. LINSERT is O(N) (use sparingly on large lists). LPOS finds element positions without removing them. For frequent random access by index, a list is the wrong choice — consider a sorted set.

redis-cli
# get a range of elements (0-based, negative from end)
LRANGE mylist 0 -1        # all elements
LRANGE mylist 0 2         # first 3 elements
LRANGE mylist -3 -1       # last 3 elements

# get element by index (O(N) traversal)
LINDEX mylist 0           # first element
LINDEX mylist -1          # last element

# set an element by index
LSET mylist 1 "newval"

# get length
LLEN mylist

# insert before or after a pivot element
LINSERT mylist BEFORE "pivot" "new"
LINSERT mylist AFTER "pivot" "new"

# find the position of an element (6.0+)
LPOS mylist "value"
LPOS mylist "value" RANK 2   # find 2nd occurrence

Blocking Pops & Queues

BRPOPLPUSH/BLMOVE moves an item atomically from source to destination — the consumer can process it safely, and if it crashes, the item is still in the destination (reliable queue pattern). This beats BRPOP+processing because a crash after BRPOP loses the item. After processing, delete from the destination list.

redis-cli
# simple FIFO queue: LPUSH to add, RPOP to consume
LPUSH tasks "job1" "job2"
RPOP tasks                # "job1" (FIFO order)

# reliable queue with BRPOP (blocking, waits for work)
BRPOP tasks 0             # blocks until a task is available

# move items between lists atomically
RPOPLPUSH source destination  # move one item

# blocking move (reliable queue pattern)
BRPOPLPUSH source destination 30  # blocks up to 30s

# Redis 6.2+: BLMOVE replaces BRPOPLPUSH
BLMOVE source destination RIGHT LEFT 30

# capped queue (e.g., recent events)
LPUSH recent "event"
LTRIM recent 0 99         # keep latest 100

LREM, LSET & LTRIM

LTRIM is a common pattern to cap list size (e.g., keep only the latest 100 events). LREM removes by value (count: positive=head, negative=tail, 0=all). LMPOP (7.0+) pops from multiple lists in one call, useful for prioritized queues. For frequent middle-of-list operations, a list is the wrong choice.

redis-cli
# trim to a range (removes everything outside)
LTRIM mylist 0 99         # keep only first 100 elements
LTRIM mylist -50 -1       # keep only last 50

# remove elements by value
LREM mylist 2 "value"     # remove first 2 occurrences of "value"
LREM mylist -2 "value"    # remove last 2 occurrences
LREM mylist 0 "value"     # remove ALL occurrences

# pop and push in one atomic operation
RPOPLPUSH source dest

# pop from multiple lists (7.0+)
LMPOP 2 mylist1 mylist2 LEFT COUNT 3

# blocking pop from multiple lists (7.0+)
BLMPOP 30 2 mylist1 mylist2 LEFT COUNT 3

# remove a key only if it's a list (type-safe)
DEL mylist                # works on any type

List Use Cases

Lists are best for sequences with end-access patterns: queues, stacks, feeds, buffers. LTRIM makes them ideal for capped logs/feeds. For rate limiting, a list of timestamps + LTRIM is a simple sliding window. For sorted/prioritized queues, use a sorted set instead. Lists don't support deduplication — use a set for that.

redis-cli
# 1. Task queue (FIFO)
LPUSH tasks "process_order:123"
BRPOP tasks 30

# 2. Recent activity feed (capped)
LPUSH user:1:feed "post:99" "post:98"
LTRIM user:1:feed 0 49     # latest 50

# 3. Stack (LIFO)
LPUSH stack "item"
LPOP stack

# 4. Circular buffer (limited size)
LPUSH buffer "new"
LTRIM buffer 0 999         # keep 1000 items

# 5. Rate limiter (sliding window)
LPUSH rate:user:1 1700000000
LTRIM rate:user:1 0 99
LLEN rate:user:1           # request count in window

# 6. Chat history (capped)
LPUSH chat:room:1 "alice: hello"
LTRIM chat:room:1 0 999    # keep last 1000 messages
04

Sets

SADD, SMEMBERS & SISMEMBER

Sets store unique values — perfect for tags, categories, and deduplication. SISMEMBER is O(1) — far faster than checking a list. SMEMBERS can block on large sets; use SSCAN for iteration. SMISMEMBER (7.4+) batch-checks membership, reducing round-trips. Sets use intset (sorted array) when all members are integers — very memory-efficient.

redis-cli
# sets: unordered collection of unique strings
SADD tags "redis" "db" "cache"
SADD tags "redis"          # ignored (already exists)

# check membership (O(1))
SISMEMBER tags "redis"     # 1 if member, 0 if not

# get all members (avoid on large sets)
SMEMBERS tags              # 1) "redis" 2) "db" 3) "cache"

# get the number of members
SCARD tags                 # 3

# remove a member
SREM tags "cache"          # 1

# check multiple memberships at once (7.4+)
SMISMEMBER tags "redis" "db" "missing"
# 1) 1  2) 1  3) 0

Set Operations (Union/Inter/Diff)

Set operations are Redis's superpower — union, intersection, and difference in O(N) without client-side processing. Use cases: mutual friends (SINTER), unique visitors (SUNION), tag-based filtering (SINTER of tag sets). SINTERCARD (7.0+) returns just the count, faster when you don't need the members. Store results to avoid recomputation.

redis-cli
# set union, intersection, difference
SADD set1 "a" "b" "c"
SADD set2 "b" "c" "d"

# union (all unique members from all sets)
SUNION set1 set2           # a, b, c, d

# intersection (members in ALL sets)
SINTER set1 set2           # b, c

# difference (members in set1 but NOT in set2)
SDIFF set1 set2            # a

# store the result in a new set
SUNIONSTORE result set1 set2
SINTERSTORE result set1 set2
SDIFFSTORE result set1 set2

# count intersection without returning members (7.0+)
SINTERCARD 2 set1 set2 LIMIT 0

# operations on multiple sets at once
SUNION s1 s2 s3 s4

SREM, SMOVE & SPOP

SMOVE is atomic — useful for state transitions (e.g., move a user from 'pending' to 'active' set). SPOP removes random members (useful for lottery/sharding). SRANDMEMBER doesn't remove. BSPOP (7.0+) blocks like BLPOP but for sets — useful for event-driven patterns where order doesn't matter. SREM returns the count actually removed.

redis-cli
# remove one or more members
SADD myset "a" "b" "c" "d"
SREM myset "a" "b"         # 2 (number removed)

# move a member from one set to another atomically
SMOVE source dest "member"

# pop a random member (removes it)
SPOP myset                 # removes and returns one
SPOP myset 3               # removes and returns 3 (6.2+)

# get a random member (does NOT remove)
SRANDMEMBER myset          # one random member
SRANDMEMBER myset 3        # 3 random (may repeat)
SRANDMEMBER myset -3       # 3 unique random

# blocking set pop (7.0+) - waits until a member exists
BSPOP myset 30             # blocks up to 30 seconds

# delete the entire set
DEL myset

SSCAN Iteration

SSCAN is the safe way to iterate large sets — SMEMBERS blocks the server for huge sets. SSCAN is cursor-based: call repeatedly until the cursor returns to 0. COUNT is a hint (it may return more or fewer). SSCAN offers weak guarantees: it may return duplicates or miss members added during iteration, but it never blocks. Always handle the cursor loop in your code.

redis-cli
# SSCAN: cursor-based iteration for large sets
SSCAN myset 0 MATCH "pre*" COUNT 100
# returns: [nextCursor, [member1, member2, ...]]

# continue until cursor returns to 0
SSCAN myset <nextCursor> MATCH "pre*" COUNT 100

# iterate all members safely
cursor=0
while true:
  cursor, members = SSCAN myset cursor COUNT 100
  process(members)
  if cursor == 0: break

# scan with type filtering (7.0+ for SCAN, sets always same type)
# COUNT is a hint, not exact (may return more or fewer)

# MATCH pattern supports glob: *, ?, [abc]
SSCAN myset 0 MATCH "user:*" COUNT 1000

# for small sets, SMEMBERS is fine
# for large sets (>10k), always use SSCAN

Set Use Cases

Sets are the go-to for uniqueness and relationships. The SINTER pattern for mutual followers/friends is a classic. For 'visitors per day' with huge cardinality, consider HyperLogLog (approximate but fixed 12KB). For ordered unique data (leaderboards), use a sorted set. Sets are unordered — if order matters, use a list or sorted set.

redis-cli
# 1. Tags / categories
SADD post:1:tags "redis" "db"
SADD post:2:tags "redis" "cache"
SINTER post:1:tags post:2:tags   # posts sharing "redis"

# 2. Unique visitors per day
SADD visitors:2025-01-01 "user:1" "user:2"
SCARD visitors:2025-01-01        # visitor count

# 3. Followers / following (mutual friends)
SADD user:1:following "user:2" "user:3"
SADD user:2:following "user:1" "user:3"
SINTER user:1:following user:2:following  # mutual follows

# 4. Blacklist / whitelist
SADD blacklist "ip:1.2.3.4"
SISMEMBER blacklist "ip:1.2.3.4"   # 1 = blocked

# 5. Lottery / random selection
SADD participants "u1" "u2" "u3" "u4"
SPOP participants 1        # draw a winner
05

Sorted Sets

ZADD, ZSCORE & ZRANK

Sorted sets (zsets) are Redis's most powerful structure — unique members ordered by score, with O(log N) operations. ZADD options (NX/XX/GT/LT) enable conditional updates: GT only updates if the new score is greater (great for 'best score' tracking). Scores are doubles; members are unique. Ties are broken lexicographically.

redis-cli
# sorted set: unique members ordered by a float score
ZADD leaderboard 100 "alice" 200 "bob" 150 "carol"

# update a score (re-add with new score)
ZADD leaderboard 250 "alice"   # alice's score becomes 250

# add with options (NX: only new, XX: only existing, GT/LT)
ZADD leaderboard NX 300 "new"        # only if new
ZADD leaderboard GT 300 "alice"      # only if new score > current
ZADD leaderboard LT 50 "alice"       # only if new score < current

# get a member's score
ZSCORE leaderboard "alice"      # "250"

# get a member's rank (0-based, ascending)
ZRANK leaderboard "alice"       # rank from lowest
ZREVRANK leaderboard "alice"    # rank from highest (0 = top)

# get the number of members
ZCARD leaderboard               # 3

# get multiple scores at once (6.2+)
ZMSCORE leaderboard "alice" "bob"  # ["250", "200"]

ZRANGE & ZREVRANGE

ZRANGE returns by rank (index); with BYSCORE it returns by score range. Redis 6.2 unified ZRANGEBYSCORE/ZREVRANGE into ZRANGE with BYSCORE/BYLEX/REV options. WITHSCORES includes the score in output. For leaderboards, ZRANGE 0 9 REV gives the top 10. ZRANGESTORE (6.2+) stores the result in a new key for later processing.

redis-cli
# get members by index range (ascending)
ZRANGE leaderboard 0 -1              # all, ascending
ZRANGE leaderboard 0 2               # first 3 (lowest scores)
ZRANGE leaderboard -3 -1             # last 3 (highest scores)

# descending order
ZREVRANGE leaderboard 0 2            # top 3 (highest scores)

# with scores
ZRANGE leaderboard 0 -1 WITHSCORES

# Redis 6.2+: unified ZRANGE with BYSCORE/REV options
ZRANGE leaderboard 100 200 BYSCORE
ZRANGE leaderboard 200 100 BYSCORE REV

# store a range in a new key (6.2+)
ZRANGESTORE top3 leaderboard 0 2 REV

# get by rank with scores
ZRANGE leaderboard 0 9 REV WITHSCORES  # top 10

# limit results (pagination)
ZRANGE leaderboard 0 -1 BYSCORE LIMIT 0 10

ZRANGEBYSCORE & ZCOUNT

ZRANGEBYSCORE returns by score range; '(' prefix makes a score exclusive. ZCOUNT gives the count in a range without fetching members. ZINCRBY atomically adjusts a score and re-sorts — perfect for live leaderboards. ZREMRANGEBYRANK is great for keeping a zset bounded (e.g., keep only top 1000).

redis-cli
# get members by score range
ZRANGEBYSCORE leaderboard 100 200    # scores 100-200
ZRANGEBYSCORE leaderboard 100 +inf   # scores >= 100
ZRANGEBYSCORE leaderboard -inf 200   # scores <= 200
ZRANGEBYSCORE leaderboard (100 200   # scores > 100 (exclusive)

# with pagination
ZRANGEBYSCORE leaderboard 100 200 LIMIT 0 10

# count members in a score range
ZCOUNT leaderboard 100 200

# increment a member's score atomically
ZINCRBY leaderboard 50 "alice"     # alice += 50

# remove a member
ZREM leaderboard "bob"

# remove by rank range
ZREMRANGEBYRANK leaderboard 0 9    # remove lowest 10
ZREMRANGEBYSCORE leaderboard 0 100 # remove scores <= 100

ZUNIONSTORE & ZINTERSTORE

ZUNIONSTORE/ZINTERSTORE combine zsets with configurable aggregation (SUM/MIN/MAX) and weights — powerful for weighted scoring (e.g., relevance = text_match*2 + recency*1). Redis 6.2 added ZUNION/ZINTER/ZDIFF that return results without storing (one less command). These are O(N*M) — be careful with many large sets. The set count must precede the set names.

redis-cli
# combine multiple sorted sets
ZADD set1 1 "a" 2 "b"
ZADD set2 2 "a" 3 "c"

# union: sum scores by default (a = 1+2 = 3)
ZUNIONSTORE result 2 set1 set2
ZRANGE result 0 -1 WITHSCORES   # a=3, b=2, c=3

# specify aggregation: SUM (default), MIN, MAX
ZUNIONSTORE result 2 set1 set2 AGGREGATE MAX   # a=2, b=2, c=3

# apply weights to each set's scores
ZUNIONSTORE result 2 set1 set2 WEIGHTS 2 1     # set1 scores * 2

# intersection (only members in ALL sets)
ZINTERSTORE result 2 set1 set2                 # only "a"

# Redis 6.2+: return without storing
ZUNION 2 set1 set2 WITHSCORES
ZINTER 2 set1 set2 WITHSCORES
ZDIFF 2 set1 set2 WITHSCORES

# count intersection (7.0+)
ZINTERCARD 2 set1 set2 LIMIT 0

Lexicographical Ranges (BYLEX)

BYLEX enables range queries on members when scores are equal — turning a sorted set into an auto-sorted string index. Use cases: autocomplete (store words with score 0, query prefix ranges), sorted dictionaries. '[a' means inclusive, '(a' exclusive, '-' start, '+' end. This only works meaningfully when all scores are identical.

redis-cli
# when all scores are equal, sorted sets act as a sorted string set
ZADD myset 0 "apple" 0 "banana" 0 "cherry" 0 "date"

# range by lexicographical order
ZRANGEBYLEX myset "[a" "[c"        # apple, banana, cherry
ZRANGEBYLEX myset "(a" "[c"        # banana, cherry (a exclusive)
ZRANGEBYLEX myset "-" "[b"         # from start to banana
ZRANGEBYLEX myset "[c" "+"         # cherry to end

# count in a lex range
ZLEXCOUNT myset "[a" "[c"          # 3

# remove by lex range
ZREMRANGEBYLEX myset "[a" "[b"

# Redis 6.2+: use ZRANGE with BYLEX
ZRANGE myset "[a" "[c" BYLEX

# limit results (pagination)
ZRANGEBYLEX myset "[a" "[c" LIMIT 0 10

Leaderboard Pattern

Sorted sets make leaderboards trivial: ZADD to update scores, ZREVRANGE for top N, ZREVRANK for a player's position. ZINCRBY atomically updates scores. For time-bucketed leaderboards (daily/weekly), use a key per period with a TTL. Pagination uses ZREVRANGE with offset. This is one of Redis's most natural and powerful use cases.

redis-cli
# build a leaderboard
ZADD leaderboard 100 "alice" 250 "bob" 150 "carol"

# top 10 players (highest scores)
ZREVRANGE leaderboard 0 9 WITHSCORES

# a player's rank (0 = top)
ZREVRANK leaderboard "alice"

# update a score (atomic)
ZINCRBY leaderboard 50 "alice"

# players within a score range (e.g., silver tier 100-199)
ZRANGEBYSCORE leaderboard 100 199

# percentile rank
total = ZCARD(leaderboard)
rank = ZREVRANK(leaderboard "alice")
# percentile = (total - rank) / total * 100

# pagination (page 3, 10 per page)
ZREVRANGE leaderboard 20 29 WITHSCORES

# time-bucketed leaderboards (weekly)
ZADD leaderboard:2025-W01 100 "alice"
EXPIRE leaderboard:2025-W01 604800   # 1 week TTL
06

Hashes

HSET, HGET & HGETALL

Hashes are ideal for storing objects — one key per object, fields per property. This beats serializing JSON because you can update individual fields atomically (HSET) without read-modify-write. HGETALL returns all fields; for large hashes, prefer HSCAN. Hashes use listpack encoding when small (under ~128 fields), making them memory-efficient.

redis-cli
# a hash maps fields to values (like a small object)
HSET user:1 name "Alice" age 30 email "[email protected]"

# get a single field
HGET user:1 name            # "Alice"

# get all fields and values
HGETALL user:1
# 1) "name"   -> "Alice"
# 2) "age"    -> "30"
# 3) "email"  -> "[email protected]"

# get the number of fields
HLEN user:1                 # 3

# get the string length of a field's value
HSTRLEN user:1 name          # 5 (length of "Alice")

# set a field only if it doesn't exist
HSETNX user:1 status "new"   # 1 if set, 0 if existed

# delete a field
HDEL user:1 email            # removes the email field

HMGET (Multiple Field Get)

HMGET batch-fetches multiple fields in one round-trip — much faster than separate HGET calls. HSET (since 4.0) accepts multiple field-value pairs, making HMSET deprecated. HKEYS/HVALS return all field names/values. HRANDFIELD (6.2+) is useful for sampling (e.g., random feature flags). HGETALL blocks on large hashes — use HSCAN for production iteration.

redis-cli
# get multiple fields at once
HMGET user:1 name age       # 1) "Alice" 2) "30"
HMGET user:1 name missing age  # 1) "Alice" 2) nil 3) "30"

# set multiple fields (HSET supports multiple field-value pairs)
HSET user:1 name "Bob" age 25 email "[email protected]"

# HMSET is deprecated (use HSET with multiple fields)
# HMSET still works but returns OK instead of count

# get all field names
HKEYS user:1                # 1) "name" 2) "age" 3) "email"

# get all values
HVALS user:1                # 1) "Alice" 2) "30" 3) "[email protected]"

# check if a field exists
HEXISTS user:1 name          # 1 if exists, 0 if not

# get a random field (6.2+)
HRANDFIELD user:1            # one random field name
HRANDFIELD user:1 3 WITHVALUES  # 3 random fields + values

HINCRBY (Numeric Fields)

HINCRBY on hash fields is atomic — perfect for per-user counters (login count, view count, cart quantities). HINCRBYFLOAT supports decimals. Hashes auto-delete when the last field is removed (HDEL). This makes hashes the ideal structure for objects with numeric counters — far more efficient than separate string keys per counter.

redis-cli
# increment a numeric field atomically
HSET user:1:stats logins 0
HINCRBY user:1:stats logins 1    # logins = 1
HINCRBY user:1:stats logins 1    # logins = 2
HINCRBY user:1:stats logins -1   # logins = 1

# increment by a specific amount
HINCRBY user:1:stats page_views 10

# floating point increment
HINCRBYFLOAT user:1:stats score 0.5

# all HINCRBY operations are atomic
# perfect for per-user/per-object counters

# use case: shopping cart (product_id -> quantity)
HSET cart:user:1 product:100 2
HINCRBY cart:user:1 product:100 1   # now 3
HINCRBY cart:user:1 product:100 -1  # now 2
HDEL cart:user:1 product:100        # remove item

HSCAN (Hash Iteration)

HGETALL blocks the server for large hashes — use HSCAN for production iteration. HSCAN is cursor-based: call repeatedly until the cursor returns to 0. COUNT is a hint. Results come as a flat [field, value, field, value, ...] array — pair them in your code. For hashes with millions of fields, consider sharding into multiple keys.

redis-cli
# HSCAN: cursor-based iteration (for large hashes)
HSCAN user:1 0 MATCH "na*" COUNT 10
# returns [cursor, [field, value, field, value, ...]]

# iterate all fields safely
cursor=0
while true:
  cursor, fields = HSCAN user:1 cursor COUNT 100
  process(fields)
  if cursor == 0: break

# HGETALL is fine for small hashes (< 100 fields)
# HSCAN is required for large hashes to avoid blocking

# match field names with glob patterns
HSCAN user:1 0 MATCH "user:*" COUNT 1000

# no VALUES-only option; results are [field, value, field, value, ...]
# pair them up in your code

# COUNT is a hint — may return more or fewer than requested

# HSCAN never blocks; safe for production use

Hash Use Cases

Hashes shine for objects and grouped counters. A powerful memory technique: when you have many small string keys, group them into one hash — the listpack encoding uses far less overhead than thousands of individual keys (each with metadata). This 'key bucketing' can cut memory usage by 5-10x for small values. Field-level TTL (7.4+) is a major addition.

redis-cli
# 1. Store an object (user profile, config)
HSET config:app name "MyApp" version "2.0" debug "off"

# 2. Per-user counters
HINCRBY user:1:stats logins 1
HINCRBY user:1:stats page_views 1
HGET user:1:stats logins

# 3. Shopping cart (product_id -> quantity)
HSET cart:user:1 product:100 2
HINCRBY cart:user:1 product:100 1
HDEL cart:user:1 product:100

# 4. Feature flags
HSET feature_flags user:1 new_ui 1 beta 0
HGET feature_flags user:1 new_ui   # 1

# 5. Session store with TTL
HSET session:abc user_id 1 data "..."
EXPIRE session:abc 1800   # 30 min

# 6. Hash field expiration (Redis 7.4+)
HEXPIRE user:1 60 FIELDS 1 session_token
HPTTL user:1 FIELDS 1 session_token
07

Keys Management

EXISTS, TYPE & DEL

EXISTS with multiple keys returns the count of existing ones. TYPE returns the data structure type — always check before operating to avoid WRONGTYPE errors (a key's type is immutable for its lifetime). DEL is blocking and can stall the server on large structures; prefer UNLINK for big keys. TYPE returns 'none' for non-existent keys.

redis-cli
# check if a key exists
SET mykey "hello"
EXISTS mykey             # 1
EXISTS missing           # 0
EXISTS key1 key2 key3    # count of existing keys (multiple)

# get the type of a value
TYPE mykey               # "string"
TYPE mylist              # "list"
TYPE missing             # "none"

# delete one or more keys
DEL mykey                # 1 if deleted, 0 if not
DEL key1 key2 key3       # number of keys deleted

# delete is blocking for large keys — use UNLINK instead
# UNLINK deletes in a background thread

# check type before operating to avoid WRONGTYPE errors
TYPE mykey
# returns: string | list | hash | set | zset | stream | none

UNLINK (Async Delete)

UNLINK (4.0+) is the non-blocking alternative to DEL for large keys — it removes the key from the keyspace immediately but frees memory in a background thread, preventing server stalls. For keys with millions of elements, always use UNLINK. FLUSHDB ASYNC / FLUSHALL ASYNC do the same for bulk deletion. The lazyfree thread pool handles the actual freeing.

redis-cli
# UNLINK: non-blocking delete (large keys)
UNLINK biglist bighash   # returns count of deleted keys

# UNLINK removes the key from the keyspace immediately
# but frees the memory in a background thread

# compare:
DEL biglist              # blocks until fully freed
UNLINK biglist           # returns immediately, frees later

# for small keys, DEL and UNLINK are equivalent
# for large keys (millions of elements), UNLINK is essential

# flush database asynchronously (non-blocking)
FLUSHDB ASYNC
FLUSHALL ASYNC

# check what's being freed in background
INFO memory | grep lazyfree

RENAME & RENAMENX

RENAME atomically moves a key to a new name, overwriting any existing destination key (and its value). RENAMENX (6.2+) only renames if the destination doesn't exist, preventing accidental overwrites. The TTL is preserved on rename. RENAME is atomic — no other client can see an intermediate state. Cross-database rename isn't supported; use MOVE or DUMP/RESTORE.

redis-cli
# rename a key (overwrites destination if it exists)
SET oldkey "value"
RENAME oldkey newkey     # OK; oldkey is gone, newkey has "value"

# rename only if destination doesn't exist (6.2+)
RENAMENX oldkey newkey   # 1 if renamed, 0 if newkey already exists

# rename preserves the type and TTL
SET tempkey "data" EX 3600
RENAME tempkey permkey   # permkey now has TTL of ~3600s
TTL permkey              # ~3600

# rename across databases is not supported
# use MOVE + RENAME, or DUMP/RESTORE for cross-db moves

# type is immutable — rename keeps the type
TYPE permkey             # "string"

# renaming is atomic

KEYS vs SCAN

KEYS blocks the server while scanning the entire keyspace — NEVER use it in production on large datasets. SCAN is the production-safe alternative: cursor-based, non-blocking, returns a cursor you follow until 0. SCAN may return duplicates or miss keys added during iteration, but it never blocks. COUNT is a hint (it may return more or fewer). Always handle the cursor loop in your code.

redis-cli
# KEYS: blocks the server while scanning — NEVER in production
KEYS *                   # all keys (DANGEROUS on large DBs)
KEYS user:*              # keys matching pattern
KEYS session:*

# SCAN: cursor-based iteration (non-blocking, production-safe)
SCAN 0 MATCH user:* COUNT 100
# returns: [nextCursor, [key1, key2, ...]]

# continue until cursor returns to 0
SCAN <nextCursor> MATCH user:* COUNT 100

# scan by type (7.0+)
SCAN 0 TYPE hash
SCAN 0 MATCH user:* TYPE string COUNT 100

# SCAN is weakly consistent:
# - may return duplicates
# - may miss keys added during iteration
# - but NEVER blocks the server

# COUNT is a hint, not exact

RANDOMKEY & DBSIZE

RANDOMKEY returns a random key without removing it — useful for sampling. DBSIZE returns the exact count of keys in the current database (O(1)). INFO keyspace shows per-database counts with expiration stats (keys, expires, avg_ttl). For a random key of a specific type, there's no built-in command — use SCAN + TYPE in a loop.

redis-cli
# get a random key (O(1), does not remove)
RANDOMKEY                # "user:1" or nil if db is empty

# get the number of keys in the current database
DBSIZE                   # 1234

# count keys across all databases (in a cluster)
# note: DBSIZE only counts the current db
SELECT 0; DBSIZE
SELECT 1; DBSIZE

# get keyspace info (keys per db with expiry info)
INFO keyspace
# db0:keys=1000,expires=500,avg_ttl=3600000
# db1:keys=50,expires=10,avg_ttl=60000

# scan with count for approximate size estimation
SCAN 0 COUNT 1           # get cursor + small sample

# find a random key of a specific type
# (no built-in; use SCAN + TYPE in a loop)

OBJECT Commands

OBJECT ENCODING reveals the internal representation (debugging memory). OBJECT IDLETIME finds cold keys (candidates for eviction). OBJECT FREQ works only with maxmemory-policy LFU. MEMORY USAGE with SAMPLES 0 counts exactly (default samples for large collections). COMMAND DOCS (7.0+) returns structured documentation. These introspection tools help debug memory bloat.

redis-cli
# OBJECT subcommands for introspection
OBJECT ENCODING mykey     # internal encoding (embstr, raw, int, listpack, etc.)
OBJECT REFCOUNT mykey     # reference count
OBJECT IDLETIME mykey     # seconds since last access
OBJECT FREQ mykey         # access frequency (LFU mode only)

# check memory usage of a key (bytes)
MEMORY USAGE mykey
MEMORY USAGE mykey SAMPLES 0   # exact count (slower for large keys)

# total memory used by the server
INFO memory | grep used_memory_human

# inspect a key's internal structure (debugging, can be slow)
DEBUG OBJECT mykey        # raw internal info

# the encoding changes automatically as the structure grows
# e.g., hash: listpack -> hashtable when fields exceed threshold

# help understand a command
COMMAND INFO GET
COMMAND DOCS SET          # structured docs (7.0+)
08

Expiration & Persistence

EXPIRE & EXPIREAT

EXPIRE sets TTL on an existing key; SET with EX/PX/EXAT/PXAT does it atomically (preferred). NX/GT/LT options (7.0+) enable conditional TTL setting — useful for 'only add expiry if none' or 'only extend, never shorten'. TTL returns -1 for keys without expiry, -2 for non-existent keys. Expiration is lazy (checked on access) plus periodic background sweeps.

redis-cli
# set expiration on an existing key (seconds)
SET mykey "value"
EXPIRE mykey 60              # expires in 60 seconds
EXPIRE mykey 60 NX           # only set TTL if none exists (7.0+)
EXPIRE mykey 60 GT           # only set if new TTL > current (7.0+)
EXPIRE mykey 60 LT           # only set if new TTL < current (7.0+)

# set expiration in milliseconds
PEXPIRE mykey 60000

# set expiration at a Unix timestamp
EXPIREAT mykey 1700000000       # seconds
PEXPIREAT mykey 1700000000000    # milliseconds

# set value AND expiration atomically (preferred)
SET token "abc" EX 3600
SET token "abc" PX 3600000
SET token "abc" EXAT 1700000000

# get remaining TTL
TTL mykey          # seconds (-1 = no expiry, -2 = no key)
PTTL mykey         # milliseconds

PERSIST (Remove TTL)

PERSIST removes the TTL, making a key persistent (the value remains). GETEX (6.2+) atomically returns the value and sets or removes the TTL — useful for refreshing session expiration on read (GETEX key EX 3600) or making a temporary key permanent (GETEX key PERSIST). COPY (6.2+) copies a key, optionally across databases, without the race of GET+SET.

redis-cli
# remove expiration (make key persistent)
SET session "data" EX 3600
TTL session               # 3600
PERSIST session           # 1 (success)
TTL session               # -1 (no expiry)

# PERSIST returns 1 if the TTL was removed, 0 if key had no TTL or doesn't exist

# copy a key with optional TTL handling (6.2+)
COPY source dest               # copy without TTL
COPY source dest REPLACE       # overwrite destination
COPY source dest DB 1          # copy to another database

# GETEX: get value and optionally set/remove TTL (6.2+)
GETEX mykey EX 3600       # returns value, sets 3600s TTL
GETEX mykey PERSIST       # returns value, removes TTL

# note: PERSIST only removes TTL; the key and value remain

SAVE & BGSAVE (RDB)

SAVE blocks the server until the snapshot completes — never use it in production. BGSAVE forks the process (copy-on-write) so the main thread isn't blocked, but large datasets can cause memory spikes during fork. The 'save' rules trigger automatic snapshots based on change rate. RDB is compact and ideal for backups but risks losing data written after the last snapshot.

redis-cli
# SAVE: synchronous snapshot (BLOCKS the server until done)
SAVE                     # blocks — use only in maintenance windows

# BGSAVE: background snapshot (non-blocking, forks a child)
BGSAVE                   # returns immediately, saves in background
LASTSAVE                 # Unix timestamp of the last successful save

# check if a BGSAVE is in progress
INFO persistence | grep rdb_bgsave_in_progress

# configure automatic snapshots in redis.conf:
#   save 3600 1     # save if >= 1 key changed in 3600s
#   save 300 100    # save if >= 100 keys changed in 300s
#   save 60 10000   # save if >= 10000 keys changed in 60s
#   save ""         # disable RDB entirely

# RDB file location
CONFIG GET dbfilename    # "dump.rdb"
CONFIG GET dir           # "/var/lib/redis"

# RDB pros: compact, fast restart, great for backups
# RDB cons: potential data loss between snapshots

BGREWRITEAOF (AOF Compaction)

AOF appends every write command to a log — far more durable than RDB (max 1 second of data loss with everysec). 'always' fsyncs on every write (very slow, use only for critical data). BGREWRITEAOF compacts the log (rewrites it as the minimal set of commands). Redis 7 uses a multi-part AOF format (base RDB + incremental log) — appenddirname holds the parts.

redis-cli
# AOF: logs every write command (durable, replayable)
# enable in redis.conf:
#   appendonly yes
#   appendfilename "appendonly.aof"

# fsync policies:
#   appendfsync always    # fsync every write (safest, slowest)
#   appendfsync everysec  # fsync once per second (default, balanced)
#   appendfsync no        # let OS decide (fastest, risk of data loss)

# trigger AOF rewrite (compacts the log)
BGREWRITEAOF             # non-blocking, rewrites in background

# AOF auto-rewrite thresholds:
#   auto-aof-rewrite-percentage 100
#   auto-aof-rewrite-min-size 64mb

# check AOF status
INFO persistence | grep aof

# AOF pros: minimal data loss (1 sec max with everysec)
# AOF cons: larger files, slower load on restart

# Redis 7: multi-part AOF (base + incremental files)
CONFIG GET appenddirname   # "appendonlydir"

Persistence Configuration

CONFIG SET changes settings at runtime (no restart needed); CONFIG REWRITE persists them to redis.conf. The recommended production setup is RDB + AOF hybrid: RDB for fast restart and backups, AOF for minimal data loss. Since Redis 7, the AOF starts with an RDB base snapshot followed by incremental commands — combining fast load with durability.

redis-cli
# view current persistence settings
CONFIG GET save
CONFIG GET appendonly
CONFIG GET appendfsync
CONFIG GET dir
CONFIG GET dbfilename
CONFIG GET appendfilename

# enable AOF at runtime
CONFIG SET appendonly yes

# change fsync policy at runtime
CONFIG SET appendfsync everysec

# disable RDB snapshots
CONFIG SET save ""

# persist config changes to redis.conf
CONFIG REWRITE

# hybrid persistence (recommended):
# RDB for fast restart + AOF for durability
# redis.conf:
#   save 3600 1
#   appendonly yes
#   aof-use-rdb-preamble yes   # RDB header in AOF (faster load)

# check persistence status
INFO persistence

Backup & Restore

Back up the RDB file (compact, fast) regularly — it's a consistent snapshot even while Redis runs (copy-on-write). For AOF, back up the whole appendonly directory (Redis 7 multi-part). Restore by stopping Redis, replacing files, and restarting. To recover from a bad command, edit the AOF to remove it before restart. redis-cli --rdb performs an online backup without touching the filesystem directly.

redis-cli
# backup: copy the RDB file while Redis runs
BGSAVE
# wait for LASTSAVE to update, then copy the file
cp /var/lib/redis/dump.rdb /backup/dump-$(date +%F).rdb

# AOF backup: copy the AOF manifest + files (Redis 7)
cp -r /var/lib/redis/appendonlydir /backup/

# restore: stop Redis, replace data files, restart
redis-cli SHUTDOWN
cp /backup/dump.rdb /var/lib/redis/dump.rdb
redis-server /etc/redis/redis.conf

# for point-in-time recovery from AOF:
# edit the AOF file to remove unwanted commands, then restart

# RDB file analysis (offline, safe)
redis-check-rdb /var/lib/redis/dump.rdb
redis-check-aof /var/lib/redis/appendonly.aof

# fix a truncated AOF
redis-check-aof --fix /var/lib/redis/appendonly.aof

# online backup with --rdb (dumps RDB to stdout/file)
redis-cli --rdb /backup/dump.rdb
09

Pub/Sub

SUBSCRIBE & UNSUBSCRIBE

SUBSCRIBE blocks the client to receive messages from one or more channels. A subscribed client can only call SUBSCRIBE/UNSUBSCRIBE/PSUBSCRIBE/PUNSUBSCRIBE/PING/QUIT — no other commands. PUBSUB introspects the pub/sub state (active channels, subscriber counts). Messages are fire-and-forget: if no subscriber is listening, the message is lost. For reliable delivery, use Streams.

redis-cli
# subscribe to channels (in one client, blocks)
SUBSCRIBE news alerts

# the client now listens for messages:
# 1) "subscribe" 2) "news" 3) 1
# 1) "subscribe" 2) "alerts" 3) 2

# unsubscribe from specific channels
UNSUBSCRIBE news
UNSUBSCRIBE news alerts

# unsubscribe from ALL channels
UNSUBSCRIBE

# note: published messages are NOT persisted
# if no subscriber is listening, the message is lost
# for persistent messaging, use Streams instead

# count active subscriptions (in another client)
PUBSUB CHANNELS           # list active channels
PUBSUB NUMSUB news        # subscriber count for "news"

PUBLISH

PUBLISH returns the number of subscribers that received the message (0 if none). There's no way to retrieve a message after it's published — if no subscriber is listening, it's gone. PUBSUB CHANNELS lists active channels (those with at least one subscriber); PUBSUB NUMSUB returns subscriber counts. For reliable, replayable messaging, use Streams (XADD/XREAD).

redis-cli
# publish a message to a channel (from another client)
PUBLISH news "Breaking: Redis 7.4 released"
# returns: number of subscribers that received the message

# publish to a channel with no subscribers
PUBLISH empty "hello"
# returns: 0 (message is lost)

# publish to multiple channels (separate PUBLISH calls)
PUBLISH news "msg1"
PUBLISH alerts "msg2"

# pub/sub has no persistence, no replay, no consumer groups
# subscribers must be connected to receive messages

# check how many clients are listening
PUBSUB NUMSUB news
PUBSUB NUMSUB news alerts    # multiple channels

# list active channels (with pattern)
PUBSUB CHANNELS              # all active channels
PUBSUB CHANNELS news:*       # channels matching pattern

PSUBSCRIBE (Pattern Subscription)

PSUBSCRIBE subscribes to channels matching glob patterns — powerful for event routing (e.g., user:*:login catches all user login events). A message matching both a direct subscription and a pattern is delivered twice. Pattern subscribers receive 'pmessage' events with the pattern, channel, and message. PUNSUBSCRIBE cancels pattern subscriptions.

redis-cli
# subscribe to channels matching a glob pattern
PSUBSCRIBE news.*
PSUBSCRIBE user:*:login
PSUBSCRIBE events:*

# patterns use glob syntax:
#   *    matches any sequence
#   ?    matches one character
#   [ab] matches 'a' or 'b'

# unsubscribe from patterns
PUNSUBSCRIBE news.*
PUNSUBSCRIBE               # unsubscribe from ALL patterns

# a message matching both a direct subscription and a pattern
# is delivered twice (once for each)

# pattern subscribers receive:
# 1) "pmessage" 2) "news.*" 3) "news.tech" 4) "the message"

# use case: event routing
PSUBSCRIBE user:*:login       # all user logins
PSUBSCRIBE order:*:created    # all order creations
PSUBSCRIBE cache:*:invalidate # cache invalidation events

Sharded Pub/Sub (7.0+)

Sharded Pub/Sub (7.0+) solves a Cluster-mode problem: regular Pub/Sub broadcasts every PUBLISH to all cluster nodes, wasting bandwidth. Sharded Pub/Sub routes messages only to the shard that owns the channel key — far more efficient in large clusters. Use SSUBSCRIBE/SPUBLISH in Cluster deployments. The channel name determines the shard (like a key).

redis-cli
# regular Pub/Sub broadcasts to ALL cluster nodes (expensive)
# sharded Pub/Sub routes messages only to the shard owning the channel

# subscribe to a shard channel
SSUBSCRIBE mychannel

# publish to a shard channel
SPUBLISH mychannel "hello"

# sharded pub/sub uses the channel name for shard routing
# so messages only travel within one shard (more efficient)

# use sharded pub/sub in a Redis Cluster to avoid
# broadcasting messages to every node

# regular SUBSCRIBE/PUBLISH still work in cluster mode
# but they broadcast to all shards (cluster-wide overhead)

# check shard channel subscribers (7.0+)
PUBSUB SHARDCHANNELS
PUBSUB SHARDNUMSUB mychannel

Keyspace Notifications

Keyspace notifications let clients react to key lifecycle events (set, delete, expire, evict). Configure with notify-keyspace-events. They're fire-and-forget (no persistence) and not reliable — if the Redis server restarts, pending events are lost. For reliable event processing, use Streams. Keyspace notifications are great for side-effects like cleanup or cache warming.

redis-cli
# enable keyspace notifications (disabled by default)
CONFIG SET notify-keyspace-events "KEA"
# K = keyspace events, E = keyevent events
# g = generic (DEL, EXPIRE, ...), $ = string, l = list
# h = hash, s = set, z = sorted set, x = expired, e = evicted
# A = all except m (m = keymiss)

# subscribe to events on a specific key
SUBSCRIBE __keyspace@0__:mykey
# publishes "set", "del", "expire", etc.

# subscribe to events of a specific type
SUBSCRIBE __keyevent@0__:del
# publishes the key name that was deleted

# example: react when a key expires
SUBSCRIBE __keyspace@0__:session:abc
# receives "expired" when the key TTL ends

# example: react to all expired keys
SUBSCRIBE __keyevent@0__:expired

# check current setting
CONFIG GET notify-keyspace-events
10

Transactions

MULTI & EXEC

MULTI/EXEC groups commands into an atomic, sequential block — no other client can interleave. Commands are queued (returning QUEUED) and executed together at EXEC. A queuing error (bad command name) aborts the whole transaction. A runtime error (wrong type on a key) skips only that command — the rest still execute. This differs from SQL transactions.

redis-cli
# MULTI starts a transaction; commands are queued
MULTI
SET counter 1
INCR counter
INCR counter
GET counter
EXEC
# returns: 1) OK  2) 2  3) 3  4) "3"

# all queued commands execute atomically (no interleaving)
# other clients cannot run commands between MULTI and EXEC

# each queued command returns "QUEUED" until EXEC
MULTI
SET x 1
# "QUEUED"
INCR x
# "QUEUED"
EXEC
# 1) OK 2) 2

# the transaction is atomic: all or nothing (at queuing time)

DISCARD (Cancel Transaction)

DISCARD cancels a transaction, clearing all queued commands and releasing the connection from transaction mode. It also clears any WATCHed keys (see WATCH). After DISCARD, the client can issue normal commands again. DISCARD is the 'rollback' equivalent — but note Redis doesn't support partial rollback within EXEC; runtime errors skip the failing command but commit the rest.

redis-cli
# DISCARD cancels a transaction (clears all queued commands)
MULTI
SET x 1
INCR x
DISCARD
# OK — no commands are executed

# after DISCARD, the connection returns to normal mode
GET x
# (nil) or the previous value — nothing changed

# DISCARD also clears all WATCHed keys
WATCH counter
MULTI
INCR counter
DISCARD
# counter is no longer watched

# you must call MULTI before DISCARD
DISCARD
# ERR DISCARD without MULTI

WATCH (Optimistic Locking)

WATCH implements optimistic locking: if a watched key changes between WATCH and EXEC, the transaction is aborted (EXEC returns nil). This is the standard Redis pattern for read-modify-write atomicity (since Redis has no row locks). Always retry on nil. WATCH is checked at EXEC time. UNWATCH cancels watches (also happens on DISCONNECT). A watched key modified by the same client also triggers abort.

redis-cli
# WATCH monitors keys; if any change before EXEC, transaction aborts
WATCH counter
val = GET counter          # read current value
MULTI
INCR counter               # queue the update
EXEC                       # nil if counter changed -> retry

# typical pattern: read-modify-write with retry
WATCH key
current = GET key
new_value = compute(current)
MULTI
SET key new_value
EXEC
# if EXEC returns nil, the key changed — loop and retry

# UNWATCH cancels all watches
UNWATCH

# WATCH is checked at EXEC time, not during queuing
# if a watched key is modified (by any client, including yourself),
# EXEC returns nil and the transaction is not executed

UNWATCH & WATCH Edge Cases

After EXEC (whether the transaction committed or was aborted due to WATCH), all watches are automatically cleared. Same for DISCARD. To retry an optimistic-lock loop, you must call WATCH again before the next MULTI. WATCH inside MULTI is an error — WATCH must always precede MULTI. UNWATCH is mainly for canceling watches without entering a transaction.

redis-cli
# UNWATCH cancels all watches
WATCH key1 key2
UNWATCH                    # both keys are no longer watched

# WATCH inside a MULTI is not allowed (error)
MULTI
WATCH key
# ERR WATCH inside MULTI is not allowed

# after EXEC (success or abort), watches are cleared
WATCH key
MULTI
SET key "new"
EXEC
# key is no longer watched after EXEC

# after DISCARD, watches are cleared
WATCH key
MULTI
DISCARD
# key is no longer watched

# re-WATCH after EXEC to retry the optimistic lock loop
WATCH key
val = GET key
MULTI
SET key newval
EXEC
# if nil, loop: WATCH key again, GET, MULTI, SET, EXEC

Transaction Error Handling

Redis has two error types in transactions. Queuing errors (bad command name/arity) abort the entire transaction at EXEC time (EXECABORT) — nothing runs. Runtime errors (e.g., INCR on a non-integer) skip only the failing command — the rest still execute. There is NO rollback. This is a critical difference from SQL databases. Validate your data before MULTI to avoid runtime errors.

redis-cli
# two types of errors in transactions:

# 1. Queuing errors (bad command syntax) — aborts the WHOLE transaction
MULTI
SET key "value"
INCRBY key "notanumber"   # error during queuing (wrong arity/type at parse)
GET key
EXEC
# EXECABORT Transaction discarded because of previous errors.
# NOTHING executes — not even the SET

# 2. Runtime errors (wrong type at execution) — skips ONLY that command
SET key "stringvalue"
MULTI
INCR key                  # queues fine (syntax OK)
SET otherkey "ok"
EXEC
# 1) (error) ERR value is not an integer
# 2) OK
# the INCR failed but SET otherkey succeeded — no rollback!

# lesson: Redis transactions do NOT have rollback like SQL
11

Pipelining

Pipeline Basics

Pipelining batches commands to reduce network round-trips — dramatically faster than individual commands over a network. Each round-trip costs network latency (~0.1-1ms); 1000 individual commands = 100-1000ms, but pipelined = ~1ms. Most client libraries provide a pipeline/multi method. redis-cli can pipeline via stdin. Pipelining does NOT guarantee atomicity — other clients can interleave.

redis-cli
# pipelining sends multiple commands in one network round-trip
# (no atomicity guarantee, just reduced latency)

# using redis-cli with stdin (simple pipeline):
printf '%s\n' "SET k1 v1" "SET k2 v2" "GET k1" | redis-cli
# OK
# OK
# "v1"

# using redis-cli --pipe (raw RESP, fastest for bulk loads):
# (see the data-import-export section for details)

# in a client library (e.g., Node.js with ioredis):
const pipe = redis.pipeline()
pipe.set("k1", "v1")
pipe.set("k2", "v2")
pipe.incr("counter")
const results = await pipe.exec()

# vs individual commands:
await redis.set("k1", "v1")    # round-trip 1
await redis.set("k2", "v2")    # round-trip 2
await redis.incr("counter")    # round-trip 3

MULTI vs Pipelining

MULTI/EXEC guarantees atomicity (no other client can interleave) AND is automatically pipelined by most client libraries (so you also get reduced round-trips). Plain pipelining only reduces round-trips — other clients can execute between your pipelined commands. Use MULTI/EXEC when you need atomicity (e.g., read-modify-write with WATCH); use plain pipelining for bulk operations where order/atomicity doesn't matter.

redis-cli
# MULTI/EXEC: atomic (no interleaving), pipelined automatically
MULTI
SET k1 v1
SET k2 v2
INCR counter
EXEC
# all 3 commands run atomically as one block

# Pipelining: NOT atomic, just batched (other clients can interleave)
# (in client code)
pipe.set("k1", "v1")
pipe.set("k2", "v2")
pipe.incr("counter")
pipe.exec()
# commands may interleave with other clients

# when to use which:
# - need atomicity (all-or-nothing)?      -> MULTI/EXEC
# - just want speed (bulk operations)?    -> pipelining
# - both?                                  -> MULTI/EXEC (auto-pipelined)

# MULTI/EXEC is automatically pipelined by most clients
# so you get atomicity + batch round-trips in one

RESP Protocol

RESP is Redis's wire protocol — simple, text-based, and fast. *N denotes an array of N elements; $N denotes a bulk string of N bytes; + is a simple string; : is an integer; - is an error. redis-cli --pipe mode accepts raw RESP for maximum-speed bulk loading (much faster than text commands via stdin). Understanding RESP helps with debugging and building custom clients.

redis-cli
# RESP (REdis Serialization Protocol) is the wire format
# redis-cli sends RESP; you can also send it raw with --pipe

# simple command (PING) in RESP:
# *1\r\n$4\r\nPING\r\n

# SET key value in RESP:
# *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

# RESP types:
#   +OK      simple string
#   -ERR     error
#   :42      integer
#   $5\r\nhello   bulk string
#   *2\r\n...     array

# raw pipeline with RESP (fastest bulk insert):
# generate a RESP file and pipe it:
cat commands.resp | redis-cli --pipe

# --pipe mode expects raw RESP; checks reply counts
# output: "Last reply received from server: ..."
# "errors: 0, replies: N"

Pipeline in Shell Scripts

Shell scripting with redis-cli is a common way to bulk-load data or run batch operations. The simplest approach pipes text commands via stdin (echo/cat | redis-cli). For maximum speed, generate RESP and use --pipe mode (an order of magnitude faster for large loads). The bash loop examples show both approaches — choose based on your volume and performance needs.

redis-cli
# pipeline multiple commands via stdin
echo -e "SET k1 v1\nSET k2 v2\nGET k1\nGET k2" | redis-cli

# pipeline from a file
cat commands.txt | redis-cli

# pipeline with --pipe-mode (raw RESP, fastest)
# first generate RESP, then pipe:
redis-cli --pipe < commands.resp

# bulk insert example (bash, generate RESP):
for i in $(seq 1 1000); do
  printf '*3\r\n$3\r\nSET\r\n$4\r\nkey:\r\n$1\r\n%s\r\n' "$i"
done | redis-cli --pipe

# or simpler (text mode, slightly slower):
for i in $(seq 1 1000); do
  echo "SET key:$i value$i"
done | redis-cli

# --pipe-mode reports errors and throughput

Pipeline Limits & Best Practices

Pipelining is the single biggest Redis performance lever, but don't over-pipeline — queued replies consume memory on both client and server. Batch in groups of 100-1000 commands. Benchmark with redis-benchmark -P to find your sweet spot (throughput levels off, latency increases with very large pipelines). For commands that depend on previous results, use Lua scripts (atomic, single round-trip) instead of multiple pipelined calls.

redis-cli
# don't pipeline too many commands at once
# (queued replies consume memory on both client and server)

# bad: 1 million commands in one pipeline
# -> huge memory buffer, potential timeout

# good: batch in groups of 100-1000
for batch in chunks(data, 500):
  pipe = redis.pipeline()
  for item in batch:
    pipe.set(item.key, item.value)
  pipe.exec()

# benchmark pipelining
redis-benchmark -t set -n 100000 -P 16   # -P = pipeline size
# try -P 1, 10, 100, 1000 to find the sweet spot

# use pipelining for:
# - bulk imports
# - batch reads (MGET is better for simple multi-get)
# - pre-warming cache
# - reducing latency in high-RTT environments

# avoid pipelining for:
# - commands that depend on previous results (use Lua instead)
# - very large batches (split into chunks)
12

Lua Scripting

EVAL Basics

EVAL runs Lua scripts atomically — while a script runs, no other command executes, making it ideal for multi-step atomic operations. KEYS and ARGV pass data from the caller; never build keys by string concatenation in the script (violates cluster rules — all keys must be in KEYS). redis.call raises errors (halts), redis.pcall returns them. Scripts are cached by their SHA1 hash for EVALSHA.

redis-cli
# EVAL: run a Lua script inline
# EVAL "script" numkeys key1 key2 ... arg1 arg2 ...
EVAL "return redis.call('GET', KEYS[1])" 1 mykey
# 1 = number of keys
# mykey = KEYS[1]

# run a simple script (no keys)
EVAL "return 42" 0
EVAL "return {1, 2, 3}" 0
EVAL "return 'hello'" 0

# a check-and-set script:
EVAL "
  local cur = redis.call('GET', KEYS[1])
  if cur == ARGV[1] then
    return redis.call('SET', KEYS[1], ARGV[2])
  end
  return 0
" 1 mykey "expected" "newvalue"

# redis.call() raises error if command fails (stops script)
# redis.pcall() returns error as a table (can handle)
EVAL "local ok, err = pcall(redis.call, 'GET', 'missing'); return err" 0

EVALSHA & SCRIPT LOAD

EVALSHA runs a previously loaded script by its SHA1 hash, avoiding resending the script body on every call — a major bandwidth win for apps that run the same script repeatedly. On cache miss (after SCRIPT FLUSH or restart), Redis returns NOSCRIPT; clients catch this and fall back to EVAL + SCRIPT LOAD. Always use libraries (redis-py, ioredis) that handle this transparently.

redis-cli
# load a script once, get its SHA1
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# returns: "fa00d1f0..." (the SHA1 hash)

# execute by hash (saves bandwidth on repeated calls)
EVALSHA "fa00d1f0..." 1 mykey

# check if a script is cached
SCRIPT EXISTS "fa00d1f0..." "abcd1234..."
# 1) 1  2) 0   (1 = exists, 0 = not cached)

# flush the script cache
SCRIPT FLUSH

# list all running scripts (during debug)
SCRIPT DEBUG SYNC   # blocking debug mode
SCRIPT DEBUG NO     # disable debugger

# typical client library pattern:
# 1. SCRIPT LOAD the script on startup
# 2. EVALSHA to run it (falls back to EVAL on NOSCRIPT error)

SCRIPT LOAD, FLUSH & EXISTS

SCRIPT LOAD caches a script and returns its SHA1. SCRIPT EXISTS checks if scripts are cached (useful after a restart). SCRIPT FLUSH clears all cached scripts — subsequent EVALSHA returns NOSCRIPT. SCRIPT KILL stops a long-running script, but only if it hasn't done any writes yet (to maintain consistency); if it has, you must SHUTDOWN NOSAVE. Scripts are ephemeral — use Functions (7.0+) for persistence.

redis-cli
# SCRIPT LOAD: load a script without running it
SCRIPT LOAD "return redis.call('SET', KEYS[1], ARGV[1])"
# returns the SHA1 hash

# SCRIPT EXISTS: check if scripts are cached (after load or restart)
SCRIPT EXISTS "abc123..." "def456..."
# 1) 1  2) 0

# SCRIPT FLUSH: clear the script cache
SCRIPT FLUSH
# all EVALSHA calls will now return NOSCRIPT until scripts are reloaded

# SCRIPT KILL: stop a running script (only if it hasn't written yet)
SCRIPT KILL

# if the script has already executed a write command:
# SHUTDOWN NOSAVE is the only way to stop it (extreme measure)

# scripts are ephemeral — they don't survive a restart
# use Redis Functions (7.0+) for persistent scripts

# check the script cache size
INFO memory | grep script_cache

KEYS and ARGV

KEYS[] carries key names (essential for cluster routing — the cluster uses these to determine which shard handles the script). ARGV[] carries values and parameters. Never hardcode key names inside the script (e.g., redis.call('GET', 'mykey')) — this breaks cluster routing because the cluster can't tell which shard 'mykey' lives on. Always pass keys via KEYS[]. The numkeys argument tells Redis how many KEYS to expect.

redis-cli
# KEYS[]: key names passed to the script
# ARGV[]: additional arguments (values, options)

# example: atomic increment with cap
EVAL "
  local current = tonumber(redis.call('GET', KEYS[1]) or 0)
  local max = tonumber(ARGV[1])
  if current >= max then
    return -1  -- cap reached
  end
  return redis.call('INCR', KEYS[1])
" 1 counter 100
# KEYS[1] = counter, ARGV[1] = 100

# example: move item between lists atomically
EVAL "
  local item = redis.call('RPOP', KEYS[1])
  if item then
    redis.call('LPUSH', KEYS[2], item)
  end
  return item
" 2 source_list dest_list

# NEVER hardcode key names in scripts (breaks cluster routing)
# always pass keys via KEYS[] so the cluster can route correctly

Atomic Operations with Lua

Lua scripts are the canonical way to do atomic multi-command operations in Redis (no MULTI/EXEC race). Compare-and-set, semaphore acquire, queue migrations, and rate limiting all benefit. Scripts block the server while running — keep them short and avoid loops over many keys (which can stall other clients). For very long work, use Redis Functions (Redis 7.0+) which are declared and versioned like stored procedures.

redis-cli
# atomic compare-and-set (only update if value matches)
EVAL "
  local cur = redis.call('GET', KEYS[1])
  if cur == ARGV[1] then
    return redis.call('SET', KEYS[1], ARGV[2])
  end
  return 0
" 1 mykey "expected" "newvalue"

# atomic 'decrement if positive' (semaphore / rate limit)
EVAL "
  local val = tonumber(redis.call('GET', KEYS[1]) or 0)
  if val > 0 then
    redis.call('DECR', KEYS[1])
    return 1
  end
  return 0
" 1 semaphore

# atomic list pop + push (move between queues)
EVAL "
  local item = redis.call('RPOP', KEYS[1])
  if item then
    redis.call('LPUSH', KEYS[2], item)
  end
  return item
" 2 source_queue dest_queue

# atomic rate limiter (fixed window)
EVAL "
  local count = redis.call('INCR', KEYS[1])
  if count == 1 then
    redis.call('EXPIRE', KEYS[1], ARGV[1])
  end
  return count
" 1 rate:user:1 60

Redis Functions (7.0+)

Functions (7.0+) improve on EVAL scripts: they're named (not just hashed), organized into libraries, and persist across restarts (EVAL scripts are ephemeral). This makes them easier to manage and deploy. FCALL invokes a function by name. Functions are the modern way to add atomic server-side logic. Like scripts, they must be deterministic for replication. FUNCTION DUMP/RESTORE enables backup and migration.

redis-cli
# FUNCTIONS are named, versioned Lua scripts (better than EVAL)
# register a function library
FUNCTION LOAD '#!lua name=mylib
redis.register_function("my_set",
  function(keys, args)
    return redis.call("SET", keys[1], args[1])
  end
)'

# call a function by name
FCALL my_set 1 mykey "hello"

# list loaded libraries
FUNCTION LIST
FUNCTION LIST WITHCODE   # include source

# get the function's code (for backup)
FUNCTION DUMP

# delete a library
FUNCTION DELETE mylib

# restore from a dump (for migration)
FUNCTION RESTORE <payload>

# functions persist across restarts (unlike EVAL scripts)
# they're saved in RDB/AOF
13

Connection Management

CLIENT LIST

CLIENT LIST shows every connected client with address, age, idle time, current command, and flags — invaluable for finding leaks or stuck clients. The 'idle' field helps spot abandoned connections. 'cmd' shows the last command (useful for finding a client running a slow command). 'flags' indicates the client type (N=normal, M=master, S=slave/replica, P=pubsub, b=blocked). Filter by TYPE in 7.0+.

redis-cli
# list all connected clients
CLIENT LIST
# fields: id, addr, laddr, fd, name, age, idle, flags, db, cmd, ...

# filter by type (7.0+)
CLIENT LIST TYPE normal    # normal clients only
CLIENT LIST TYPE replica   # replicas
CLIENT LIST TYPE pubsub    # pub/sub subscribers

# filter by ID (7.0+)
CLIENT LIST ID 1 2 3

# output includes useful fields:
#   addr=10.0.0.1:12345   remote address
#   age=3600              seconds since connected
#   idle=0                seconds since last command
#   cmd=get               last command executed
#   db=0                  current database
#   flags=N               N=normal, M=master, S=slave, P=pubsub

# count connected clients
CLIENT LIST | wc -l

CLIENT SETNAME & GETNAME

CLIENT SETNAME labels a connection so you can identify it in CLIENT LIST — essential for debugging connection leaks in multi-service environments. Set the name immediately after connecting in your app (e.g., 'api-server:pid12345:pool1'). Names appear in the 'name' field of CLIENT LIST. This makes it trivial to find which service owns a stuck or leaked connection.

redis-cli
# name your connection (for identification in CLIENT LIST)
CLIENT SETNAME "my-app-worker-1"

# get the current connection's name
CLIENT GETNAME

# in application code, set the name immediately after connecting:
# redis-cli example:
redis-cli CLIENT SETNAME "debug-session"

# names appear in CLIENT LIST output:
CLIENT LIST
# ... name=my-app-worker-1 ...

# names are useful for:
# - identifying which app/pool a connection belongs to
# - finding leaks (connections that should have been closed)
# - debugging stuck clients
# - naming conventions: app:hostname:pid or pool:name

# names are limited to a reasonable length and cannot contain spaces

CLIENT KILL

CLIENT KILL forcibly disconnects a client — useful for removing stuck or leaky connections. Kill by ADDR (ip:port), ID, TYPE, or USER (ACL). MAXAGE (7.0+) kills clients idle longer than N seconds. SKIPME yes (default) prevents killing your own connection. Use CLIENT LIST first to find the target, then CLIENT KILL to disconnect it. Kills are immediate (no graceful shutdown).

redis-cli
# kill a client by address
CLIENT KILL ADDR 10.0.0.1:12345

# kill a client by ID
CLIENT KILL ID 42

# kill by type (7.0+)
CLIENT KILL TYPE normal    # kill all normal clients
CLIENT KILL TYPE pubsub    # kill all pub/sub clients

# kill by user (ACL, 6.0+)
CLIENT KILL USER appuser

# kill by address with skip-me
CLIENT KILL ADDR 10.0.0.1:0 SKIPME yes   # don't kill myself

# kill all clients matching filters (7.0+)
CLIENT KILL TYPE normal MAXAGE 3600      # kill idle > 1h

# kill returns the number of clients killed
# use with caution — kills are immediate, no graceful shutdown

# common use: disconnect a stuck client consuming resources
CLIENT LIST
CLIENT KILL ID <id>

CLIENT ID & INFO

CLIENT ID returns a unique, never-reused integer for the connection — useful for logging and correlation. CLIENT INFO gives detailed info about your own connection. CLIENT NO-EVICT (6.0+) protects a client's writes from triggering eviction (useful for critical writes). CLIENT REPLY controls whether the server sends replies (OFF for fire-and-forget bulk loads, SKIP for one command).

redis-cli
# get the current client's unique ID (never reused)
CLIENT ID

# get the current client's info (detailed)
CLIENT INFO
# id=42 addr=127.0.0.1:12345 laddr=127.0.0.1:6379 name= db=0 ...

# the ID is a monotonically increasing integer
# it's unique per connection and never reused
# useful for logging and tracking

# CLIENT NO-EVICT (6.0+): this client's writes won't trigger eviction
CLIENT NO-EVICT ON    # protect this client from eviction
CLIENT NO-EVICT OFF   # normal behavior

# CLIENT REPLY (6.0+): control reply mode
CLIENT REPLY ON       # normal replies
CLIENT REPLY OFF      # no replies
CLIENT REPLY SKIP     # skip reply for next command only

# CLIENT UNPAUSE: resume processing after CLIENT PAUSE
CLIENT UNPAUSE

CLIENT PAUSE

CLIENT PAUSE blocks command execution for all other clients for N milliseconds — useful for safe snapshots, migrations, and cluster failovers. WRITE mode (7.0+) pauses only writes while allowing reads to continue (less disruptive). The pausing client itself is not blocked. During pause, queued commands consume memory, so keep pauses short. CLIENT UNPAUSE resumes immediately.

redis-cli
# pause all client processing for N milliseconds
CLIENT PAUSE 10000         # pause for 10 seconds
# all other clients' commands are queued, not executed

# pause modes (7.0+):
CLIENT PAUSE 10000 ALL        # pause all commands (default)
CLIENT PAUSE 10000 WRITE      # pause only writes (reads continue)

# resume processing immediately
CLIENT UNPAUSE

# use cases:
# - safe snapshot/migration (pause writes, take consistent backup)
# - cluster failover (pause to ensure clean handoff)
# - debugging (pause to inspect state)

# during pause:
# - commands from OTHER clients are queued (memory grows)
# - the pausing client can still run commands
# - paused commands execute when the pause ends

# check if a pause is active
CLIENT INFO | grep flags
# 'P' flag = paused

Connection Limits & Timeout

maxclients caps concurrent connections (default 10000) — raise it if you have many app servers or connection pools. timeout disconnects idle clients (0 = never; set to 300s for safety). tcp-keepalive detects dead TCP connections (default 300s). Each client consumes memory for its output buffer — monitor with INFO clients. client-output-buffer-limit prevents one slow client from consuming all server memory (especially important for pub/sub and replicas).

redis-cli
# max concurrent clients
CONFIG GET maxclients        # default 10000
CONFIG SET maxclients 50000  # increase limit

# timeout: disconnect idle clients after N seconds
CONFIG GET timeout           # default 0 (never)
CONFIG SET timeout 300       # 5 minutes

# TCP keepalive (detect dead connections)
CONFIG GET tcp-keepalive     # default 300
CONFIG SET tcp-keepalive 60

# when maxclients is reached, new connections are rejected
# error: "ERR max number of clients reached"

# check current client count vs limit
INFO clients
# connected_clients:150
# blocked_clients:5
# maxclients:10000

# each connection uses memory (~a few KB for the output buffer)
# monitor for output buffer growth:
INFO clients | grep client_recent_max_output_buffer

# set output buffer limits (prevent one client from using all memory)
CONFIG GET client-output-buffer-limit
14

Server Management

INFO Sections

INFO is the primary monitoring command — it reports memory, clients, replication, persistence, and stats in one shot. Filter sections (INFO memory) to reduce output. Key metrics: used_memory_rss (real footprint), mem_fragmentation_ratio (ideal ~1.0-1.5), connected_clients, instantaneous_ops_per_sec, keyspace_hits/misses (cache hit ratio). TIME returns server time for clock synchronization.

redis-cli
# comprehensive server information
INFO                  # all sections
INFO server           # version, uptime, config
INFO clients          # connected clients
INFO memory           # memory usage, fragmentation
INFO persistence      # RDB/AOF status
INFO stats            # ops/sec, hit rate, keyspace
INFO replication      # primary/replica state
INFO cpu              # CPU usage
INFO keyspace         # keys per database
INFO commandstats     # command frequency
INFO errorstats       # error frequency

# get a specific metric
INFO memory | grep used_memory_human
INFO stats | grep instantaneous_ops_per_sec
INFO replication | grep role

# check Redis version and mode
INFO server | grep redis_version
INFO server | grep redis_mode        # standalone, cluster, sentinel

# get server time
TIME                    # [unix_timestamp, microseconds]

CONFIG GET & SET

CONFIG SET changes settings at runtime without restarting Redis — ideal for tuning. CONFIG GET retrieves current values (use patterns like CONFIG GET *memory*). Common runtime tweaks: maxmemory + eviction policy, slowlog threshold, timeout, encoding thresholds. Not all settings are changeable at runtime (e.g., port, bind address require restart). Always test config changes in staging first.

redis-cli
# view config (runtime changes with CONFIG SET)
CONFIG GET maxmemory
CONFIG GET maxmemory-policy
CONFIG GET save
CONFIG GET appendonly
CONFIG GET requirepass

# change config at runtime (no restart needed)
CONFIG SET maxmemory 4gb
CONFIG SET maxmemory-policy allkeys-lru
CONFIG SET slowlog-log-slower-than 10000   # 10ms in microseconds
CONFIG SET timeout 300

# view all config (large output)
CONFIG GET *

# get a specific pattern
CONFIG GET *max*
CONFIG GET *timeout*

# common runtime tweaks:
CONFIG SET maxclients 20000
CONFIG SET hash-max-listpack-entries 256
CONFIG SET appendfsync everysec

CONFIG REWRITE

CONFIG REWRITE persists runtime CONFIG SET changes back to redis.conf — the file is updated in-place, preserving comments and structure. This bridges the gap between runtime changes and persistence across restarts. Always back up redis.conf before REWRITE (rarely causes issues, but safety first). Version-control your redis.conf so you can track and audit changes over time.

redis-cli
# persist config changes to redis.conf
CONFIG REWRITE

# after CONFIG SET maxmemory 4gb
# CONFIG REWRITE updates redis.conf to include:
#   maxmemory 4gb

# the original redis.conf is updated in-place
# comments and structure are preserved

# view the config file path
CONFIG GET *

# typical redis.conf (production):
# bind 127.0.0.1
# protected-mode yes
# port 6379
# maxmemory 4gb
# maxmemory-policy allkeys-lru
# save 3600 1
# appendonly yes
# appendfsync everysec
# tcp-keepalive 300
# timeout 0

# always back up redis.conf before CONFIG REWRITE

SLOWLOG

SLOWLOG records commands slower than the configured threshold (default 10ms = 10000us) — invaluable for finding latency culprits. Set the threshold to your SLO (e.g., 5ms). Common slow commands: KEYS, SMEMBERS on huge sets, HGETALL on huge hashes, SORT on large data. slowlog-max-len caps the log (default 128). Set threshold to 0 to log everything (debugging only — high overhead).

redis-cli
# configure slow log (threshold in microseconds)
CONFIG SET slowlog-log-slower-than 10000   # 10ms (10000 us)
CONFIG SET slowlog-max-len 128             # keep last 128 entries

# view slow commands
SLOWLOG GET          # last 10 (default)
SLOWLOG GET 5        # last 5
SLOWLOG GET 0        # all entries

# each entry:
# 1) id              unique ID
# 2) timestamp       Unix time
# 3) duration        microseconds
# 4) command         the command and args
# 5) client          address:port
# 6) client name     if set

# slowlog length
SLOWLOG LEN

# reset (clear all entries)
SLOWLOG RESET

# set threshold to 0 to log ALL commands (debugging)
# set to -1 to disable
CONFIG SET slowlog-log-slower-than 0

MONITOR

MONITOR streams every command from every client in real time — a powerful debugging tool but with a HUGE performance impact (roughly halves throughput). NEVER use MONITOR in production unless absolutely necessary and only briefly. It's perfect for staging: connect MONITOR, reproduce the issue, see exactly what commands your app sends. For production, use SLOWLOG, LATENCY MONITOR, or INFO commandstats instead.

redis-cli
# MONITOR: real-time log of ALL commands (debugging only!)
MONITOR
# every command from every client is printed:
# 1700000000.123456 [0 10.0.0.1:12345] "SET" "key" "value"
# 1700000000.123567 [0 10.0.0.1:12345] "GET" "key"

# to stop monitoring, send any non-MONITOR command or disconnect
# press Ctrl+C in redis-cli

# WARNING: MONITOR has a HUGE performance impact
# it roughly halves throughput — use only for debugging

# use cases:
# - debugging what commands an app is sending
# - auditing command patterns
# - finding the source of unexpected writes

# safer alternatives for production:
# - SLOWLOG (only slow commands)
# - LATENCY MONITOR (specific events)
# - COMMANDSTATS in INFO (command frequency)

# MONITOR is best used in a staging/dev environment

LATENCY Monitoring

LATENCY MONITOR tracks latency events (set a threshold, e.g., 100ms). LATENCY DOCTOR analyzes recent events and suggests fixes — a great starting point for diagnosis. Common events: 'command' (slow commands), 'fork' (BGSAVE fork delay), 'expire-cycle' (expiration sweep), 'aof-write' (AOF fsync delay). LATENCY GRAPH shows an ASCII timeline. Set the threshold to your SLO and monitor regularly.

redis-cli
# enable latency monitoring (threshold in ms)
CONFIG SET latency-monitor-threshold 100   # 100ms

# view latency events
LATENCY HISTORY event-name
LATENCY LATEST                            # most recent events
LATENCY HISTORY command
LATENCY HISTORY expire-cycle

# ASCII art graph of an event
LATENCY GRAPH command

# human-readable diagnosis and recommendations
LATENCY DOCTOR

# reset latency data
LATENCY RESET
LATENCY RESET event-name

# common events to monitor:
#   command         slow commands
#   fast-command    slow O(1) commands
#   expire-cycle    expiration cycle taking too long
#   fork            BGSAVE/BGREWRITEAOF fork taking too long
#   aof-write       AOF write delays
#   unlink          async delete delays

# check current threshold
CONFIG GET latency-monitor-threshold
15

Cluster

CLUSTER INFO

CLUSTER INFO gives a quick health summary: cluster_state should be 'ok' (all 16384 slots covered), cluster_slots_ok should equal 16384. cluster_known_nodes is the total node count; cluster_size is the number of primaries. If cluster_state is 'fail', some slots are unavailable (likely a primary and its replica both down). Check from any node — the info is cluster-wide.

redis-cli
# cluster-wide health
CLUSTER INFO
# key fields:
#   cluster_state:ok              # ok or fail
#   cluster_slots_assigned:16384  # total slots
#   cluster_slots_ok:16384        # slots covered
#   cluster_known_nodes:6         # total nodes
#   cluster_size:3                # number of primaries
#   cluster_current_epoch:7
#   cluster_my_epoch:1

# cluster_state:ok means all slots are covered
# cluster_state:fail means some slots are unavailable

# check from any node in the cluster
redis-cli -p 7000 CLUSTER INFO

# other cluster commands require being in cluster mode
# (redis-server --cluster-enabled yes)

CLUSTER NODES

CLUSTER NODES is the definitive topology view — one line per node with ID, address, role (master/slave), the master it follows (for replicas), and slot ranges. Look for 'fail' or 'fail?' flags indicating unhealthy nodes. The slot ranges show which primary owns which hash slots (0-16383). CLUSTER SHARDS (7.0+) groups nodes by shard (primary + its replicas).

redis-cli
# view the full cluster topology
CLUSTER NODES
# each line is a node:
# <id> <addr:port@cport> <flags> <master> <ping-sent> <pong-recv> <epoch> <link> <slots>

# flags include:
#   M  = master (primary)
#   S  = slave (replica)
#   mymaster = the node this replica follows
#   fail? = suspected down
#   fail  = confirmed down
#   handshake = new node joining
#   nofailover = failover disabled

# example output:
# 1a2b... 127.0.0.1:7000@17000 myself,master - 0 0 1 connected 0-5460
# 3c4d... 127.0.0.1:7001@17001 master - 0 0 2 connected 5461-10922
# 5e6f... 127.0.0.1:7002@17002 master - 0 0 3 connected 10923-16383
# 7g8h... 127.0.0.1:7003@17003 slave 1a2b... 0 0 4 connected

# count nodes by type
CLUSTER NODES | grep -c master
CLUSTER NODES | grep -c slave

# Redis 7.0+: shard view
CLUSTER SHARDS

CLUSTER KEYSLOT & COUNTKEYSINSLOT

CLUSTER KEYSLOT shows which slot a key maps to (CRC16 % 16384). Hash tags ({...}) force related keys to the same slot — essential for multi-key operations (MGET, transactions, Lua scripts) in a cluster. Use redis-cli -c (cluster mode) to automatically follow MOVED redirects. CLUSTER GETKEYSINSLOT retrieves keys in a slot (used during resharding).

redis-cli
# get the hash slot for a key
CLUSTER KEYSLOT mykey       # e.g., 1234
# CRC16(key) % 16384

# count keys in a specific slot (on the owning node)
CLUSTER COUNTKEYSINSLOT 1234

# get keys in a slot (must be run on the slot's owning node)
CLUSTER GETKEYSINSLOT 1234 10   # up to 10 keys

# in a cluster, keys must be on the same slot for multi-key ops
# use hash tags to force keys to the same slot:
SET {user:1}:profile "alice"
SET {user:1}:cart "items"
# both map to the same slot (the part inside {})

# CLUSTER KEYSLOT {user:1}:profile
# CLUSTER KEYSLOT {user:1}:cart
# same result -> same slot -> same shard

# if you access a key on the wrong node, you get a MOVED redirect:
# (error) MOVED 1234 127.0.0.1:7000
# redis-cli -c follows redirects automatically

Hash Tags

Hash tags ({...}) force keys to the same slot, enabling multi-key operations in a cluster. But use them judiciously — over-using one tag (e.g., {users}:1, {users}:2) creates a hotspot on one shard. Design hash tags around access patterns: group keys accessed together (a user's data, an order's items) but keep unrelated keys separate. This is the core cluster data-modeling challenge.

redis-cli
# hash tags: the substring inside {} determines the slot
# {user:1}:profile and {user:1}:cart share a slot

# enabling multi-key operations in a cluster:
MSET {user:1}:name "Alice" {user:1}:age 30   # OK (same slot)
MSET user:1:name "Alice" user:2:age 30       # CROSSSLOT error

# transactions across keys (must be same slot)
WATCH {order:1}:items
MULTI
HINCRBY {order:1}:items product:1 1
HINCRBY {order:1}:total 10
EXEC

# Lua scripts accessing multiple keys (all keys must share a slot)
EVAL "..." 2 {user:1}:a {user:1}:b

# design hash tags carefully:
# - too broad -> hotspots (one shard overloaded)
#   e.g., {users}:1, {users}:2 -> all on one shard (bad!)
# - too narrow -> can't do multi-key ops
#   e.g., user:1:a, user:1:b -> different slots (can't MGET)

# good: {user:1}:* groups one user's data on one shard
# bad:  {all-users}:* puts everyone on one shard

CLUSTER FAILOVER

Cluster failover is automatic when a primary is unreachable beyond cluster-node-timeout (default 15s). Manual failover is for maintenance (zero-downtime upgrades): FAILOVER is graceful (waits for replication sync), FORCE skips sync (faster, slight data loss risk), TAKEOVER bypasses consensus (emergency only, risks split-brain). After failover, the old primary becomes a replica when it returns. Always run with replicas in production.

redis-cli
# when a primary fails, its replica is promoted automatically
# manual failover (for maintenance), run on the REPLICA:
redis-cli -p 7001 CLUSTER FAILOVER         # graceful (waits for sync)
redis-cli -p 7001 CLUSTER FAILOVER FORCE   # without primary agreement (faster)
redis-cli -p 7001 CLUSTER FAILOVER TAKEOVER # force, may split brain (emergency)

# graceful failover steps:
# 1. replica stops syncing from master
# 2. master stops accepting writes
# 3. replica catches up on replication offset
# 4. replica is promoted to master
# 5. cluster is updated with new master info
# 6. old master becomes a replica when it returns

# CLUSTER RESET resets a node (HARD removes all data)
CLUSTER RESET HARD
CLUSTER RESET SOFT

# check failover status
CLUSTER INFO | grep cluster_state

# rolling upgrade pattern:
# for each replica: CLUSTER FAILOVER (becomes master), upgrade old master

redis-cli --cluster Tool

The redis-cli --cluster tool handles complex cluster operations: creation, resharding, rebalancing, node add/remove, health checks, and repairs. --cluster check is your go-to for verifying cluster health after changes. --cluster fix auto-repairs minor issues (orphaned slots, etc.). --cluster call runs a command on all nodes. Use these instead of manual CLUSTER commands for operational tasks.

redis-cli
# create a cluster (6 nodes: 3 primaries + 3 replicas)
redis-cli --cluster create \
  host1:7000 host2:7000 host3:7000 \
  host1:7001 host2:7001 host3:7001 \
  --cluster-replicas 1

# check cluster health
redis-cli --cluster check host1:7000

# repair issues
redis-cli --cluster fix host1:7000

# reshard (move slots between nodes)
redis-cli --cluster reshard host1:7000
# prompts for: how many slots, target node, source nodes

# rebalance slots across all nodes
redis-cli --cluster rebalance host1:7000

# add a new node
redis-cli --cluster add-node newhost:7000 host1:7000

# add a replica
redis-cli --cluster add-node newhost:7001 host1:7000 \
  --cluster-slave --cluster-master-id <nodeId>

# remove a node (migrates its slots first)
redis-cli --cluster del-node host1:7000 <nodeId>

# call a command on all nodes
redis-cli --cluster call host1:7000 INFO server
16

Sentinel (HA)

Sentinel Setup

Sentinel provides HA without sharding: it monitors a primary, promotes a replica on failure, and reconfigures clients. Quorum (the last number) is how many sentinels must agree a primary is down — set to (N/2)+1 for N sentinels. Deploy 3 sentinels on separate machines for fault tolerance. down-after-milliseconds should be > network jitter; too low causes false failovers.

redis-cli
# Sentinel monitors a primary + replicas, handles failover
# sentinel.conf (port 26379):
sentinel monitor mymaster 192.168.1.10 6379 2
# name, host, port, quorum (number of sentinels to agree on failure)

sentinel down-after-milliseconds mymaster 30000   # 30s to declare down
sentinel failover-timeout mymaster 180000         # 3min failover timeout
sentinel parallel-syncs mymaster 1                # replicas to resync in parallel

# start a sentinel:
redis-sentinel /etc/redis/sentinel.conf
# or:
redis-server /etc/redis/sentinel.conf --sentinel

# deploy at least 3 sentinels (odd number) for quorum
# sentinels communicate with each other and the Redis nodes

# connect to a sentinel to find the current primary:
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster

# sentinel auto-discovers other sentinels and replicas

SENTINEL masters & replicas

SENTINEL masters lists all monitored primaries; SENTINEL master <name> gives details (including num-slaves and num-other-sentinels). SENTINEL replicas shows the replicas; SENTINEL sentinels shows peer sentinels. ckquorum verifies you have enough sentinels for a quorum — run this in monitoring. All major Redis client libraries support Sentinel natively (they query sentinels to find the current primary).

redis-cli
# query sentinel state
redis-cli -p 26379 SENTINEL masters          # all monitored primaries
redis-cli -p 26379 SENTINEL master mymaster  # details of one primary
# fields: name, ip, port, runid, role, ...
#   num-slaves, num-other-sentinels
#   quorum, down-after-milliseconds
#   failover-timeout, parallel-syncs

# list replicas of a master
redis-cli -p 26379 SENTINEL replicas mymaster

# list other sentinels monitoring the same master
redis-cli -p 26379 SENTINEL sentinels mymaster

# count monitoring sentinels (should be >= quorum)
redis-cli -p 26379 SENTINEL sentinels mymaster | grep -c name

# check the number of OK sentinels
SENTINEL ckquorum mymaster
# "OK 3 usable Sentinels. Quorum and failover authorization can be reached"
# or "NOQUORUM" warning (not enough sentinels)

SENTINEL get-master-addr-by-name

SENTINEL get-master-addr-by-name returns the current primary's IP and port — clients call this to discover the primary, then connect to it. On failover, sentinels return the new primary's address. Clients should subscribe to +switch-master events to learn about failovers immediately. This discovery pattern is built into all major Redis client libraries' Sentinel mode.

redis-cli
# find the current primary address
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
# 1) "192.168.1.10"
# 2) "6379"

# clients use this to discover the current primary
# on failover, the sentinel returns the new primary's address

# clients should connect to sentinels, NOT directly to the primary
# libraries: redis-py, jedis, ioredis all support sentinel mode

# example client connection (pseudo-code):
# 1. connect to a sentinel
# 2. SENTINEL get-master-addr-by-name mymaster
# 3. connect to the returned primary
# 4. on connection error, repeat from step 1

# monitor for failover events
redis-cli -p 26379 SUBSCRIBE +switch-master
# publishes: mymaster 192.168.1.10 6379 192.168.1.11 6379

# other events:
redis-cli -p 26379 SUBSCRIBE +failover-state
redis-cli -p 26379 SUBSCRIBE +sdown
redis-cli -p 26379 SUBSCRIBE +odown

SENTINEL failover

SENTINEL failover triggers a manual failover (for rolling upgrades/maintenance). Automatic failover occurs when a primary is down longer than down-after-milliseconds. Failover is automatic but causes a brief write outage (seconds). The elected sentinel leader picks the most up-to-date replica to promote. When the old primary returns, it becomes a replica — writes it accepted during the network partition are LOST (split-brain risk).

redis-cli
# force a manual failover (for maintenance)
SENTINEL failover mymaster
# triggers failover: promotes a replica to primary

# the failover process:
# 1. sentinels mark primary SDOWN (subjectively down)
# 2. quorum agrees -> ODOWN (objectively down)
# 3. a sentinel is elected leader
# 4. leader picks the best replica (most up-to-date)
# 5. promotes the replica: REPLICAOF NO ONE
# 6. reconfigures other replicas to follow the new primary
# 7. updates sentinel configs with the new primary address
# 8. clients reconnect via sentinel discovery

# during failover, writes fail briefly (seconds)
# reads from replicas continue (if the app uses them)

# the old primary, when it returns, is reconfigured as a replica
# of the new primary (its writes during partition are lost)

# monitor failover events:
redis-cli -p 26379 SUBSCRIBE +switch-master
redis-cli -p 26379 SUBSCRIBE +failover-*

Sentinel Quorum & Monitoring

Quorum is the number of sentinels that must agree a primary is down before triggering failover — set to majority (N/2+1) for N sentinels. ckquorum verifies you have enough. down-after-milliseconds should be > network jitter (30s is safe). failover-timeout caps the failover duration. parallel-syncs controls how many replicas resync simultaneously (1 = safe but slow). Sentinels auto-update their config on topology changes.

redis-cli
# quorum: number of sentinels that must agree a master is down
# set in sentinel monitor command:
sentinel monitor mymaster 192.168.1.10 6379 2
#                                          ^ quorum = 2

# for 3 sentinels: quorum = 2 (majority)
# for 5 sentinels: quorum = 3 (majority)

# check quorum status
SENTINEL ckquorum mymaster
# "OK 3 usable Sentinels" or "NOQUORUM"

# tune down-after-milliseconds (time before declaring down)
sentinel down-after-milliseconds mymaster 30000   # 30s default

# tune failover-timeout (max time for failover to complete)
sentinel failover-timeout mymaster 180000   # 3min default

# parallel-syncs: how many replicas resync simultaneously
sentinel parallel-syncs mymaster 1   # 1 at a time (safe, slow)

# view sentinel configuration
SENTINEL masters
SENTINEL master mymaster

# reset a master's state (clears discovered replicas/sentinels)
SENTINEL reset mymaster

# Sentinel automatically updates its config file when topology changes
17

Monitoring

MONITOR Command

MONITOR streams every command from every client in real time. The output format is: timestamp [db client_addr] "command" "args". It's invaluable for debugging what commands an app is actually sending. But it roughly halves throughput — use only in staging/dev or for brief production debugging. Filter with grep for specific patterns. For production monitoring, use SLOWLOG, INFO commandstats, or LATENCY MONITOR instead.

redis-cli
# MONITOR: real-time log of ALL commands (debugging only!)
MONITOR
# every command from every client is printed:
# 1700000000.123456 [0 10.0.0.1:12345] "SET" "key" "value"
# 1700000000.123567 [0 10.0.0.1:12345] "GET" "key"

# format: <unix_time> [<db> <addr>] "<cmd>" "<arg>" ...
# the [0 ...] shows which database and client

# to stop monitoring: Ctrl+C or send any command

# filter for specific patterns (in another terminal):
redis-cli MONITOR | grep "SET"
redis-cli MONITOR | grep "10.0.0.1"
redis-cli MONITOR | grep -E "(SET|DEL)"

# WARNING: MONITOR roughly halves throughput
# use only for debugging, never in production

# safer alternatives:
# - SLOWLOG (only slow commands)
# - INFO commandstats (command frequency)
# - LATENCY MONITOR (latency events)

LATENCY Monitoring

LATENCY MONITOR tracks latency spikes per event type. Set a threshold (e.g., 100ms) and Redis records events that exceed it. LATENCY DOCTOR analyzes recent events and suggests fixes — a great starting point for diagnosis. LATENCY GRAPH shows an ASCII timeline. Common culprits: 'fork' (BGSAVE on large datasets), 'expire-cycle' (mass expiration), 'aof-fsync-always' (AOF fsync). Set the threshold to your SLO and monitor regularly.

redis-cli
# enable latency monitoring (threshold in ms)
CONFIG SET latency-monitor-threshold 100   # 100ms

# view recent latency events
LATENCY LATEST
# event, timestamp, last latency, max latency

# history of an event
LATENCY HISTORY command
LATENCY HISTORY expire-cycle
LATENCY HISTORY fork

# ASCII art graph
LATENCY GRAPH command

# human-readable diagnosis and recommendations
LATENCY DOCTOR

# reset latency data
LATENCY RESET
LATENCY RESET command

# common events:
#   command         slow commands (over threshold)
#   fast-command    slow O(1)/O(log N) commands
#   expire-cycle    key expiration sweep too slow
#   fork            BGSAVE/BGREWRITEAOF fork delay
#   aof-fsync-always  AOF fsync delay
#   unlink          async delete delay

# check threshold
CONFIG GET latency-monitor-threshold

MEMORY DOCTOR & STATS

MEMORY USAGE reports bytes for one key (SAMPLES 0 for full accuracy). MEMORY STATS gives detailed allocator stats. MEMORY DOCTOR provides automated diagnosis (looks for fragmentation, large keys, etc.). Watch mem_fragmentation_ratio: >1.5 wastes memory (fragmentation), <1 means Redis is swapping (catastrophic for latency). MEMORY PURGE tries to return memory to the OS. Active defrag (4.0+) automatically reclaims fragmented memory.

redis-cli
# MEMORY USAGE: bytes used by a single key
MEMORY USAGE mykey
MEMORY USAGE bigset SAMPLES 0   # exact (default samples for large keys)

# MEMORY STATS: detailed allocator stats
MEMORY STATS

# MEMORY DOCTOR: automated memory diagnosis
MEMORY DOCTOR

# MEMORY MALLOC-STATS: low-level allocator stats (jememalloc)
MEMORY MALLOC-STATS

# check total memory used
INFO memory | grep used_memory_human

# key metrics to watch:
#   used_memory             logical bytes used
#   used_memory_rss         RSS from OS (real footprint)
#   used_memory_peak        peak usage
#   mem_fragmentation_ratio >1.5 = fragmentation, <1 = swapping (bad!)

# MEMORY PURGE: attempt to free memory back to OS
MEMORY PURGE

# active defragmentation (4.0+)
CONFIG SET activedefrag yes

DEBUG Commands

DEBUG commands are for diagnostics and testing — use with care. DEBUG SLEEP tests how your app handles latency (blocks the server). DEBUG OBJECT reveals a key's internal encoding and refcount. DEBUG RELOAD saves, flushes, and reloads the dataset (useful for testing persistence). These are powerful but dangerous — restrict with ACLs in production (the -@admin or -@dangerous category). Never run DEBUG on a production primary without understanding the impact.

redis-cli
# DEBUG SLEEP: block the server for N seconds (testing only!)
DEBUG SLEEP 2             # blocks for 2 seconds (testing latency)

# DEBUG OBJECT: raw internal info about a key
DEBUG OBJECT mykey
# Value at:0x7f... refcount:1 encoding:embstr serializedlength:12 ...

# DEBUG SET-ACTIVE-EXPIRE: toggle expiration sweeps
DEBUG SET-ACTIVE-EXPIRE 0  # disable expiration sweeps (debugging)
DEBUG SET-ACTIVE-EXPIRE 1  # re-enable

# DEBUG JMAP: memory map (jemalloc)
DEBUG JMAP

# DEBUG CHANGE-REPL-ID: change replication ID (advanced)
DEBUG CHANGE-REPL-ID

# DEBUG RELOAD: save RDB, flush, reload from RDB (testing)
DEBUG RELOAD

# DEBUG LOADAOF: reload from AOF (testing)
DEBUG LOADAOF

# WARNING: DEBUG commands can be dangerous
# use ACL to restrict them in production:
# ACL SETUSER default -@admin

redis-cli --latency

redis-cli --latency continuously measures round-trip time — essential for baselining network performance. --latency-history logs rolling stats every 15s (use -i to change). --latency-dist shows a histogram (visualizes jitter and tail latency). For cluster mode, check latency to each node separately. Establish a baseline, then monitor for spikes. Latency >1ms on localhost indicates a problem (CPU, memory pressure, or slow commands).

redis-cli
# continuous latency measurement (round-trip time)
redis-cli --latency
# output: min: 0, max: 1, avg: 0.27 (3421 samples)

# latency history (rolling, every N seconds)
redis-cli --latency-history
# default interval: 15 seconds
# output: min: 0, max: 2, avg: 0.31 (1500 samples) -- 15.01 seconds

# custom interval (1 second)
redis-cli --latency-history -i 1

# latency distribution (histogram)
redis-cli --latency-dist
# shows a visual histogram of latency distribution

# latency to a remote host
redis-cli -h remote-host --latency

# use cases:
# - baseline your network latency
# - detect network issues (jitter, spikes)
# - compare latency before/after config changes
# - monitor latency over time with --latency-history

# for cluster mode, check latency to each node:
redis-cli -h node1 -p 7000 --latency &
redis-cli -h node2 -p 7000 --latency &

redis-cli --stat

redis-cli --stat is like 'top' for Redis — it shows a refreshing summary of keys, memory, clients, blocked clients, requests/sec, and connections. The (+N) next to requests shows the delta since the last sample. Use it for quick health checks and to spot anomalies (memory growing, clients leaking, request rate spiking). The 'child' column shows if a BGSAVE/BGREWRITEAOF child is running.

redis-cli
# --stat: continuous server stats (like top for Redis)
redis-cli --stat
# output (refreshes every second):
# ------- data ------ --------------------- load -------------------- - child -
# keys       mem      clients blocked requests            connections
# 1000       1.50M    50      0       12345 (+12)         1000

# --stat shows:
#   keys       total keys in the db
#   mem        memory usage
#   clients    connected clients
#   blocked    blocked clients (BLPOP, etc.)
#   requests   total requests (+N since last sample)
#   connections total connections since start
#   child      child process info (BGSAVE/BGREWRITEAOF)

# custom interval (every 5 seconds)
redis-cli --stat -i 5

# monitor a remote server
redis-cli -h host --stat

# use cases:
# - quick health check (are clients/memory/keys growing?)
# - spot traffic spikes (requests/second)
# - monitor during load tests
# - detect connection leaks (clients increasing steadily)

# combine with --no-raw for machine-readable output
18

Security

AUTH (Legacy Password)

AUTH is the legacy single-password authentication (pre-Redis 6). In Redis 6+, use ACL users (AUTH username password). The -a flag exposes the password in shell history — prefer the REDISCLI_AUTH environment variable. Always use strong passwords (64+ random characters). In production, combine AUTH with TLS, network restrictions (bind/firewall), and ACL-based least privilege for defense in depth.

redis-cli
# set a password (simple, legacy)
CONFIG SET requirepass "strongpassword"

# authenticate (legacy, before Redis 6)
AUTH "strongpassword"
# OK

# authenticate as a specific user (Redis 6+ ACL)
AUTH appuser "secretpassword"
# OK

# connect with password via CLI
redis-cli -a "strongpassword"
redis-cli -a "strongpassword" --no-auth-warning  # suppress warning

# safer: use environment variable
export REDISCLI_AUTH="strongpassword"
redis-cli   # reads REDISCLI_AUTH automatically

# remove password
CONFIG SET requirepass ""

# AUTH returns ERR if the password is wrong
# after AUTH, all commands work normally

ACL SETUSER

Redis 6+ ACLs replace the single password model — create users with scoped permissions. The syntax: on (enabled), >password, ~pattern (key patterns), +command/-command, +@category/-@category (e.g., +@read, -@dangerous). Always create a least-privilege user for each app. The default user always exists — set a strong password or restrict it. Save ACLs to a file (ACL SAVE) for persistence.

redis-cli
# Redis 6+: ACL system with users and permissions
# create a user with limited access
ACL SETUSER appuser on >secretpassword ~app:* +get +set +del -@dangerous
# on = enabled
# >password = set password
# ~pattern = key patterns this user can access
# +command = allowed commands
# -command = denied commands
# +@category / -@category = allow/deny command categories

# categories: @read, @write, @admin, @dangerous, @keyspace, @connection, etc.

# create a read-only user
ACL SETUSER readonly on >pass ~* +@read -@dangerous

# create an admin user
ACL SETUSER admin on >adminpass ~* +@all

# create a user for a specific app (restricted keys)
ACL SETUSER app1 on >pass ~app1:* +@read +@write +@connection -@dangerous

# the default user (always exists)
ACL SETUSER default on >defaultpass ~* +@all -@dangerous

ACL LIST & GETUSER

ACL LIST shows all users in compact rule format. ACL GETUSER gives detailed permissions for one user (including hashed passwords, allowed/denied commands, key patterns). ACL WHOAMI returns the current user. ACL LOG (7.0+) records denied commands and auth failures — useful for security auditing. ACL SAVE/LOAD manage the ACL file for persistence across restarts.

redis-cli
# list all users (compact rules)
ACL LIST
# 1) "user default on nopass ~* +@all"
# 2) "user appuser on #abc123... ~app:* +get +set +del -@dangerous"

# view a user's detailed permissions
ACL GETUSER appuser
# flags, passwords, commands, keys, channels, selectors

# view current user
ACL WHOAMI
# "appuser"

# view all users (just names, 7.0+)
ACL USERS

# delete a user
ACL DELUSER appuser

# save ACLs to a file (persists across restarts)
ACL SAVE

# reload ACLs from file
ACL LOAD

# aclfile in redis.conf:
#   aclfile /etc/redis/users.acl

# view current user's rules
ACL LOG        # security audit log (7.0+)
ACL LOG RESET  # clear the audit log

ACL WHOAMI & DELUSER

ACL WHOAMI identifies the current user. ACL DELUSER removes a user (and disconnects their active connections). Setting a user 'off' temporarily disables them without deleting their rules — useful for suspending access. ACL LOG records security events (denied commands, failed auth) — monitor it for suspicious activity. Rotate passwords regularly with ACL SETUSER >newpass.

redis-cli
# get the current authenticated user
ACL WHOAMI
# "appuser"

# switch user (re-authenticate)
AUTH adminuser "adminpass"
ACL WHOAMI
# "adminuser"

# delete a user (disconnects their clients)
ACL DELUSER tempuser

# temporarily disable a user (keeps the rules, blocks login)
ACL SETUSER appuser off
# the user cannot authenticate, but their rules are preserved
# re-enable:
ACL SETUSER appuser on

# rotate a user's password
ACL SETUSER appuser >newpassword

# remove a user's password
ACL SETUSER appuser <secretpassword  # remove this password
# or remove all passwords:
ACL SETUSER appuser resetpass

# view the audit log (denied commands, auth failures)
ACL LOG
ACL LOG COUNT 5  # last 5 entries
ACL LOG RESET    # clear the log

rename-command (Disable Commands)

rename-command (in redis.conf) disables or renames dangerous commands — set to "" to disable, or a string to rename. It requires a restart (not runtime-changeable). In Redis 6+, prefer ACLs (-flushall, -@dangerous) which are runtime-changeable and per-user. Commands worth restricting: FLUSHALL/FLUSHDB (data loss), KEYS (blocks server), CONFIG (security), DEBUG (can crash), SHUTDOWN (stops server).

redis-cli
# rename or disable dangerous commands (in redis.conf)
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG "CONFIG_9f8a7b"
rename-command KEYS ""
rename-command DEBUG ""
rename-command SHUTDOWN "SHUTDOWN_7x9y"

# "" completely disables the command (returns error)
# a string renames it (must use the new name)

# Redis 6+: prefer ACLs instead of rename-command
# ACL SETUSER default -flushall -config -keys -@dangerous

# view available commands (after rename/restrictions)
COMMAND

# note: rename-command requires a restart (not changeable at runtime)
# ACLs are runtime-changeable and more flexible

# dangerous commands to restrict:
#   FLUSHALL, FLUSHDB  - delete all data
#   KEYS               - blocks server
#   CONFIG             - can change security settings
#   DEBUG              - can crash the server
#   SHUTDOWN           - stops the server
#   MONITOR            - performance impact

TLS/SSL Encryption

TLS encrypts data in transit — essential for production, especially over untrusted networks. Redis 6+ supports TLS natively. mTLS (tls-auth-clients yes) requires clients to present a certificate, providing mutual authentication without passwords. For cluster/sentinel, enable TLS on inter-node communication too. Port 0 disables plain-text; during migration, run both ports temporarily. Always use proper CA-signed certs in production.

redis-cli
# enable TLS in redis.conf (Redis 6+)
port 0                       # disable plain-text port
tls-port 6379
tls-cert-file /etc/redis/redis.crt
tls-key-file /etc/redis/redis.key
tls-ca-cert-file /etc/redis/ca.crt

# connect with TLS
redis-cli --tls --cert client.crt --key client.key \
  --cacert ca.crt -h host -p 6379

# mutual TLS (mTLS) for client authentication
tls-auth-clients yes         # require client certs
tls-auth-clients no          # don't require (default)
tls-auth-clients optional    # verify if provided

# TLS for cluster and replication
tls-cluster yes              # TLS between cluster nodes
tls-replication yes          # TLS between primary/replica

# mix TLS and plain text (transition period)
# port 6380
# tls-port 6379

# generate self-signed certs for testing:
# openssl req -x509 -newkey rsa:4096 -keyout redis.key \
#   -out redis.crt -days 365 -nodes
19

Data Import & Export

redis-cli --rdb (Backup)

redis-cli --rdb performs an online backup — it streams the RDB to a file without touching the server's filesystem directly. It's non-blocking (uses BGSAVE's background fork) and safe for production. Combined with SSH, you can back up a remote Redis to a local file. Verify backups with redis-check-rdb. Schedule regular backups via cron. Always test restore procedures — an untested backup is not a backup.

redis-cli
# dump the RDB snapshot to a file (online backup)
redis-cli --rdb /backup/dump.rdb
# Output: RDB file written to /backup/dump.rdb

# dump RDB to a remote host's file (via SSH):
ssh remote-host "redis-cli --rdb /tmp/dump.rdb"

# the --rdb flag:
# - connects to Redis and requests a BGSAVE
# - streams the RDB to the specified file
# - non-blocking (uses the background fork)
# - safe to run on a production server

# verify the RDB file
redis-check-rdb /backup/dump.rdb

# restore: stop Redis, replace dump.rdb, restart
redis-cli SHUTDOWN
cp /backup/dump.rdb /var/lib/redis/dump.rdb
redis-server /etc/redis/redis.conf

# schedule regular backups:
# 0 2 * * * redis-cli --rdb /backup/dump-$(date +\%F).rdb

redis-cli --pipe (Mass Insert)

redis-cli --pipe is the fastest way to bulk-load data into Redis — it streams raw RESP and doesn't wait for replies until the end. Generate RESP with printf or a script. --pipe reports errors and total replies. For simpler/smaller loads, piping text commands (echo | redis-cli) works but is slower. Always benchmark: --pipe can load millions of keys in minutes. Keep an eye on memory during large loads.

redis-cli
# --pipe: bulk load raw RESP data (fastest method)
# generate RESP and pipe it:

# example: bulk SET commands (bash)
for i in $(seq 1 100000); do
  printf '*3\r\n$3\r\nSET\r\n$%d\r\nkey:%d\r\n$%d\r\nvalue%d\r\n' \
    $((4 + ${#i})) $i $((5 + ${#i})) $i
done | redis-cli --pipe

# simpler text mode (slower but easier):
for i in $(seq 1 100000); do
  echo "SET key:$i value$i"
done | redis-cli

# --pipe mode output:
# All data transferred. Waiting for the last reply...
# Last reply received from server.
# errors: 0, replies: 100000

# prepare RESP from a CSV/data file:
# (each line: key,value)
while IFS=, read -r key value; do
  printf '*3\r\n$3\r\nSET\r\n$%d\r\n%s\r\n$%d\r\n%s\r\n' \
    ${#key} "$key" ${#value} "$value"
done < data.csv | redis-cli --pipe

DUMP & RESTORE

DUMP serializes a key's value to a binary blob (includes type and value, but not TTL — specify TTL in RESTORE in milliseconds). RESTORE deserializes it into a key, working across different Redis instances (great for migrations). The serialized format is version-specific — a blob from Redis 4.x may not load in 7.x; always verify compatibility. For in-instance copies, prefer the native COPY command (6.2+) over DUMP+RESTORE.

redis-cli
# DUMP: serialize a key's value (binary blob)
DUMP mykey
# returns a serialized blob (binary, includes type + value + TTL info)

# RESTORE: deserialize into a key
RESTORE newkey 0 <serialized_blob>
# 0 = no TTL (milliseconds); set a TTL otherwise
RESTORE newkey 3600000 <blob>   # 1 hour TTL
RESTORE newkey 0 <blob> REPLACE  # overwrite if exists

# copy a key (6.2+ does this natively with COPY)
# manual copy via DUMP + RESTORE:
SET source "value"
blob = DUMP source
RESTORE dest 0 blob

# DUMP/RESTORE preserve the type but NOT the TTL
# (TTL must be specified in RESTORE)

# DUMP/RESTORE work across different Redis instances
# (migrate data between servers)

# note: serialized format is version-specific
# a blob from Redis 4.x may not load in 7.x — verify compatibility

# use COPY (6.2+) instead for in-instance copies:
COPY source dest                # copy without TTL
COPY source dest REPLACE        # overwrite destination
COPY source dest DB 1           # copy to another database
COPY source dest REPLACE DB 1   # both

MIGRATE (Move Between Servers)

MIGRATE atomically moves (or copies) a key between Redis instances — the key is never in both places at once (atomic). It's the building block for cluster resharding. COPY keeps the source key; default deletes it. REPLACE overwrites an existing destination key. For multiple keys, pass an empty string as the key and list them with KEYS. Set the timeout high enough for large values. MIGRATE is blocking on both ends during transfer.

redis-cli
# MIGRATE: atomically move a key from one Redis to another
# syntax: MIGRATE host port key|"" dest-db timeout [COPY] [REPLACE]
#         [AUTH password] [KEYS key1 key2 ...]

# move a single key (default: deletes from source after copy)
MIGRATE 192.168.1.20 6379 mykey 0 5000
# host, port, key, dest-db, timeout-ms

# copy without deleting from source
MIGRATE 192.168.1.20 6379 mykey 0 5000 COPY

# overwrite if the key exists on destination
MIGRATE 192.168.1.20 6379 mykey 0 5000 REPLACE

# migrate multiple keys (pass "" as the key, then KEYS ...)
MIGRATE 192.168.1.20 6379 "" 0 5000 KEYS key1 key2 key3

# authenticate with the destination
MIGRATE 192.168.1.20 6379 mykey 0 5000 AUTH destpassword

# MIGRATE is atomic: the key appears on exactly one instance at any moment
# timeout is in milliseconds — large keys need longer timeouts
# returns NOKEY if the key doesn't exist on source

COPY (In-Instance Copy)

COPY (6.2+) duplicates a key within the same instance atomically — far simpler than DUMP+RESTORE. It does NOT copy the TTL by default (use EXPIRE afterward). COPY works across databases with the DB option. REPLACE overwrites an existing destination. For cross-instance copies, use MIGRATE (which is atomic across the network). COPY is the modern replacement for the DUMP+RESTORE pattern for in-instance duplication.

redis-cli
# COPY: duplicate a key within the same instance (6.2+)
COPY source dest               # copy value (no TTL by default)
COPY source dest REPLACE       # overwrite destination if it exists
COPY source dest DB 1          # copy to another database
COPY source dest REPLACE DB 1  # both options

# before COPY existed, you needed DUMP + RESTORE:
SET src "hello"
blob = DUMP src
RESTORE dst 0 blob

# COPY preserves the type (string, list, hash, ...)
SET mystr "hello"
COPY mystr mystr_copy
TYPE mystr_copy               # "string"

# COPY does NOT copy TTL by default — use after EXPIRE if needed
SET temp "data" EX 3600
COPY temp temp_copy
TTL temp_copy                 # -1 (no TTL)
EXPIRE temp_copy 3600         # set TTL manually

# COPY is atomic and never blocks (unlike DUMP on huge keys)

redis-check-rdb & redis-check-aof

redis-check-rdb and redis-check-aof verify the integrity of persistence files — always run them on backups before restoring. redis-check-aof --fix recovers from a truncated AOF by removing the incomplete tail (it prompts for confirmation). Redis 7 uses a multi-part AOF (base + incrementals) — check the manifest file. These tools are read-only except --fix, making them safe to run anytime. Validate backups religiously — an unverified backup is not a backup.

redis-cli
# verify the integrity of an RDB file (offline, safe)
redis-check-rdb /var/lib/redis/dump.rdb
# outputs: [offset] Checksum OK | CRC error | file truncated

# verify an AOF file
redis-check-aof /var/lib/redis/appendonly.aof
# outputs: AOF analyzed: size=... ok_up_to=... ok_up_to_line=...

# fix a truncated AOF (interactive prompt)
redis-check-aof --fix /var/lib/redis/appendonly.aof

# Redis 7 multi-part AOF: check the manifest
redis-check-aof --manifest /var/lib/redis/appendonlydir/appendonly.aof.manifest

# truncate an RDB to a specific offset (recovery)
redis-check-rdb --fix /var/lib/redis/dump.rdb

# use cases:
# - verify a backup before restore
# - diagnose corruption after a crash
# - recover from a truncated AOF (removes the incomplete tail)
# - validate files copied across servers

# always run these checks BEFORE replacing production data files
20

Performance Testing

redis-benchmark Basics

redis-benchmark is Redis's built-in load testing tool — it simulates concurrent clients and reports throughput (requests/sec) and latency percentiles. -t selects tests (PING, SET, GET, INCR, etc.); -n sets request count; -d sets payload size. Always benchmark on production-equivalent hardware to baseline performance. The default 50 parallel clients is a starting point — tune -c to match your real workload. Run benchmarks before and after config changes to measure impact.

redis-cli
# redis-benchmark: built-in performance benchmarking tool
# runs commands against a Redis server and reports throughput/latency

# default benchmark (50 parallel clients, 100000 requests)
redis-benchmark
# default commands: PING, SET, GET, INCR, LPUSH, RPUSH, LPOP, RPOP,
#   SADD, HSET, SPOP, ZADD, ZPOPMIN, LRANGE_100, LRANGE_300, ...

# run a specific command only
redis-benchmark -t set,get -n 100000
# -t = test (comma-separated commands)
# -n = number of requests

# test a specific command with custom data
redis-benchmark -t set -n 100000 -d 100
# -d = data size in bytes (default 3)

# output example:
# ====== SET ======
#   100000 requests completed in 0.85 seconds
#   50 parallel clients
#   3 bytes payload
#   keep alive: 1
#   98.90% <= 1 milliseconds
#   100.00% <= 1 milliseconds
#   117647.05 requests per second

# run against a remote server with auth
redis-benchmark -h host -p 6379 -a password -t set,get -n 100000

Pipeline & Concurrency Tuning

The two biggest throughput levers are -c (concurrency) and -P (pipeline size). -c increases parallel clients (more in-flight requests); -P batches commands per round-trip (less network overhead). Combined, they can push throughput 10-20x. Single-client (-c 1) benchmarks measure pure latency; multi-client benchmarks measure throughput. Larger payloads (-d) shift the bottleneck to network bandwidth. Always tune -c/-P to match your real client library's behavior.

redis-cli
# -c = number of parallel connections (clients)
redis-benchmark -t set -n 100000 -c 50     # default: 50 clients
redis-benchmark -t set -n 100000 -c 200    # 200 concurrent clients
redis-benchmark -t set -n 100000 -c 1      # single client (latency focus)

# -P = pipeline size (commands per round-trip)
redis-benchmark -t set -n 100000 -P 1      # no pipelining (baseline)
redis-benchmark -t set -n 100000 -P 16     # pipeline 16 commands
redis-benchmark -t set -n 100000 -P 100    # pipeline 100 commands

# combine -c and -P for max throughput
redis-benchmark -t set -n 100000 -c 50 -P 16

# typical results (localhost, small values):
#   -c 1  -P 1   -> ~100k req/s  (latency-bound)
#   -c 50 -P 1   -> ~600k req/s  (concurrency)
#   -c 50 -P 16  -> ~2M req/s    (pipelining)

# test with a larger payload (network-bound)
redis-benchmark -t set -n 100000 -d 1024   # 1KB values
redis-benchmark -t set -n 100000 -d 65536  # 64KB values

Latency Percentiles & CSV Output

Always look at latency percentiles (P50, P99, P99.9), not just averages — averages hide tail latency spikes that hurt user experience. A P99 5x worse than P50 indicates jitter (GC, network, or slow commands). --csv outputs machine-readable results for tracking trends over time. Save benchmarks before and after config changes to measure impact objectively. Track P99 latency and requests/sec together — optimizing one at the expense of the other is a common trap.

redis-cli
# --csv: CSV output (easy to parse / log)
redis-benchmark -t set,get -n 100000 --csv
# output: "test","rps","avg_latency_ms","min_latency_ms",...
# "SET","117647.05","0.284","0.144"
# "GET","125000.00","0.272","0.136"

# --precision: set decimal places for latency
redis-benchmark -t set -n 100000 --precision 4

# latency percentiles (Redis 6+):
# the default output shows "X% <= Y milliseconds" lines
# ====== SET ======
#   50.00% <= 0.239 milliseconds
#   99.00% <= 0.511 milliseconds
#   99.90% <= 1.023 milliseconds
#   100.00% <= 1.519 milliseconds

# focus on tail latency (P99, P99.9) — average hides outliers
# P99 > 5x P50 indicates jitter or GC pauses

# save results to a file for comparison
redis-benchmark -t set,get -n 100000 --csv > bench-$(date +%F).csv

# compare two runs (before/after a config change)
diff bench-2025-01-01.csv bench-2025-01-02.csv

Cluster & Lua Benchmarking

--cluster mode distributes keys across cluster slots — essential for realistic cluster benchmarks (single-node benchmarks miss cross-slot routing overhead). -r sets the random keyspace size (use a large value to avoid all ops hitting one key, which inflates numbers via in-memory caching). --eval benchmarks Lua scripts. --threads (Redis 6+) uses multiple benchmark threads — useful when a single thread can't generate enough load to saturate the server. Always use -r for realistic numbers.

redis-cli
# benchmark a Redis Cluster (redis-benchmark supports cluster mode)
redis-benchmark --cluster -h node1 -p 7000 -t set,get -n 100000
# automatically distributes keys across cluster slots

# cluster mode with multiple nodes
redis-benchmark --cluster -h node1 -p 7000 -h node2 -p 7000 \
  -t set,get -n 100000 -c 50

# benchmark a Lua script (--eval)
redis-benchmark -n 100000 eval "return redis.call('SET', KEYS[1], ARGV[1])" 0 mykey hello
# note: key and args are positional after the script

# benchmark with random keys (--random-data + keyspace length)
redis-benchmark -t set -n 100000 -r 1000000
# -r 1000000 = random keys from a 1M keyspace (SET key:RAND_INT value)
# avoids hitting the same key (tests real-world distribution)

# use a random payload (not the same bytes each time)
redis-benchmark -t set -n 100000 -r 1000000 -d 100 --random-data

# only run the benchmark for a fixed time (seconds)
redis-benchmark -t set --threads 4 -n 100000
# --threads (6+) uses multiple threads (default 1, doesn't help single-core)

--idle & Warmup

--idle measures pure connection/network overhead with PINGs only — the baseline you can't beat. Always warm up the server before measuring (cold caches understate performance): run a throwaway benchmark first, then the measured one. --loop runs continuously (useful for sustained-load testing). --seed makes the random keyspace reproducible — essential for A/B comparisons. --dbnum selects the database (use a separate db to avoid polluting production data during benchmarks).

redis-cli
# --idle: only send PINGs (no commands) — measure pure connection overhead
redis-benchmark -t ping -n 100000 --idle
# useful for measuring connection / network baseline latency

# --dbnum: select a specific database
redis-benchmark -t set -n 100000 --dbnum 1

# --clients: same as -c (alias)
redis-benchmark -t set -n 100000 --clients 100

# warm up the server before measuring (avoid cold-start bias)
redis-benchmark -t set -n 10000   # warmup run (discard results)
redis-benchmark -t set -n 100000  # measured run

# --loop: run the benchmark forever (Ctrl+C to stop)
redis-benchmark -t set -n 100000 --loop

# --seed: reproducible random keys (same -r sequence every run)
redis-benchmark -t set -n 100000 -r 1000000 --seed 42

# benchmark with a specific client socket buffer
redis-benchmark -t set -n 100000 --numdatadb 1

Benchmark Interpretation & Pitfalls

Benchmark pitfalls inflate numbers unrealistically: (1) localhost-only benchmarks miss network latency; (2) without -r, all ops hit one key (inflated by caching); (3) tiny payloads don't stress memory/network; (4) ignoring persistence overhead (AOF fsync can cut throughput 2-5x); (5) a single benchmark thread can't saturate a multi-core server. A realistic benchmark uses -r (random keys), -d (realistic payload), --threads (multi-threaded client), and tests with production-equivalent persistence and network. Always disclose the benchmark parameters when sharing numbers.

redis-cli
# COMMON PITFALL #1: benchmarking localhost only
# localhost numbers don't reflect real network latency
# always test over your production network path

# COMMON PITFALL #2: all keys the same (no -r)
# redis-benchmark -t set -n 100000
#   -> all SETs hit "key:0" — inflated by in-memory caching
# FIX: use -r to randomize keys
redis-benchmark -t set -n 100000 -r 1000000

# COMMON PITFALL #3: tiny payloads only
# 3-byte values don't reflect real memory/network pressure
# test with realistic sizes
redis-benchmark -t set -n 100000 -d 256 -r 1000000

# COMMON PITFALL #4: ignoring persistence overhead
# benchmark with AOF on vs off to see the durability cost:
redis-benchmark -t set -n 100000   # with current persistence settings
# (then) CONFIG SET appendonly no
redis-benchmark -t set -n 100000   # without AOF — compare

# COMMON PITFALL #5: single-threaded benchmark client
# one benchmark thread may not generate enough load
redis-benchmark -t set -n 100000 --threads 4 -c 100

# GOOD BENCHMARK (realistic):
redis-benchmark -t set,get -n 1000000 -c 50 -P 16 -r 1000000 -d 100 --threads 4 --csv

Was this helpful?