Redis: Strings, Hashes, Sorted Sets and Streams Reference Guide
By DevShelfHub
redis-cli, strings, hashes, lists, sets, sorted sets, streams, pub/sub, scripting, persistence, cluster — the full data-structure store.
122 items
◷ 8 min
Types
Streams
Lua
Start hereQuick start · 6 you’ll reach for daily
String with TTLSET k v EX 60
Hash mappingHSET u:1 name Ana plan pro
CounterINCR pageviews
Top-N leaderboardZADD lb 1500 alice
Append to streamXADD events * k v
Pipeline (batch)r.pipeline(); ...; .execute()
Target versions · paceVersions
Targets:redis ≥ 7.2redis-py ≥ 5.0RESP3 protocol
Redis 7 added FUNCTION (replaces ad-hoc EVAL scripts),
sharded pub/sub, ACL v2, and the new RESP3 wire protocol.
Streams (5+), HyperLogLog (2.8+) and Geo (3.2+) are stable. License changed to SSPL/RSALv2 in 7.4 —
if you need OSS-pure, valkey is the fork. This sheet targets Redis 7.2+.
Install · connectSetup
bash
# macOS — Homebrew
brew install redis
brew services start redis
# Linux — Debian/Ubuntu
sudo apt install redis-server
sudo systemctl enable --now redis-server
# Docker — disposable dev instance
docker run --name redis -p 6379:6379 -d redis:7
# Connect (CLI)
redis-cli # localhost:6379
redis-cli -h prod.example -p 6380 -a $PW # remote + auth
redis-cli -u "rediss://default:$PW@host:6380/0" # URL form (TLS)
# Python — modern client (sync + async in one package)
pip install "redis>=5.0"
# Test the connection
redis-cli PING
# → PONG
The CLIredis-cli essentials
redis-cli -h host -p 6379 -a $PW
Connect with auth.
redis-cli -u rediss://user:pw@h:6380/0
URL form, TLS.
redis-cli --scan --pattern 'user:*'
Preferred non-blocking key scan. Never use KEYS in prod.
redis-cli MONITOR
Live tail of every command. Heavy — debug only.
redis-cli --latency / --latency-history
Network + server latency probes.
redis-cli --bigkeys
Find the biggest key per type. Hot spots.
redis-cli --memkeys --memkeys-samples 100
Sampled memory profile by key.
redis-cli ping
Health check.
redis-cli -x SET k
Read value from stdin (good for piping files).
redis-cli --rdb dump.rdb
Pull a snapshot off a remote.
Lifecycle · expiryKeys & expiration
EXISTS k
1 if present.
DEL k [k ...]
Synchronous delete.
UNLINK k [k ...]
Async delete in background. Preferred for big keys.
TYPE k
string / hash / list / set / zset / stream.
EXPIRE k 60 / PEXPIRE k 60000
TTL in seconds / ms.
EXPIREAT k <unix>
Absolute expiry.
TTL k / PTTL k
-2 if missing, -1 if no TTL.
PERSIST k
Remove TTL.
RENAME k newk
Rename atomically.
SCAN 0 MATCH 'user:*' COUNT 1000
Cursor-based scan. Safe under load.
OBJECT ENCODING k
Internal encoding (embstr, listpack, ...). Useful for memory tuning.
DBSIZE / FLUSHDB
Count keys / wipe current db. FLUSHDB is destructive — never in prod.
The default typeStrings & counters
SET k v
Set or overwrite.
SET k v EX 60
Set with TTL.
SET k v NX EX 60
Set only if missing. Preferred over SETNX + EXPIRE (atomic).
SET k v XX
Set only if exists.
SET k v GET
Return the previous value.
GET k / GETDEL k / GETEX k EX 60
Read; read+delete; read+touch TTL.
MGET k1 k2 ... / MSET k1 v1 k2 v2
Multi-key read / write in one round-trip.
INCR k / INCRBY k 5 / INCRBYFLOAT k 0.5
Atomic counters. Initial 0.
DECR k / DECRBY k 5
Decrement.
APPEND k suffix
Append; returns new length.
STRLEN k / GETRANGE k 0 7
Length / substring.
Objects in one keyHashes
HSET k f1 v1 f2 v2
Set fields. Returns # of new fields.
HGET k f / HMGET k f1 f2
Read fields.
HGETALL k
All fields + values. Avoid on huge hashes.
HEXISTS k f
1 if field present.
HDEL k f [f ...]
Drop fields.
HINCRBY k f 1
Atomic per-field counter.
HKEYS k / HVALS k / HLEN k
Inspect shape.
HSCAN k 0 MATCH 'pref:*'
Cursor scan within a big hash.
HEXPIRE k 60 FIELDS 1 f
Per-field TTL (7.4+).
Queues · stacksLists
LPUSH k v [v ...] / RPUSH k v
Push to head / tail. LPUSH+RPOP = FIFO queue.
LPOP k / RPOP k
Pop from head / tail.
LMPOP 2 q1 q2 LEFT COUNT 10
Multi-key pop with count (7+).
BLPOP k 5
Blocking pop — classic worker pattern.
LRANGE k 0 -1
Slice. -1 = last.
LLEN k
Length.
LINDEX k 0 / LSET k 0 v
Random access (O(N) past head/tail).
LREM k 1 v
Remove first match.
LMOVE src dst LEFT RIGHT
Atomic move. Preferred over deprecated RPOPLPUSH.
LTRIM k 0 99
Cap a list to first N (e.g. rolling activity feed).
Unique membersSets
SADD k m [m ...]
Add members. Duplicates ignored.
SISMEMBER k m / SMISMEMBER k m1 m2
Membership check (single / multi).
SREM k m
Remove.
SMEMBERS k
All members. Avoid on huge sets.
SCARD k
Cardinality.
SINTER a b / SUNION a b / SDIFF a b
Intersect / union / difference.
SINTERSTORE dest a b
Store the result. Useful for materialized cohorts.
SRANDMEMBER k 3 / SPOP k
Random sample / atomic random remove.
SSCAN k 0
Cursor scan.
Leaderboards · priority queuesSorted sets (ZSET)
ZADD k score member [score member ...]
Add / update with score.
ZADD k NX 100 m / ZADD k XX 100 m
Only-if-missing / only-if-exists variants.
ZADD k GT 100 m
Update only if new score > old.
ZINCRBY k 1 m
Atomic score bump.
ZRANGE k 0 9 WITHSCORES
Index-based range.
ZRANGE k 0 9 REV WITHSCORES
Top-N. Preferred over deprecated ZREVRANGE.
ZRANGEBYSCORE k 100 200 LIMIT 0 10
Score window with pagination.
ZRANK k m / ZREVRANK k m
0-based rank.
ZSCORE k m / ZMSCORE k m1 m2
Read score(s).
ZREM k m / ZREMRANGEBYSCORE k -inf 100
Remove by member or score range.
ZPOPMIN k / ZPOPMAX k
Priority-queue pop.
BZPOPMIN k 5
Blocking pop with timeout.
Append-only log · consumer groupsStreams
XADD k * field val [field val ...]
Append entry. * = auto id.
XADD k MAXLEN ~ 10000 * ...
Approximate trim — cheap, keeps newest ~10k.
XLEN k
Entry count.
XRANGE k - +
All entries (replay).
XREAD COUNT 10 BLOCK 5000 STREAMS k $
Fan-out read. $ = only new.
XGROUP CREATE k group $ MKSTREAM
Create consumer group at tail.
XREADGROUP GROUP g c-1 COUNT 10 BLOCK 5000 STREAMS k >
Worker reads its share. > = new only.
XACK k group id
Mark entry as processed.
XPENDING k group
Claimed-but-unacked entries (PEL).
XAUTOCLAIM k group new-c 60000 0
Reassign entries idle for 60s.
XDEL k id / XTRIM k MAXLEN 1000
Delete entry / trim length.
bash
# Append an event ("*" → server-generated id)
XADD orders * sku abc qty 3
# → "1715944800123-0"
# Create a consumer group starting from the current end ("$")
XGROUP CREATE orders billing '$' MKSTREAM
# A worker reads new events (blocks up to 5s)
XREADGROUP GROUP billing worker-1 COUNT 10 BLOCK 5000 STREAMS orders '>'
# Acknowledge after processing
XACK orders billing 1715944800123-0
# See still-pending (claimed but un-ACKed) entries
XPENDING orders billing
XAUTOCLAIM orders billing worker-2 60000 0 # steal stuck messages
# Cap stream length (lossy trim — keeps newest ~10k)
XADD orders MAXLEN '~' 10000 * sku abc qty 3
# Range scan (useful for replay)
XRANGE orders - + COUNT 100
Fire-and-forget messagingPub/Sub
SUBSCRIBE channel [c ...]
Listen on channels. No persistence — messages lost if no subscribers.
UNSUBSCRIBE [c ...]
Stop listening.
PSUBSCRIBE 'news.*'
Pattern subscription.
PUBLISH channel "hello"
Send. Returns # of subscribers reached.
SPUBLISH / SSUBSCRIBE
Sharded pub/sub (7+) — cluster-safe.
PUBSUB CHANNELS [pattern]
Active channels.
PUBSUB NUMSUB ch
Subscriber count per channel.
Pub/Sub has no delivery guarantees and no replay. For at-least-once with backpressure, use Streams + consumer groups.
MULTI · WATCH · LuaTransactions & scripting
MULTI ... EXEC
Queue + atomically execute. No mid-tx logic.
DISCARD
Abort a queued transaction.
WATCH k
Optimistic CAS — EXEC aborts if k changed.
EVAL "script" 1 k1 arg1
Run Lua server-side. Atomic.
EVALSHA sha 1 k1 arg1
Cached script by hash. Preferred for hot paths.
SCRIPT LOAD "..."
Preload + get SHA.
FUNCTION LOAD "..."
Replaces EVAL with named, versioned, replicated functions (7+).
FCALL fn 1 k1 arg1
Call a loaded function.
sql
-- Safe lock release: only delete if the value still matches our token.
-- Run via EVAL — KEYS[1] = lock key, ARGV[1] = token.
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
RDB · AOFPersistence & durability
SAVE
Synchronous snapshot. Blocks the server — rarely used.
BGSAVE
Fork + snapshot in background (RDB).
LASTSAVE
Unix ts of last successful save.
BGREWRITEAOF
Compact the AOF log.
CONFIG SET appendonly yes
Enable AOF. Pair with appendfsync everysec.
save 3600 1 300 100 60 10000
RDB triggers (changes in window). redis.conf default.
aof-use-rdb-preamble yes
Hybrid AOF — faster restart, smaller log. Default since 4.0.
Attach / detach as replica. Preferred over SLAVEOF.
INFO replication
Role, lag, connected replicas.
WAIT numreplicas timeout-ms
Block until N replicas ACK. Best-effort durability.
CLUSTER INFO / CLUSTER NODES
Topology + slot ownership.
CLUSTER SLOTS / CLUSTER SHARDS
Slot → node map.
CLUSTER KEYSLOT k
Which slot a key hashes into.
{tag}suffix / user:{42}:name
Hash-tag braces force keys to the same slot (multi-key ops).
CLUSTER FAILOVER [FORCE]
Trigger replica promotion.
redis-cli --cluster reshard host:port
Move slots between shards.
maxmemory · eviction policiesMemory & eviction
CONFIG SET maxmemory 2gb
Cap memory usage.
maxmemory-policy noeviction
Default. New writes error when full.
allkeys-lru / allkeys-lfu
Evict by recency / frequency across all keys. Cache mode.
volatile-lru / volatile-lfu / volatile-ttl
Only evict keys that have a TTL.
allkeys-random / volatile-random
Random eviction.
MEMORY USAGE k
Bytes for a single key.
MEMORY STATS
Global memory breakdown.
INFO memory
used_memory_human, mem_fragmentation_ratio.
DEBUG OBJECT k
Internals + encoding for a key.
Sync · async · pipelinesPython with redis-py
python
from redis import Redis
from redis.asyncio import Redis as AsyncRedis
# Sync — connection pool is created for you
r = Redis(host="localhost", port=6379, db=0, decode_responses=True)
r.set("user:42:name", "Ana", ex=3600) # TTL 1h
r.get("user:42:name") # "Ana"
# Pipeline — batch commands, one round-trip
with r.pipeline(transaction=False) as pipe:
pipe.hset("user:42", mapping={"name": "Ana", "plan": "pro"})
pipe.expire("user:42", 86400)
pipe.zadd("leaderboard", {"42": 1500})
pipe.execute()
# Transaction — WATCH for optimistic concurrency
def increment_if_below(key, cap):
with r.pipeline() as pipe:
while True:
try:
pipe.watch(key)
cur = int(pipe.get(key) or 0)
if cur >= cap:
pipe.unwatch(); return cur
pipe.multi()
pipe.incr(key)
pipe.execute()
return cur + 1
except r.WatchError:
continue # somebody else wrote — retry
# Async
async def main():
a = AsyncRedis.from_url("redis://localhost", decode_responses=True)
await a.set("k", "v"); print(await a.get("k"))
await a.aclose()
Rate-limit · leaderboard · cacheEnd-to-end · three patterns
Three minimal patterns developers reach for daily — all on stock Redis 7. Adapt the keys; the logic stays.
python
# Tiny rate-limiter: N requests per minute per user.
# Uses INCR + EXPIRE — both run atomically in one pipeline.
from redis import Redis
r = Redis(decode_responses=True)
def allow(user_id: str, limit: int = 60, window_sec: int = 60) -> bool:
key = f"rl:{user_id}:{int(time.time()) // window_sec}"
with r.pipeline() as pipe:
pipe.incr(key)
pipe.expire(key, window_sec)
count, _ = pipe.execute()
return count <= limit
# Leaderboard with sorted set
r.zadd("leaderboard", {"alice": 1500, "bob": 1820, "carol": 1730})
r.zrevrange("leaderboard", 0, 2, withscores=True)
# → [('bob', 1820.0), ('carol', 1730.0), ('alice', 1500.0)]
# Cache-aside read with TTL
def get_user(user_id):
key = f"user:{user_id}"
cached = r.get(key)
if cached: return json.loads(cached)
row = db.fetch_user(user_id) # cache miss → source of truth
r.set(key, json.dumps(row), ex=300) # cache 5 min
return row
Best practiceGood to know
Pipelines are not transactions.
A pipeline batches commands into one round-trip — nothing more. For atomicity, wrap in MULTI/EXEC or use a Lua script.
Use SET k v NX EX 60, never SETNX + EXPIRE.
The two-command form races: a crash between commands leaves a key without TTL. The single-command form is atomic.
Cluster needs hash tags for multi-key commands.MSET user:1:name x user:2:name y errors in cluster mode — the keys hash to different slots. Use {group} braces to pin related keys to the same slot.
Common trapsWatch out for
Never run KEYS in production.KEYS * is O(N) and blocks the single-threaded event loop. Use SCAN, which is cursor-based and incremental.
Pub/Sub drops messages with no listeners.
Subscribers receive only what’s published while they’re online — no buffering, no replay. Use Streams when you need durability.
A big key blocks every other client.
Redis runs one command at a time. A single DEL of a 1M-element hash freezes the whole server. Use UNLINK or trim in chunks; spot them with redis-cli --bigkeys.
Redis is an in-memory data structure store used as a cache, message broker, and primary database. Common use cases include session storage, rate limiting, leaderboards (sorted sets), pub/sub event streaming, distributed locks, and task queues via Redis Streams.
What is the difference between Redis Strings and Hashes?
Strings store a single value per key — raw text, a JSON blob, or a counter you can INCR. Hashes store a map of field-value pairs under one key, like a lightweight database row. Use Hashes when you want to read or update individual fields without fetching the entire value.
What are Redis Sorted Sets used for?
Sorted sets store members with a floating-point score, kept in sorted order automatically. They are ideal for leaderboards (ZADD, ZRANK, ZRANGE), time-ordered events, rate limiting sliding windows, and priority queues. Lookups by rank and by score are both O(log N).
Does Redis persist data to disk?
Yes. Redis offers two persistence modes: RDB (point-in-time snapshots on a configurable schedule) and AOF (Append-Only File that logs every write). You can use both together — AOF for durability, RDB for fast restarts. If you need no persistence at all, both can be disabled for pure cache mode.
Is Redis free to use?
Redis Community Edition (up to 7.2) was open source under BSD. Since version 7.4, Redis uses a dual SSPL/RSALv2 license. The Valkey fork maintains the BSD license for OSS-pure environments. Redis Cloud offers a free hosted tier. The redis-py Python client remains MIT-licensed.