redis
Use when choosing a Redis data structure for a use case, implementing caching or rate limiting, building pub/sub or Streams-based real-time messaging, or writing atomic operations like distributed locks.
Works with
---
name: redis
description: Use when choosing a Redis data structure for a use case, implementing caching or rate limiting, building pub/sub or Streams-based real-time messaging, or writing atomic operations like distributed locks.
license: MIT
---
# Redis Patterns
Redis data structures, caching, pub/sub, and streams for Python async apps.
## When to Activate
- Choosing the right Redis data structure for a use case
- Implementing caching (cache-aside, write-through, TTL eviction)
- Using pub/sub or Redis Streams for real-time messaging / SSE
- Building a job queue with SKIP LOCKED semantics
- Writing atomic operations (counters, rate limits, distributed locks)
- Debugging slow Redis commands or memory bloat
---
## Connection (async redis-py)
```python
import redis.asyncio as redis
# Single connection
client = await redis.from_url("redis://localhost:6379", decode_responses=True)
# Connection pool (recommended for apps)
pool = redis.ConnectionPool.from_url(
"redis://localhost:6379",
decode_responses=True,
max_connections=20,
)
client = redis.Redis(connection_pool=pool)
# Close on shutdown
await client.aclose()
```
`decode_responses=True` returns `str` instead of `bytes` — use it unless you store binary data.
---
## Data Structure Decision Table
| Structure | Use For | Avoid When |
|---|---|---|
| **String** | Single values, JSON blobs, counters, distributed locks | Frequently updating one field of many |
| **Hash** | Objects with multiple fields; partial field reads/writes | >100 fields or deeply nested — use String+JSON instead |
| **List** | FIFO queues, activity feeds, bounded history | Random access by index — use Sorted Set |
| **Set** | Unique membership, tag intersections/unions, "online users" | Need ordering or score — use Sorted Set |
| **Sorted Set** | Leaderboards, priority queues, time-ordered events, rate limiting | Cardinality >10M — memory gets expensive |
| **Stream** | Durable pub/sub, consumer groups, event log | Simple fire-and-forget — use pub/sub |
| **HyperLogLog** | Approx unique count (±0.81% error, capped at 12 KB) | Exact count required |
| **Bitmap** | Per-user boolean flags, daily active user tracking | More than 512 MB of bits |
---
## Data Structures
### Strings — single values, counters, JSON blobs
```python
# Set / get
await client.set("user:123:name", "Alice")
await client.get("user:123:name") # "Alice"
# With TTL (seconds)
await client.set("session:abc", token, ex=3600) # expires in 1 hour
await client.setex("session:abc", 3600, token) # same
# Only set if not exists (NX) — distributed lock primitive
acquired = await client.set("lock:job:42", "worker-1", nx=True, ex=30)
# Atomic counter
await client.incr("page:views")
await client.incrby("page:views", 5)
await client.decr("inventory:product:99")
# Get + set atomically (Lua or GETEX)
await client.getex("session:abc", ex=3600) # reset TTL on read
# Store JSON
import json
await client.set("user:123", json.dumps(user_dict))
user = json.loads(await client.get("user:123"))
```
### Hashes — objects / partial updates
```python
# Set multiple fields at once
await client.hset("user:123", mapping={
"name": "Alice",
"email": "alice@example.com",
"role": "admin",
})
# Get all fields
user = await client.hgetall("user:123") # {"name": "Alice", ...}
# Get one field
name = await client.hget("user:123", "name")
# Update one field without overwriting others
await client.hset("user:123", "role", "user")
# Check existence
exists = await client.hexists("user:123", "email")
# Delete a field
await client.hdel("user:123", "temp_token")
# Get field names / values
fields = await client.hkeys("user:123")
values = await client.hvals("user:123")
```
Use hashes for objects with many fields where you update individual fields often. Cheaper than JSON string for partial reads.
### Lists — queues, activity feeds
```python
# Push to right (tail) — enqueue
await client.rpush("queue:emails", "msg-1", "msg-2")
# Pop from left (head) — dequeue FIFO
job = await client.lpop("queue:emails")
# Blocking pop — wait up to 30s for an item
job = await client.blpop("queue:emails", timeout=30) # returns (key, value)
# Stack (LIFO): rpush + rpop
await client.rpush("stack", "item")
item = await client.rpop("stack")
# Peek without removing
items = await client.lrange("queue:emails", 0, -1) # all items
recent = await client.lrange("activity:user:1", 0, 9) # first 10
# Keep list bounded (trim to last 100)
await client.ltrim("activity:user:1", -100, -1)
# Length
length = await client.llen("queue:emails")
```
### Sets — unique membership, tags
```python
await client.sadd("online_users", "user-1", "user-2")
await client.srem("online_users", "user-2")
is_online = await client.sismember("online_users", "user-1")
members = await client.smembers("online_users")
count = await client.scard("online_users")
# Set operations
common = await client.sinter("user:1:friends", "user:2:friends") # intersection
all_ = await client.sunion("tag:python", "tag:async") # union
diff = await client.sdiff("all_users", "banned_users") # difference
```
### Sorted Sets — leaderboards, priority queues, rate limiting
```python
# Add with score (score determines order)
await client.zadd("leaderboard", {"alice": 1500, "bob": 1200, "carol": 1800})
# Get top 3 (highest score first)
top3 = await client.zrevrange("leaderboard", 0, 2, withscores=True)
# [("carol", 1800.0), ("alice", 1500.0), ("bob", 1200.0)]
# Rank (0-indexed, lowest score = rank 0)
rank = await client.zrevrank("leaderboard", "alice") # 1 (2nd place)
# Increment score atomically
await client.zincrby("leaderboard", 50, "bob")
# Range by score — get items between two scores
members = await client.zrangebyscore("leaderboard", 1400, 2000)
# Remove
await client.zrem("leaderboard", "bob")
```
---
## TTL and Expiration
```python
# Set TTL on existing key
await client.expire("session:abc", 3600) # seconds
await client.expireat("session:abc", timestamp) # unix timestamp
await client.pexpire("key", 500) # milliseconds
# Check remaining TTL
ttl = await client.ttl("session:abc") # seconds remaining, -1 if no TTL, -2 if missing
pttl = await client.pttl("session:abc") # milliseconds
# Remove TTL (make persistent)
await client.persist("key")
```
---
## Pub/Sub
```python
# Publisher
async def publish_event(client, channel: str, data: dict):
await client.publish(channel, json.dumps(data))
# Subscriber — runs indefinitely
async def subscribe_to_events(client, channel: str):
async with client.pubsub() as pubsub:
await pubsub.subscribe(channel)
async for message in pubsub.listen():
if message["type"] == "message":
data = json.loads(message["data"])
yield data
# Pattern subscribe
async with client.pubsub() as pubsub:
await pubsub.psubscribe("tasks:*") # matches tasks:created, tasks:done, etc.
async for message in pubsub.listen():
if message["type"] == "pmessage":
process(message["channel"], message["data"])
```
**Limitation:** pub/sub messages are fire-and-forget. Subscribers that miss a message while offline don't receive it. Use Streams for durable delivery.
---
## Redis Streams (durable pub/sub)
Streams persist messages — consumers can read from any position, including past messages.
```python
# Produce — append message to stream
msg_id = await client.xadd(
"task:updates",
{"task_id": "t-123", "status": "running", "content": "Processing..."},
maxlen=10000, # trim to 10k entries (approximate)
)
# Consume from beginning
messages = await client.xread({"task:updates": "0-0"}, count=100)
# messages: [("task:updates", [(id, {fields...}), ...])]
# Consume only new messages (since last read)
last_id = "0-0"
messages = await client.xread({"task:updates": last_id}, count=10, block=5000)
for stream, entries in messages:
for msg_id, fields in entries:
process(fields)
last_id = msg_id
# Consumer groups — multiple workers compete for messages
await client.xgroup_create("task:updates", "workers", id="0", mkstream=True)
# Worker reads and claims a message
msgs = await client.xreadgroup("workers", "worker-1", {"task:updates": ">"}, count=1)
for stream, entries in msgs:
for msg_id, fields in entries:
process(fields)
await client.xack("task:updates", "workers", msg_id) # mark done
# Trim old entries
await client.xtrim("task:updates", maxlen=5000, approximate=True)
```
**SSE streaming pattern** (used in Agentex frontend):
```python
# Backend: push deltas to a stream per task
await client.xadd(f"task:{task_id}:stream", {"delta": chunk})
# Frontend SSE endpoint: read stream and forward to browser
async def stream_task(task_id: str):
last_id = "0-0"
while True:
messages = await client.xread({f"task:{task_id}:stream": last_id}, block=5000)
for _, entries in messages:
for msg_id, fields in entries:
yield f"data: {fields['delta']}\n\n"
last_id = msg_id
```
---
## Caching Patterns
### Cache-aside (lazy loading)
```python
async def get_user(user_id: str) -> User:
key = f"user:{user_id}"
cached = await client.get(key)
if cached:
return User(**json.loads(cached))
user = await db.fetch_user(user_id)
await client.set(key, user.model_dump_json(), ex=300) # cache 5 min
return user
async def invalidate_user(user_id: str):
await client.delete(f"user:{user_id}")
```
### Write-through
```python
async def update_user(user_id: str, data: dict) -> User:
user = await db.update_user(user_id, data)
await client.set(f"user:{user_id}", user.model_dump_json(), ex=300)
return user
```
---
## Atomic Operations
### Distributed lock
```python
import uuid
async def with_lock(client, resource: str, ttl: int = 30):
lock_key = f"lock:{resource}"
lock_val = str(uuid.uuid4())
acquired = await client.set(lock_key, lock_val, nx=True, ex=ttl)
if not acquired:
raise RuntimeError(f"Could not acquire lock on {resource}")
try:
yield
finally:
# Only release if we still own it (Lua script for atomicity)
script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
await client.eval(script, 1, lock_key, lock_val)
```
### Rate limiting (sliding window)
```python
async def is_rate_limited(client, user_id: str, limit: int = 100, window: int = 60) -> bool:
key = f"rate:{user_id}:{int(time.time()) // window}"
count = await client.incr(key)
if count == 1:
await client.expire(key, window)
return count > limit
```
### Pipeline (batch commands — reduce round trips)
```python
async with client.pipeline(transaction=False) as pipe:
pipe.hset("user:1", mapping=data)
pipe.expire("user:1", 3600)
pipe.zadd("leaderboard", {"user-1": score})
results = await pipe.execute() # sent as one network round trip
# Atomic pipeline (MULTI/EXEC)
async with client.pipeline(transaction=True) as pipe:
await pipe.watch("inventory:42")
quantity = int(await pipe.get("inventory:42"))
if quantity < 1:
raise Exception("Out of stock")
pipe.multi()
pipe.decr("inventory:42")
await pipe.execute()
```
---
## SCAN — Non-Blocking Key Iteration
Never use `KEYS *` in production. Use `SCAN` with a cursor instead:
```python
# Python — iterate all keys matching a pattern without blocking
async def scan_keys(client, pattern: str) -> list[str]:
keys = []
cursor = 0
while True:
cursor, batch = await client.scan(cursor, match=pattern, count=100)
keys.extend(batch)
if cursor == 0:
break
return keys
# Scan hash fields
cursor = 0
while True:
cursor, fields = await client.hscan("user:123", cursor, count=50)
for field, value in fields.items():
process(field, value)
if cursor == 0:
break
# Scan sorted set members by score range (non-blocking alternative to ZRANGEBYSCORE on huge sets)
cursor = 0
while True:
cursor, members = await client.zscan("leaderboard", cursor, count=100)
for member, score in members:
process(member, score)
if cursor == 0:
break
```
---
## Eviction Policies
Set `maxmemory` and `maxmemory-policy` in `redis.conf` or via `CONFIG SET`:
```bash
redis-cli CONFIG SET maxmemory 2gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
```
| Policy | Evicts | Use When |
|---|---|---|
| `noeviction` | Nothing — returns error on write | Data must never be lost (primary store) |
| `allkeys-lru` | Least-recently-used key (any key) | General cache — you can't control which keys have TTL |
| `volatile-lru` | LRU among keys with TTL | Mix of persistent + cache keys in one instance |
| `allkeys-lfu` | Least-frequently-used key (any key) | Hotspot skew — some keys accessed far more |
| `volatile-ttl` | Key with shortest remaining TTL | Prefer expiring the soonest-to-expire keys |
| `allkeys-random` | Random key | Uniform access patterns, lowest overhead |
**Production default for caches:** `allkeys-lru`
**Never use `noeviction` for a cache** — the first write after memory is full raises an error.
```python
# Check current eviction policy
info = await client.config_get("maxmemory-policy")
# {'maxmemory-policy': 'allkeys-lru'}
# Monitor eviction rate
stats = await client.info("stats")
evicted = stats["evicted_keys"] # total evictions since start
```
---
## TypeScript Patterns (node-redis)
```typescript
import { createClient } from "redis";
const client = createClient({
url: "redis://localhost:6379",
socket: { reconnectStrategy: (retries) => Math.min(retries * 50, 2000) },
});
await client.connect();
// String / JSON
await client.set("user:123", JSON.stringify(user), { EX: 300 });
const raw = await client.get("user:123");
const user = raw ? JSON.parse(raw) : null;
// Hash
await client.hSet("user:123", { name: "Alice", role: "admin" });
const data = await client.hGetAll("user:123"); // Record<string, string>
// Sorted set
await client.zAdd("leaderboard", [{ score: 1500, value: "alice" }]);
const top = await client.zRangeWithScores("leaderboard", 0, 9, { REV: true });
// Pipeline
const pipeline = client.multi();
pipeline.set("a", "1");
pipeline.expire("a", 60);
pipeline.incr("counter");
const [, , count] = await pipeline.exec();
// Distributed lock
const acquired = await client.set("lock:job:42", workerId, { NX: true, EX: 30 });
if (!acquired) throw new Error("Lock unavailable");
// Pub/sub (separate subscriber client)
const sub = client.duplicate();
await sub.connect();
await sub.subscribe("events", (message) => {
const data = JSON.parse(message);
handle(data);
});
```
---
## Sentinel & Cluster Connections
```python
# Sentinel (high availability — automatic failover)
from redis.sentinel import Sentinel
sentinel = Sentinel(
[("sentinel-1", 26379), ("sentinel-2", 26379), ("sentinel-3", 26379)],
socket_timeout=0.5,
)
# master for writes, replica for reads
master = sentinel.master_for("mymaster", decode_responses=True)
replica = sentinel.slave_for("mymaster", decode_responses=True)
# Cluster (horizontal scaling)
from redis.asyncio.cluster import RedisCluster
cluster = RedisCluster.from_url("redis://node-1:7000", decode_responses=True)
await cluster.set("key", "value") # routes to correct shard automatically
```
---
## Red Flags
- **No TTL on cache or session keys** — keys without expiry accumulate forever and evict randomly under memory pressure; set `ex=` on every `set()` call for cached data and sessions
- **Using pub/sub for reliable delivery** — pub/sub is fire-and-forget; subscribers that are offline when a message is published never receive it; use Redis Streams with consumer groups for any message that must not be lost
- **Single connection instead of a pool** — a single `await redis.from_url(...)` connection serializes all commands and blocks under concurrent load; use `ConnectionPool` with `max_connections` sized to your concurrency
- **`KEYS *` in production** — `KEYS` is O(n) and blocks the Redis event loop while it scans every key; use `SCAN` with a cursor to iterate non-blocking, or redesign to avoid key enumeration entirely
- **Distributed lock without a unique value** — a lock released by any caller using only the key (not the unique lock value) can accidentally release another owner's lock; always store a UUID as the value and use a Lua script to compare-then-delete atomically
- **Unbounded stream growth** — `xadd` without `maxlen` lets the stream grow indefinitely; always set `maxlen=N` (with `approximate=True` for efficiency) or run periodic `xtrim`
- **Sending multiple independent commands one at a time** — each `await client.set(...)` is a network round trip; batch three or more independent commands in a pipeline (`async with client.pipeline()`) to cut round-trip overhead significantly
## Checklist
- [ ] Connection pool used (not single connection) for async apps
- [ ] `decode_responses=True` set unless storing binary
- [ ] TTL set on all cache/session keys
- [ ] Pub/sub replaced with Streams where offline delivery matters
- [ ] `xack` called after processing stream messages (consumer groups)
- [ ] Pipeline used when sending ≥ 3 independent commands in sequence
- [ ] Distributed locks use NX + expiry to prevent deadlocks
- [ ] Sorted sets used for leaderboards / time-ordered data instead of sorted lists
- [ ] `maxlen` set on streams to prevent unbounded growthMore API Design skills
lark-event
larksuite/cli
Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume <EventKey>` (covers IM messages/reactions/chat changes, Approval status changes, Task updates, VC meeting started/joined/ended, Minutes generated, Whiteboard updated, etc.). Use for Lark bots, real-time message processing, long-running subscribers, streaming webhook/push handlers. Supports `--max-events` / `--timeout` bounded runs and a stderr ready-marker contract — designed for AI agents running as subprocesses.
lark-contact
larksuite/cli
飞书 / Lark 通讯录:按姓名 / 邮箱解析成 open_id,或按 open_id 反查姓名 / 部门 / 邮箱 / 联系方式 / 个人状态 / 签名,以及按关键词搜索当前用户可见的机器人 / 智能体(agent)。当用户提到一个名字要下一步发消息 / 排日程,或拿到 open_id 想查具体信息时使用。不负责部门树遍历、按部门列员工、组织架构图,这类需求走原生 OpenAPI。
lark-openapi-explorer
larksuite/cli
飞书/Lark 原生 OpenAPI 探索:从官方文档库中挖掘未经 CLI 封装的原生 OpenAPI 接口。当用户的需求无法被现有 lark-* skill 或 lark-cli 已注册命令满足,需要查找并调用原生飞书 OpenAPI 时使用。

