Filter concepts by levelShowing all levels.

Python · Redis

Redis

Key-value storage, the five data structures, TTL-based expiration, atomicity, messaging, distributed locks, and caching — verified against a real executing Redis client

  • One store, five shapesstrings, lists, sets, sorted sets, and hashes under one key model
  • TTL everywhereex= on any key turns it into a self-cleaning cache entry
  • SET NX PXthe acquire half of a distributed lock — release needs a token check
  • Pub/Sub vs. Streamsbroadcast with no history, or a durable, replayable, ID-ordered log

1.Key-value storage and strings

Redis stores every value under a unique text key, like a giant dictionary that lives outside your process. r.set("user:1001:name", "Priya Shah") writes it; r.get("user:1001:name") reads it back — a string is the simplest value a key can hold.

Key features

  • r.set(key, value) writes a string value; r.get(key) reads it back, or returns None if the key does not exist
  • r.incr(key) / r.incrby(key, n) atomically add to a value Redis parses as an integer — no read-modify-write race
  • r.setnx(key, value) only sets the value if the key does not already exist yet — a cheap "claim this key once" check
  • r.exists(key) returns 1 if the key exists and 0 if it does not — not a Python bool
  • A colon-separated key like "user:1001:name" is a naming convention, not special syntax — Redis treats the whole string as one opaque key

Core string commands

Core string commands
Command (redis-py)EffectReturns
r.set(key, value)Writes value, overwriting any existing value at keyTrue
r.get(key)Reads the value at keythe value, or None if absent
r.mset({...}) / r.mget(*keys)Sets or gets several keys in one round tripTrue / list, with None for missing keys
r.incr(key) / r.incrby(key, n)Atomically increments an integer-valued key by 1 or nthe new integer value
r.append(key, value)Appends value to the end of an existing string, or creates itthe new string length
r.strlen(key)Returns the length of the string in bytesan int
Redis as a key-value store

r.set("user:1001:name", "Priya Shah")

key → value, written in memory

Redis server

holds every key across every connected client

r.get("user:1001:name")

returns 'Priya Shah'

  1. r.set("user:1001:name", "Priya Shah") — key → value, written in memory
  2. Redis server — holds every key across every connected client
  3. r.get("user:1001:name") — returns 'Priya Shah'

Command

python
import redis

r = redis.Redis(host="localhost", port=6379, decode_responses=True)
r.set("session:42", "active")
r.get("session:42")          # 'active'
r.get("no_such_key")         # None

Every Redis value lives under a unique string key; r.set/r.get read and write it, and r.incr/r.incrby update a numeric one atomically without a read-modify-write race.

2.Lists

A Redis list holds an ordered sequence of strings under one key. r.rpush appends to the right end, r.lpush prepends to the left, and r.lrange(key, 0, -1) reads the whole thing back in order.

Key features

  • r.rpush(key, *values) appends one or more values to the right (tail) end; r.lpush prepends to the left (head)
  • r.lrange(key, 0, -1) returns the whole list in order — 0 to -1 means "first element to last"
  • r.lpop(key) / r.rpop(key) remove and return one element from an end; both return None on an empty list
  • r.blpop(key, timeout) blocks the connection until an element is available or timeout seconds pass — the basis of a work queue
  • Indexing into the middle of a long list (r.lindex, r.linsert) is O(N) — lists are for queue/stack access patterns, not random access

Command

python
r.rpush("recent_orders", "order:1", "order:2")   # append
r.lpush("recent_orders", "order:0")               # prepend
r.lrange("recent_orders", 0, -1)   # ['order:0', 'order:1', 'order:2']
r.lpop("recent_orders")            # 'order:0', removed
r.llen("recent_orders")            # remaining length

r.rpush/r.lpush push onto either end in O(1); r.lrange(key, 0, -1) reads the whole list; r.blpop blocks for work instead of polling.

3.Sets and sorted sets

A Redis set (r.sadd) holds unique unordered members with fast O(1) membership checks via r.sismember. A sorted set (r.zadd) is the same, but each member also carries a float score, and members always come back ordered by score.

Key features

  • r.sadd(key, *members) adds members to a set; adding an existing member is a silent no-op — sets never have duplicates
  • r.sismember(key, member) is an O(1) membership check — the main reason to reach for a set over a list
  • r.sinter / r.sunion / r.sdiff compute intersection/union/difference across sets on the server
  • r.zadd(key, {member: score, ...}) adds members with a score; r.zrange(key, 0, -1, withscores=True) reads them back in ascending score order
  • r.zincrby(key, amount, member) atomically adds to one member's score — the standard way to bump a leaderboard score

Set and sorted-set commands

Set and sorted-set commands
Command (redis-py)StructureEffect
r.sadd(key, *members)setAdds members; duplicates are ignored
r.smembers(key)setReturns all members as a Python set
r.sismember(key, m)setO(1) check: is m in the set
r.sinter(k1, k2) / r.sunion / r.sdiffsetServer-side intersection / union / difference
r.zadd(key, {m: score})zsetAdds/updates member m with a float score
r.zrange(key, 0, -1, withscores=True)zsetMembers in ascending score order, with scores
r.zrevrange(key, 0, n)zsetTop n+1 members, highest score first
r.zscore(key, m) / r.zrank(key, m)zsetMember m's score, or its 0-based rank (ascending)
Set vs. sorted set

Set — r.sadd

  • Unique, unordered members
  • O(1) r.sismember membership check
  • sinter/sunion/sdiff between sets
  • Use for: tags, "seen" tracking, dedup

Sorted set — r.zadd

  • Unique members, each with a float score
  • Always readable in score order
  • zrevrange for "top N", zincrby to bump a score
  • Use for: leaderboards, ranked queues, rate windows
  • Set — r.sadd
    • Unique, unordered members
    • O(1) r.sismember membership check
    • sinter/sunion/sdiff between sets
    • Use for: tags, "seen" tracking, dedup
  • Sorted set — r.zadd
    • Unique members, each with a float score
    • Always readable in score order
    • zrevrange for "top N", zincrby to bump a score
    • Use for: leaderboards, ranked queues, rate windows

Command

python
r.sadd("tags:post42", "python", "redis", "backend")
r.sismember("tags:post42", "redis")   # True

r.zadd("leaderboard", {"alice": 100, "bob": 250})
r.zrevrange("leaderboard", 0, 0, withscores=True)  # [('bob', 250.0)]

r.sadd/r.sismember give O(1) unique-membership; r.zadd/r.zrevrange keep members ordered by a float score — reach for a zset the moment "top N" or "rank" enters the requirement.

4.Hashes

A Redis hash stores field-value pairs under one key, like a dict nested under a single top-level key. r.hset("user:1001", mapping={"name": "Priya", "logins": 4}) writes it; r.hget("user:1001", "name") reads one field back.

Key features

  • r.hset(key, mapping={field: value, ...}) sets one or more fields on the hash at key in a single call
  • r.hget(key, field) reads one field; r.hgetall(key) reads every field back as a dict
  • r.hincrby(key, field, n) atomically increments a numeric field, the same guarantee r.incr gives a plain string key
  • r.hdel(key, field) removes a single field without touching the rest of the hash
  • r.hexists(key, field) checks whether a field is present without transferring its value

Command

python
r.hset("user:1001", mapping={"name": "Priya Shah", "logins": 4})
r.hget("user:1001", "name")     # 'Priya Shah'
r.hgetall("user:1001")          # {'name': 'Priya Shah', 'logins': '4'}
r.hincrby("user:1001", "logins", 1)

r.hset(key, mapping={...}) writes several fields under one key; r.hget/r.hgetall read one field or all of them; r.hincrby updates a numeric field atomically.

5.TTL and expiration

A TTL (time to live) is a countdown Redis attaches to a key; when it hits zero, Redis deletes the key by itself. r.expire("otp:42", 300) gives a key 300 seconds to live; r.ttl("otp:42") checks how many seconds remain.

Key features

  • r.expire(key, seconds) sets or replaces a TTL in seconds; r.ttl(key) returns seconds remaining
  • r.set(key, value, ex=seconds) sets the value and TTL together in one call — the most common pattern for a cache entry
  • r.ttl(key) returns -1 for "exists, no TTL" and -2 for "key does not exist" — never a negative countdown
  • r.persist(key) removes a key's TTL, making it live forever again, without touching its value
  • A plain r.set(key, value) with no ex=/px=/keepttl= silently clears any TTL the key already had

Setting and reading TTLs

Setting and reading TTLs
Command (redis-py)UnitEffect
r.expire(key, n)secondsSet/replace TTL to n seconds from now
r.pexpire(key, n)millisecondsSame, with millisecond precision
r.expireat(key, unix_ts)secondsSet TTL to expire at an absolute Unix timestamp
r.ttl(key) / r.pttl(key)seconds / msRemaining time; -1 no TTL, -2 key missing
r.persist(key)—Removes the TTL — key lives forever until deleted
r.set(key, value, ex=n, keepttl=True)secondsex= sets a new TTL; keepttl=True keeps the key's existing TTL instead
A key with a 300-second TTL
  1. t=0s

    r.set("otp:42", "914213", ex=300)

    value written, 300s countdown starts

  2. t=299s

    r.ttl("otp:42") → 1

    still readable, one second left

  3. t=300s

    key expires

    Redis deletes it automatically

  4. t=301s

    r.get("otp:42") → None

    r.ttl("otp:42") → -2 (does not exist)

  1. t=0s: r.set("otp:42", "914213", ex=300) — value written, 300s countdown starts
  2. t=299s: r.ttl("otp:42") → 1 — still readable, one second left
  3. t=300s: key expires — Redis deletes it automatically
  4. t=301s: r.get("otp:42") → None — r.ttl("otp:42") → -2 (does not exist)

Command

python
r.set("otp:user42", "914213")
r.expire("otp:user42", 300)     # expires in 300 seconds
r.ttl("otp:user42")             # 300

r.set("cache:homepage", "<html>...</html>", ex=60)   # value + TTL together
r.ttl("cache:homepage")         # 60

A TTL is a self-deleting countdown on a key (r.expire/r.ttl); a plain r.set() clears it unless you pass ex=/keepttl=True — -1 means no TTL, -2 means the key is gone.

6.Atomic operations

A single Redis command like r.incr always runs atomically — no other client's command can interleave in the middle of it. r.pipeline(transaction=True) groups several commands into one all-or-nothing MULTI/EXEC block when you need more than one command to be atomic together.

Key features

  • Every individual Redis command (incr, hset, sadd, lpush, ...) is already atomic — no client-side lock is needed for one command
  • r.pipeline() batches several commands into one network round trip, cutting latency, but is not atomic unless transaction=True (the default)
  • pipe.execute() sends the whole queued batch and returns a list of results, one per command, in order
  • pipe.watch(key) marks a key to monitor; if it changes before pipe.execute(), Redis aborts the transaction with a WatchError instead of running it
  • incr/incrby/hincrby/sadd/zadd are the built-in atomic alternative to "read a value in Python, modify it, write it back" for the single-key case
WATCH / MULTI / EXEC — optimistic locking
if keychanged

pipe.watch("stock:sku9")

start monitoring the key

read current value

decide the new value in Python

pipe.multi() + queue writes

pipe.execute()

runs atomically — unless key changed

WatchError → retry the loop

  • pipe.watch("stock:sku9") — start monitoring the key
    • leads to read current value
  • read current value — decide the new value in Python
    • leads to pipe.multi() + queue writes
  • pipe.multi() + queue writes
    • leads to pipe.execute()
  • pipe.execute() — runs atomically — unless key changed
    • on error, leads to WatchError → retry the loop (if key changed)
  • WatchError → retry the loop

Command

python
pipe = r.pipeline(transaction=True)
pipe.set("balance:acct1", 100)
pipe.decrby("balance:acct1", 30)
pipe.get("balance:acct1")
results = pipe.execute()   # [True, 70, '70'] — one result per queued command

Every single Redis command is already atomic; use r.pipeline(transaction=True) for MULTI/EXEC, and pipe.watch(key) for optimistic locking when the write depends on a value you just read.

7.Pub/Sub and Streams

Pub/Sub broadcasts a message to whoever is subscribed right now — r.publish(channel, msg) — with no history kept. A Stream (r.xadd) is a persisted, ordered log a consumer can read from any point, including messages sent before it started listening.

Key features

  • r.publish(channel, message) sends to subscribers of channel right now; a subscriber that connects after publish() never sees that message
  • pubsub.get_message() returns one message at a time from a subscribed connection, including a one-time "subscribe" confirmation message first
  • r.xadd(stream, {field: value, ...}) appends an entry and returns an auto-generated ID like "1526919030474-55" — timestamp-milliseconds, then a sequence number
  • r.xrange(stream, "-", "+") reads the full stream in ID order; "-" and "+" mean the lowest and highest possible IDs
  • r.xgroup_create(stream, group, id="0") plus r.xreadgroup(group, consumer, {stream: ">"}) splits stream entries across named consumers, each entry delivered once per group

Command

python
# Pub/Sub
pubsub = r.pubsub()
pubsub.subscribe("notifications")
r.publish("notifications", "order:1001 shipped")
pubsub.get_message(timeout=1)   # {'type': 'message', 'channel': 'notifications', 'data': '...'}

# Streams
r.xadd("events:orders", {"order_id": "1001", "status": "created"})   # '<ts>-0'
r.xrange("events:orders", "-", "+")   # every entry, in ID order

Pub/Sub (r.publish) is fire-and-forget with zero history; Streams (r.xadd/r.xreadgroup) persist every entry and let a consumer group split and replay them — pick Streams whenever a missed message is not acceptable.

8.Distributed locks

A distributed lock uses r.set(key, token, nx=True, px=ttl_ms) so only one process can hold the key at a time. Releasing it must check the stored token matches yours before deleting — otherwise you can delete a lock someone else already acquired after yours expired.

Key features

  • r.set(key, token, nx=True, px=ttl_ms) acquires the lock only if the key is currently absent — nx=True is the mutual-exclusion guarantee
  • The value written must be a unique, unguessable token per lock attempt — it is what proves you released your own lock, not someone else's
  • Releasing safely requires a single atomic "check token, then delete" step — a Lua script (or Redis 8.4+'s DELEX key IFEQ value) — never a separate GET then DEL
  • The lock's PX/TTL is both the auto-release safety net and a deadline: work must finish before it expires, or another process may acquire the same lock while the first is still running
  • A single Redis instance is a single point of failure for the lock — official guidance recommends the multi-instance Redlock algorithm, majority quorum across independent Redis nodes, for real fault tolerance
Safe acquire and release
Client A
Redis
Client B
  1. 1SET lock:invoice:99 token-A NX PX 5000succeeds — key was absent
  2. 2SET lock:invoice:99 token-B NX PX 5000fails — key already set, returns nil
  3. 3EVAL release_script KEYS[1]=lock:invoice:99 ARGV[1]=token-Achecks value == token-A before DEL
  4. 4DEL succeeds — token matched
  1. Client A → Redis: SET lock:invoice:99 token-A NX PX 5000 (succeeds — key was absent)
  2. Client B → Redis: SET lock:invoice:99 token-B NX PX 5000 (fails — key already set, returns nil)
  3. Client A → Redis: EVAL release_script KEYS[1]=lock:invoice:99 ARGV[1]=token-A (checks value == token-A before DEL)
  4. Redis → Client A: DEL succeeds — token matched

Command

python
import uuid
token = str(uuid.uuid4())
acquired = r.set("lock:invoice:99", token, nx=True, px=5000)   # True or None

release_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
"""
release = r.register_script(release_script)
release(keys=["lock:invoice:99"], args=[token])   # 1 if released, 0 if not yours

Acquire with r.set(key, token, nx=True, px=ttl_ms); release by checking the token and deleting in one atomic script — and treat a single Redis instance as fault-tolerant only for low-stakes locks, not ones where a double-acquire is unacceptable.

9.Caching

Cache-aside means checking Redis first, and only doing the expensive lookup (a database query, an API call) if the value is not there — then writing the result into Redis with a TTL so it expires instead of going stale forever.

Key features

  • Cache-aside: r.get(key) first; on a miss, compute/fetch the real value, then r.set(key, value, ex=ttl) to populate the cache for next time
  • Every cached key needs a TTL (ex=) — an un-expiring cache entry can outlive the data it was computed from and serve stale answers indefinitely
  • A cache miss costs the same as having no cache at all (the full lookup) plus one extra write — a cache never makes the first request for a key faster, only repeat requests
  • Cache key naming (e.g. "cache:user_profile:1001") should encode everything the cached value depends on — two different queries sharing one cache key is a correctness bug, not a caching detail
  • Invalidate a stale cache entry explicitly with r.delete(key) when the underlying data changes, rather than only relying on the TTL to eventually catch up

Command

python
def get_user_profile(user_id, db_lookup):
    cache_key = f"cache:user_profile:{user_id}"
    cached = r.get(cache_key)
    if cached is not None:
        return cached, "HIT"
    value = db_lookup(user_id)      # the slow path
    r.set(cache_key, value, ex=300)  # cache for 5 minutes
    return value, "MISS"

Cache-aside: check Redis, do the expensive work only on a miss, then r.set(key, value, ex=ttl) — the TTL is what stops a cache from quietly going stale forever.

Remember

  • Every value lives under a unique key; incr/incrby/hincrby update a number atomically with no read-modify-write gap.
  • A TTL is a self-deleting countdown (r.expire/ex=) — a plain r.set() clears it unless you pass keepttl=True.
  • sismember is O(1) membership; zrevrange keeps a leaderboard readable in score order without a client-side sort.
  • A distributed lock releases safely only by checking the token and deleting in one atomic script — never GET then DEL as two commands.

Pro tip

  • Use r.pipeline(transaction=True) plus watch(key) for optimistic locking instead of blocking with a manual lock when conflicts are rare.
  • Cache every entry with ex= — an un-expiring cache key can silently outlive the data it was computed from.
  • Reach for a Stream over Pub/Sub the moment a missed message is unacceptable — Pub/Sub keeps no history for a late subscriber.
  • Treat a single-instance lock as fault-tolerant only for low-stakes work; the documented mitigation for anything else is Redlock plus a fencing token.

Next up

  • MongoDB

Sources

  • Redis commands — redis.io/docs/latest/commands
  • redis-py — redis-py.readthedocs.io
  • Every example run for real against fakeredis 2.37.1 (redis-py 8.1.0 client) · checked 2026-08-22

Spotted a mistake on this page? Tell us.