
Valkey
- 7 installs
- 1 repo stars
- Updated July 19, 2026
- avifenesh/valkey-skills
valkey is a Claude skill for building applications against Valkey, a Redis-compatible datastore, covering caching, queues, locks, rate limiting, and more.
About
A Claude skill for building application features against Valkey, a Redis-compatible datastore. It routes to seven reference files covering application idioms (caching, sessions, locks, rate limiting, queues, counters, leaderboards, pub/sub, search), plus performance, cluster and HA, scripting, and security. Developers use it when implementing Valkey-backed patterns and picking the right commands.
- Reference for building apps against Valkey with 7 topic files
- Covers caching, sessions, queues, locks, rate-limiting, leaderboards, pub/sub, streams
- Includes version-gated commands, performance, cluster/HA, security
Valkey by the numbers
- 7 all-time installs (skills.sh)
- Ranked #682 of 911 Databases skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
valkey capabilities & compatibility
- Capabilities
- valkey glide · valkey ecosystem · spring data valkey
- Works with
- redis
- Use cases
- database
What valkey says it does
Use when building apps against Valkey - caching, sessions, queues, locks, rate-limiting, leaderboards, counters, pub/sub, streams, scripting, search, cluster, replication, HA, persistence, security.
Seven reference files under `reference/`. Scan the trigger lists, open the matching file.
npx skills add https://github.com/avifenesh/valkey-skills --skill valkeyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 19, 2026 |
| Repository | avifenesh/valkey-skills ↗ |
What it does
Build application patterns like caching, queues, locks, and leaderboards against Valkey.
Who is it for?
Developers implementing caching, queues, locks, or leaderboards on Valkey.
Skip if: Valkey server internals (valkey-dev) or operations (valkey-ops).
When should I use this skill?
You are implementing an application pattern backed by Valkey and choosing commands.
What you get
Correct Valkey-backed application patterns using the right commands and version-appropriate features.
- Correct command choices for the chosen pattern
- Version-appropriate feature usage
By the numbers
- Seven reference files under reference/
- Covers Valkey 8.0/8.1/9.0 version-gated commands
Files
Seven reference files under reference/. Scan the trigger lists, open the matching file.
reference/valkey-features.md
Version-gated commands, per-release changelog, Redis compatibility, RDB magic, module gap.
Triggers: SET IFEQ, SET IFGT, SET IFLT (8.1+), DELIFEQ, DELIFGT, DELIFLT (9.0+), CAS, compare-and-swap, safe lock release, replication rewrite to SET/DEL, GET flag ambiguity, WRONGTYPE, byte-exact comparison, IFEQ never creates missing key.
Triggers: hash field TTL, HSETEX, HGETEX, HEXPIRE, HPEXPIRE, HEXPIREAT, HPEXPIREAT, HTTL, HPTTL, HEXPIRETIME, HPEXPIRETIME, HPERSIST, FNX, FXX, NX/XX/GT/LT on fields, KEEPTTL, setter return codes (1/2/0/-2), HTTL codes (-1/-2), HGETALL filters expired, HLEN does not filter, HINCRBY rewrites to HSETEX.
Triggers: COMMANDLOG GET/LEN/RESET/HELP (8.1+), commandlog-execution-slower-than, commandlog-request-larger-than, commandlog-reply-larger-than, slow / large-request / large-reply categories, SLOWLOG legacy alias, REQUEST_POLICY ALL_NODES, RESPONSE_POLICY AGG_SUM.
Triggers: GEOSEARCH BYPOLYGON (9.0+), num-vertices, auto-closed polygon, WITHDIST centroid, antimeridian, GEOSEARCHSTORE, Web Mercator lat limits.
Triggers: numbered DBs in cluster (9.0+), cluster-databases, MOVE reply 0 ambiguity, per-node SCAN/FLUSHDB/DBSIZE.
Triggers: atomic slot migration (9.0+), CLUSTER MIGRATESLOTS, CLUSTER GETSLOTMIGRATIONS, CLUSTER CANCELSLOTMIGRATIONS, no ASK window, valkey-cli --cluster reshard still legacy.
Triggers: dual-channel replication (8.0+), dual-channel-replication-enabled, MPTCP (9.0+), mptcp, repl-mptcp, kernel 5.6+.
Triggers: pipeline memory prefetch (9.0+), prefetch-batch-max-size. Reply Copy Avoidance (9.0+).
Triggers: release history, what's new in 8.0 / 8.1 / 9.0, BITCOUNT SIMD, PFMERGE SIMD, ZRANK speedup, iterator prefetch, cache-line hashtable, TLS handshake offload, lazyfree default flips, repl-diskless-sync default flip, RDB magic VALKEY at version 80+, un-deprecated commands.
Triggers: Redis compatibility, Redis OSS 2.x-7.2.x baseline, Redis CE 7.4+ incompatible, RIOT, RedisShake, extended-redis-compatibility, identity surfaces (valkey-server, valkey.conf, INFO server valkey_version).
Triggers: module gap, valkey-search, valkey-bloom, valkey-json, time series (none).
reference/app-patterns.md
Application idioms: caching, sessions, locks, rate limiting, queues, counters, leaderboards, pub/sub, search.
Triggers (caching): cache-aside, lazy loading, stampede prevention, refresh lock, early refresh, TTL jitter, explicit invalidation with UNLINK, eviction policy trade-off, allkeys-lru vs volatile-lru expires-table cost, lfu-log-factor, lfu-decay-time, write-through, write-behind.
Triggers (CLIENT TRACKING / server-assisted client-side caching): RESP3 push, RESP2 REDIRECT, __redis__:invalidate, NOLOOP, BCAST PREFIX, OPTIN, OPTOUT, tracking-table-max-keys, spurious invalidation, tracking_total_keys/_items/_prefixes, connection-pool pitfall, client library support matrix.
Triggers (sessions): classic hash session, sliding TTL, session rotation atomicity, privilege escalation, HGETEX read-and-refresh atomic, per-field TTL, concurrent-session tracking (SADD/SCARD/SPOP), session ID entropy, UUIDv4 122 bits.
Triggers (locks): SET NX PX, per-acquisition random value, DELIFEQ release (9.0+), SET ... IFEQ ... PX renewal (8.1+), IFEQ nil means no longer own, replication unsafe for locks, Redlock, 5-step algorithm, N=5 independent primaries, sequential vs parallel acquire, Redlock unlock 0 is expected, fencing tokens, monotonic token, GC pause, crash-recovery footgun, AOF fsync-always.
Triggers (rate limiting): fixed window, 2x-at-boundary, sliding window counter, sliding window log, ZADD+ZREMRANGEBYSCORE+ZCARD, token bucket, per-field rate limiting (9.0+), HSETEX FNX + HINCRBY, post-increment trap, access-refreshed fixed-window trap, HGETEX EX is not sliding.
Triggers (queues): LPUSH+BRPOP at-most-once, LPUSH+BLMOVE+LREM at-least-once, streams, XADD, XREADGROUP, XACK, XPENDING, XCLAIM, XAUTOCLAIM, three-value return, next_cursor 0-0, deleted_ids, JUSTID, XTRIM ~, MAXLEN, MINID, dead-letter queue, BUSYGROUP error, PEL pending list, multiple consumer groups, ZPOPMIN/BZPOPMIN priority queue.
Triggers (counters): INCR/INCRBY int64 overflow error, INCRBYFLOAT drift, money, INCRBYFLOAT replicates as SET+KEEPTTL, windowed counters, pipeline INCR+EXPIRE, hot-key bottleneck, sharded counters with hash tag, MGET across shards, idempotency key, SET NX EX "processing", DELIFEQ release claim.
Triggers (approximate counting): HyperLogLog, PFADD/PFCOUNT/PFMERGE, PFCOUNT is RW, sparse vs dense encoding, BITFIELD, #N positional, u63 max (u64 not supported), OVERFLOW WRAP/SAT/FAIL, dedup with SET NX EX, SMISMEMBER batch check, Bloom filter, BF.RESERVE, scaling vs NONSCALING, BF.ADD, BF.EXISTS, no false negatives.
Triggers (leaderboards): ZSET, ZADD, ZINCRBY, ZREVRANK, ZRANGE ... REV WITHSCORES (6.2+), around-me window, ZUNIONSTORE AGGREGATE SUM, time-bucketed, composite score tiebreak, IEEE 754 packing.
Triggers (pub/sub): fire-and-forget, at-most-once, subscriber connection monopolization, 32 MB output buffer, client-output-buffer-limit pubsub, PSUBSCRIBE O(N), sharded pub/sub, SPUBLISH/SSUBSCRIBE/SUNSUBSCRIBE, cluster-allow-pubsubshard-when-down, keyspace notifications, notify-keyspace-events, flags KEA, __keyspace@<db>__, __keyevent@<db>__, PUBSUB CHANNELS/NUMSUB/NUMPAT/SHARDCHANNELS/SHARDNUMSUB.
Triggers (search / autocomplete): prefix autocomplete, score-0 lex order, ZRANGE ... BYLEX, SINTER/SUNION/SINTERCARD (7.0+) tag filtering, valkey-search module, FT.SEARCH, vector similarity.
reference/performance.md
Memory, latency, throughput - encoding, eviction, fragmentation/defrag, latency diagnosis, pipelining/pooling/io-threads, keys, bitmaps, benchmarks.
Triggers (encoding): listpack, hashtable, skiplist, intset, quicklist, hash-max-listpack-entries 512, hash-max-listpack-value 64, zset-max-listpack-entries 128, set-max-listpack-entries 128, set-max-intset-entries 512, list-max-listpack-size -2, one-way conversion, delete+recreate to restore. String encoding: int, embstr (<=44 B), raw, OBJ_ENCODING_EMBSTR_SIZE_LIMIT.
Triggers (top-level key overhead): ~70-80 B per key, hash-bucketing, Instagram 21->5 GB 4x.
Triggers (sizing): hashes <10K fields, sets/zsets/lists <100K, strings <1 MB, split by time bucket or id range.
Triggers (TTL): SET ... EX atomic, TTL/PTTL/EXPIRETIME/PEXPIRETIME return codes, -1 / -2, PERSIST ambiguous 0.
Triggers (OBJECT): OBJECT ENCODING, OBJECT FREQ (requires -lfu), `OBJECT IDLETIME` (requires -lru or noeviction), OBJECT REFCOUNT, OBJECT HELP, --bigkeys, --memkeys, --hotkeys (LFU).
Triggers (big-key drain): HSCAN+HDEL, SSCAN+SREM, ZSCAN+ZREM, LPOP batches before UNLINK.
Triggers (eviction): maxmemory, maxmemory-policy, allkeys-lru, allkeys-lfu, volatile-lru, volatile-lfu, volatile-ttl, volatile-random, allkeys-random, noeviction, volatile-* with no TTLs behaves like noeviction, evicted_keys in INFO stats.
Triggers (bitmaps): SETBIT/GETBIT/BITCOUNT/BITOP, 100M users ~12 MB, BITCOUNT SIMD (8.1+).
Triggers (fragmentation): used_memory, used_memory_rss, mem_fragmentation_ratio, allocator_frag_ratio (defrag fixes), allocator_rss_ratio (defrag cannot fix), ratio <1.0 means swap. Active defrag: activedefrag, active-defrag-threshold-lower/upper, active-defrag-cycle-min/max, active-defrag-ignore-bytes, active_defrag_running/hits/misses/key_hits/key_misses. MEMORY USAGE SAMPLES, MEMORY DOCTOR, MEMORY MALLOC-STATS bins. 8.1 hashtable impact.
Triggers (latency): valkey-cli --intrinsic-latency, --latency, --latency-history, --latency-dist, latency-monitor-threshold, LATENCY LATEST, LATENCY HISTORY, LATENCY GRAPH, LATENCY DOCTOR, LATENCY RESET, LATENCY HISTOGRAM. Event types: command, fast-command, fork, expire-cycle, active-defrag-cycle, aof-fsync-always, aof-write-pending-fsync. Fork pause ~1-2 ms/GB, latest_fork_usec, rdb_last_cow_size, THP disable. Expiration storm, ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE, active-expire-effort, TTL jitter. INFO latencystats eventloop_duration_sum/_cmd_sum. CLIENT LIST flag b, omem.
Triggers (throughput): lazyfree 8.0 default flips (lazyfree-lazy-user-del/flush/eviction/expire/server-del), UNLINK intent-visible. SCAN COUNT hint, duplicates, dedupe client-side, iterate until 0, TYPE filter. Pipelining syscall reduction, ~10,000 batch sweet spot, enableAutoPipelining (ioredis), GLIDE multiplexed. MULTI/EXEC inside pipeline. Connection pooling, dedicated pool for pub/sub and blocking ops. I/O threading, io-threads includes main, io-threads-do-reads silently ignored, events-per-io-thread. Hot-key mitigation via sharding, read-from-replica, CLIENT TRACKING.
Triggers (benchmark): valkey-benchmark -t/-c/-n/-P/--threads/-d/--tls/-q. memtier_benchmark --ratio. Pitfalls: client threads must match server io-threads, pipeline plateau P64-128.
reference/cluster-and-ha.md
Cluster topology, replication, Sentinel, persistence - deployment and failure survival.
Triggers (slot model): 16384 slots, CRC16, hash tag first {...} only, empty tag means no tag, CLUSTER KEYSLOT, hot slot risk, co-location patterns.
Triggers (CROSSSLOT): error message, same-slot-required commands list (MGET, MSET, MSETNX, SINTER/SUNION/SDIFF + STORE, ZINTER/ZUNION/ZDIFF + STORE, LMOVE, SMOVE, RENAME, RENAMENX, EVAL/FCALL multi-KEYS, COPY). clusterSlotByCommand in src/cluster.c. Client-side fan-out: DEL/UNLINK multi-key, SCAN per primary.
Triggers (redirects): MOVED permanent, ASK once-with-ASKING, 9.0+ atomic migration no-ASK-window.
Triggers (read-from-replica): READONLY/READWRITE, valkey-glide ReadFrom.Primary|PreferReplica|AZAffinity|AZAffinityReplicasAndPrimary, ioredis scaleReads, valkey-py read_from_replicas, staleness, WAIT for read-after-write.
Triggers (cluster ops): pipelining per-node batch size, SCAN loop over every primary, topology-change-mid-scan, regular PUBLISH fan-outs across cluster bus, sharded pub/sub.
Triggers (replication internals): PSYNC2, partial vs full resync, full-resync triggers, dual replication IDs, repl-backlog-size default 10 MB (too small), repl-backlog-ttl, sizing formula (write_rate * max_disconnect * 2), master_repl_offset. client-output-buffer-limit replica 256mb 64mb 60, resync loop, sync_full/sync_partial_ok/sync_partial_err. Diskless: repl-diskless-sync yes (Valkey default), repl-diskless-sync-delay, repl-diskless-load swapdb. Dual-channel (8.0+). replica-priority (0 never promote). Sentinel selection order: priority, offset, run-id. Replica chains downsides. min-replicas-to-write, min-replicas-max-lag, NOREPLICAS error.
Triggers (HA / Sentinel): port 26379, group name not hostname, multiple Sentinels, down-after-milliseconds, failover-timeout, 5-30 s outage. Retry signals: READONLY, LOADING, ECONNREFUSED, ECONNRESET, CLUSTERDOWN, MASTERDOWN. Idempotency rule (INCR/LPUSH/RPUSH/XADD-no-ID/ZINCRBY unsafe; SADD safe).
Triggers (WAIT / WAITAOF): WAIT <N> <ms> in-memory, blocks caller only, timeout 0 blocks forever, applies to all preceding writes in connection. WAITAOF <local> <replicas> <ms> (7.2+) fsync durability, returns [local_fsyncs, replica_fsyncs].
Triggers (Sentinel vs Cluster): fits-one-node vs sharded choice.
Triggers (persistence): appendfsync everysec 2-s trap, aof-write-pending-fsync, always ~1000/s rotational. Hybrid: appendonly yes, aof-use-rdb-preamble yes (default, fast restart), save 3600 1 300 100 60 10000, AOF loaded if both present. Fork pause ~1-2 ms/GB, latest_fork_usec, THP disable. COW during snapshot 0-2x memory. Cache vs source-of-truth.
reference/scripting.md
Lua scripting and FUNCTIONs.
Triggers: EVAL, EVALSHA, SCRIPT LOAD, NOSCRIPT after restart/failover, FUNCTION LOAD persists in RDB/AOF, FCALL, FCALL_RO, EVAL_RO for replicas, ACL @read-only, flags={'no-writes'}, busy-reply-threshold (5000 ms default), lua-time-limit legacy alias, -BUSY, SCRIPT KILL, FUNCTION KILL, SHUTDOWN NOSAVE, kill works only before first write. Lua memory in maxmemory. Native replacements: SET IFEQ, DELIFEQ. Determinism: server.call('TIME')/RANDOMKEY/math.random() avoid in writes. Shebang #!lua name=<lib>. server.register_function positional vs table form. FUNCTION LIST/DELETE/DUMP/RESTORE, FUNCTION LOAD REPLACE. server.call raises, server.pcall single-value with {err=...} table on error (not tuple), server.error_reply, server.status_reply, redis.* alias. KEYS[] same-slot rule.
reference/security.md
Auth, ACL, TLS.
Triggers (AUTH): AUTH <pw>, AUTH <user> <pw>, requirepass, ACL SETUSER.
Triggers (ACL atoms): on/off, >password, <password, nopass, resetpass, +cmd/-cmd, +@category/-@category, +cmd|subcommand, ~pattern, %R~pattern read-only, %W~pattern write-only, allkeys, resetkeys, &channel-pattern, allchannels, resetchannels.
Triggers (categories): @read, @write, @string, @hash, @list, @set, @sortedset, @stream, @pubsub, @connection, @transaction, @scripting, @admin, @dangerous, @slow, @fast, @geo, @keyspace, @bitmap, @hyperloglog.
Triggers (introspection): ACL CAT, ACL WHOAMI, ACL LIST, ACL GETUSER.
Triggers (footgun): ACLs do NOT restrict by DB number.
Triggers (templates): read-write app, reader, cache-only, queue worker, dual read/write surfaces. -@admin + -@dangerous exclude FLUSHALL/CONFIG/DEBUG/KEYS.
Triggers (connection knobs): CLIENT SETNAME, CLIENT NO-EVICT ON.
Triggers (TLS): tls-port (commonly 6380), CA cert, mTLS cert+key, 6379 stays plaintext unless replaced, 8.1+ TLS handshake offload to I/O threads.
reference/anti-patterns.md
Corrections, detection, fix matrix - "am I doing something stupid" lookup.
Triggers (non-obvious corrections): DEL is already async by default in 8.0+ (lazyfree-lazy-user-del yes), UNLINK is intent-visible. SCAN/HSCAN/SSCAN/ZSCAN return duplicates + empty pages. --hotkeys/OBJECT FREQ require *-lfu. SLOWLOG legacy alias; canonical COMMANDLOG.
Triggers (listpack thresholds): hash-max-listpack-entries 512/value 64, set 128/64, zset 128/64, list-max-listpack-size -2. Whole-collection O(N) past threshold. Sizing rules.
Triggers (detection): --bigkeys, --memkeys, --hotkeys, CONFIG GET maxmemory, INFO memory, ACL LIST, COMMANDLOG GET 10 slow.
Triggers (fix matrix): KEYS in prod, single hot key, WATCH+MULTI replaced by IFEQ/DELIFEQ/FUNCTION, pub/sub for durable, blocking ops on pooled connection, FLUSHALL callable, missing TTL + no eviction, SORT on large, unbounded lists/streams, values >1 MB.
Anti-patterns
Non-obvious corrections
- DEL is already async by default (Valkey 8.0+): shipped config has
lazyfree-lazy-user-del yes.UNLINKis intent-visible in code - a config flip can silently make DEL sync again; UNLINK cannot. - SCAN / HSCAN / SSCAN / ZSCAN can return duplicates and empty pages. Dedupe client-side; iterate until cursor returns
0. --hotkeys/OBJECT FREQrequiremaxmemory-policy=*-lfu(needs access-frequency counter, only tracked under LFU); error otherwise.SLOWLOGstill works in 8.1+ as a legacy alias on the slow log; canonical name is COMMANDLOG - same data pluslarge-request/large-replycategories that SLOWLOG doesn't expose.
Listpack encoding thresholds (below -> compact buffer, above -> hashtable/skiplist)
hash-max-listpack-entries 512,hash-max-listpack-value 64.set-max-listpack-entries 128,set-max-listpack-value 64.zset-max-listpack-entries 128,zset-max-listpack-value 64.list-max-listpack-size -2(8 KB per node).
Past threshold, whole-collection commands (HGETALL, SMEMBERS, unbounded ZRANGE, HKEYS) become O(N) and scale with collection size.
Sizing: hashes <10K fields; lists <100K elements; sets/zsets <100K members. Split larger collections.
Detection commands
valkey-cli --bigkeys # largest key per data type
valkey-cli --memkeys # top keys by memory
valkey-cli --hotkeys # most-accessed keys (requires *-lfu policy)
valkey-cli CONFIG GET maxmemory
valkey-cli INFO memory
valkey-cli ACL LIST
valkey-cli COMMANDLOG GET 10 slowHigh-impact anti-patterns (unique to Valkey)
| Anti-pattern | Fix |
|---|---|
KEYS * in production | SCAN with cursor; variants HSCAN/SSCAN/ZSCAN. |
| Single hot key (counter, rate-limit) | Shard: counter:0..N. Use hash tag counter:{pool}:0 to co-locate shards on one slot for atomic MGET; omit tag only when cross-node spread is required. |
WATCH+MULTI for read-then-write | SET IFEQ (8.1+) for CAS, DELIFEQ (9.0+) for safe release, or FUNCTION for complex atomic logic. |
| Pub/Sub for durable messaging | Streams with consumer groups (at-least-once). |
Blocking BLPOP/BRPOP/XREAD BLOCK on a pooled connection | Dedicated connection / separate pool for blocking consumers. |
FLUSHALL callable by app creds | Disable via rename-command FLUSHALL "" or ACL -flushall. |
| Missing TTL on cache entries + no eviction policy | Always SET ... EX; set maxmemory-policy allkeys-lru or allkeys-lfu as safety net. |
SORT on large collections | O(N+M log M) on main thread; pre-sort via ZSET or sort client-side. |
| Unbounded lists / streams | LTRIM cap, XADD ... MAXLEN ~ N or XTRIM MINID <ms>. |
| Storing values > 1 MB | Compress or externalize to object store; keep only the pointer. |
Application patterns
Caching, sessions, locks, rate limiting, queues, counters, leaderboards, pub/sub, search.
Caching
Cache-aside (lazy)
val = GET cache:k
if val: return val
val = db.fetch(...)
SET cache:k val EX <ttl>
return valCold start: first read of each key misses. Stale window: up to TTL.
Stampede prevention
Lock-based refresh (expensive queries):
val = GET cache:k
if val: return val
lock = SET lock:cache:k 1 NX EX 10
if lock:
val = db.fetch(...)
SET cache:k val EX <ttl>
UNLINK lock:cache:k
return val
sleep(50 ms); retry # another worker is refreshingEarly refresh:
remaining = TTL cache:k
if remaining < <threshold>:
enqueue async refresh job
return cached valueCombine: early refresh avoids expiry bursts; lock handles callers crossing the threshold simultaneously.
TTL jitter
EX <base> + rand(0, <jitter>). Identical TTLs across a namespace all expire in one active-expire burst and stall the main thread.
Explicit invalidation
UNLINK cache:k (async free; intent-visible even if lazyfree-lazy-user-del is flipped back to no).
Pattern-scoped: CLIENT TRACKING ON BCAST PREFIX cache: (see CLIENT TRACKING below).
notify-keyspace-events Exg is fire-and-forget pub/sub - not durable; never the sole invalidation mechanism.
Eviction policy trade-off
allkeys-lru / allkeys-lfu do not need per-key TTLs - dropping TTLs saves an entry in the expires table per key. volatile-* needs TTL on every cache key to be eligible for eviction.
LFU tuning: lfu-log-factor default 10 (lower = faster counter saturation); lfu-decay-time default 1 min (lower = faster demotion of no-longer-hot keys).
Write-through vs write-behind
Write-through: app writes DB then cache. Always fresh; every write pays cache cost.
Write-behind: app writes cache, async batch to DB. Lowest write latency; crash loses uncommitted writes - requires AOF + replication planning.
CLIENT TRACKING (server-assisted client-side caching)
Protocol
RESP3: push frames on the same connection. HELLO 3 + CLIENT TRACKING ON. Invalidations arrive as push frames with a key array (nil = full flush).
RESP2: no push. Two-connection REDIRECT:
- Inval conn:
CLIENT ID-> id;SUBSCRIBE __redis__:invalidate(pub/sub mode - dedicate, never reuse for data). - Data conn:
CLIENT TRACKING ON REDIRECT <id> NOLOOP. - Messages on
__redis__:invalidate: array of keys to evict, or nil -> full local flush.
__redis__:invalidate channel name is fixed in Valkey (not renamed).
NOLOOP: suppress self-invalidation when the tracking client writes a key it read. Default choice for write-through caches.
Modes
| Mode | Enable | Server cost | Precision |
|---|---|---|---|
| Default | CLIENT TRACKING ON | per (key, client) | exact |
| BCAST | CLIENT TRACKING ON BCAST PREFIX user: PREFIX session: | per prefix subscription | prefix-wide |
| OPTIN | CLIENT TRACKING ON OPTIN + CLIENT CACHING YES before each tracked read | per opted key | exact |
| OPTOUT | CLIENT TRACKING ON OPTOUT + CLIENT CACHING NO before excluded reads | per tracked key | exact |
BCAST is the only mode that scales for high-cardinality keyspaces and is compatible with interchangeable connection pools.
Tracking table limits
tracking-table-max-keys default 1000000; default mode only.
At limit: server evicts a random key and emits a spurious invalidation (not an error) - extra misses, not failures.
INFO stats:
tracking_total_keys- distinct keys; bounds againsttracking-table-max-keys.tracking_total_items- per-(key, client) entries;items >= keys.tracking_total_prefixes- active BCAST prefix subscriptions.
Sizing: clients * avg_tracked_keys_per_client. 1000 clients x 5000 keys = 5M -> raise limit or BCAST.
Per-entry ~64-128 B; 1M entries -> ~64-128 MB. BCAST stores only per-client prefix subscriptions.
Consistency caveats
- Brief stale window between write ack and invalidation arrival.
- No ordering across multi-key writes; invalidations are independent.
- Reconnect drops pending invalidations -> flush local cache on reconnect, then re-run setup.
- Primary failover / restart wipes tracking table -> flush on reconnect.
- RESP2 inval connection must stay alive; if it drops, cache silently goes stale. Use
CLIENT NO-EVICT ONon it.
Connection-pool pitfall
Default mode is per-connection - interchangeable pools break it. Dedicate one long-lived connection per app instance, or use BCAST.
Do-not-track
Write-heavy keys; TTL < 1 s; high-cardinality volatile keys; locks / coordination keys.
Client library support
| Client | Server-assisted tracking |
|---|---|
| valkey-glide (7 langs) | yes (CacheConfig) |
| valkey-go | yes |
| redisson (Java) | yes |
| lettuce (Java) | partial |
| redis-py / valkey-py | no built-in; manual RESP2 wiring |
| ioredis / iovalkey | no built-in; manual RESP2 wiring |
| node-redis | no built-in |
Sessions
Classic hash sessions
HSET session:abc123 user_id 1000 role admin ip "10.0.0.1"
EXPIRE session:abc123 1800
# Sliding TTL on each authenticated request
EXPIRE session:abc123 1800Rotation on privilege escalation (session fixation prevention): HGETALL old -> HSET new + EXPIRE -> UNLINK old. Use MULTI/EXEC or Lua for steps 2-3 atomicity; pipelining alone allows observing intermediate state. In cluster mode, co-locate old+new keys via hash tag: session:{user:1000}:abc123, session:{user:1000}:xyz789.
Per-field TTL (9.0+)
See valkey-features.md for full HSETEX/HGETEX/HEXPIRE surface and gotchas. Session use case: different lifetimes per field (csrf_token 5 min, auth_token 30 min, profile_data stable).
HGETEX: read-and-refresh atomic
Replaces HMGET + HEXPIRE pipeline. Each listed field's TTL resets; unlisted fields untouched. True per-field sliding window.
HGETEX session:abc EX 3600 FIELDS 2 user_id emailConcurrent-session tracking
Side index of session IDs per user:
SADD user:1000:sessions abc123
EXPIRE user:1000:sessions 86400 # sweep orphans
SCARD user:1000:sessions # count
SMEMBERS user:1000:sessions # list
SREM user:1000:sessions abc123 # on destroy
SPOP user:1000:sessions # evict random for max-N enforcementIdentity hygiene
Session IDs: 128+ bits of crypto randomness. UUIDv4 is 122 bits - acceptable but tight. Never expose internal hash structure in API responses.
Distributed locks
Single-instance lock
Acquire: SET lock:resource <random_value> NX PX <ttl_ms> (atomic). Random value must be per-acquisition (UUID / crypto random) to prove ownership on release/renew.
Release:
- 9.0+:
DELIFEQ lock:resource <random_value>- returns 1 if you owned it, 0 otherwise. - Pre-9.0: Lua
if server.call('GET',K)==V then return server.call('DEL',K) else return 0 end.
Renewal / extension
SET lock:resource <val> IFEQ <val> PX <ttl_ms> (8.1+) - atomic renew-if-owner.
IFEQ returns `nil` in two cases, both meaning you no longer own this lock: 1. Value mismatch (another client owns it). 2. Key missing - your lock already expired (IFEQ never creates a missing key).
Stop the protected work on nil; a worker that ignores nil keeps operating on a resource another client now holds.
Pre-8.1: Lua if server.call('GET',K)==V then return server.call('PEXPIRE',K,T) else return 0 end.
Auto-renewal: renew at ~2/3 TTL; stop the timer on first nil.
Replication is NOT safe for locks
Async-replication hole: 1. A acquires on primary. 2. Primary crashes before the write replicates. 3. Replica promoted. 4. B acquires the same lock on the new primary - both hold it.
Sentinel failover has the same window. If mutual exclusion must survive a primary crash, use Redlock or a CP coordinator (ZooKeeper, etcd).
Redlock (N independent primaries, no replication between them)
Typical N=5. Algorithm:
1. T1 = now (ms). 2. SET key val NX PX ttl on each instance sequentially with a small per-instance timeout (5-50 ms for a 10 s lock) - slow instance trips the timeout, not the whole acquire. 3. Acquired iff successes >= N/2 + 1 AND elapsed (now - T1) < ttl. 4. Effective validity = ttl - elapsed (minus a small clock-drift allowance). 5. On failure: DELIFEQ on all contacted instances, including ambiguous ones.
Parallel fan-out is an implementation option; sequential is canonical (simpler partial-failure reasoning).
Unlock: DELIFEQ key <val> on every contacted instance. `0` is expected, not an error - lock expired there or a different owner holds it. Don't retry the 0 instance.
Extend: SET key val IFEQ val PX new_ttl (8.1+) on all instances. Extended iff majority succeeds within remaining validity. Bound retries to preserve liveness.
Crash-recovery footgun
No persistence: restarted instance has no memory of granted locks -> may grant the same lock to a different client. Either AOF fsync-always (perf hit) or keep the crashed instance down ≥ max-TTL before rejoining. AOF everysec loses up to ~2 s on power loss - still unsafe if another client is acquiring in that window.
When Redlock is overkill
- Idempotent protected op (duplicate is harmless) -> one primary enough.
- Best-effort exclusion OK (rate limits, dedup of mostly-unique work).
- Latency sensitive - Redlock costs N round-trips per acquire.
Fencing tokens
Defeats GC/network-pause failure: client holds lock, pauses past TTL, another client acquires, original resumes thinking it still holds.
Each acquire returns a monotonic token; the protected resource rejects operations with token < last_seen_token.
Acquire + INCR must be atomic in one script - otherwise acquire-without-bump (failed INCR) or bump-without-acquire (out-of-order tokens):
-- KEYS[1]=lock, KEYS[2]=token counter; ARGV[1]=val, ARGV[2]=ttl_ms
if server.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then
return server.call('INCR', KEYS[2])
else
return nil
endFencing requires the protected resource to validate tokens; APIs without that support cannot use this pattern.
Rate limiting
Algorithms summary
| Algorithm | Memory | Accuracy | Burst | Atomicity |
|---|---|---|---|---|
| Fixed window | 1 key | approx | up to 2x at boundary | INCR+EXPIRE |
| Sliding window counter | 2 keys | good | smoothed | INCR+EXPIRE |
| Sliding window log | O(N) ZSET entries | exact | none | ZADD+ZREMRANGEBYSCORE+ZCARD, needs MULTI or Lua |
| Token bucket | 1 hash (2 fields) | exact | configured capacity | requires Lua / FUNCTION |
| Per-field TTL (9.0+) | 1 hash (1 field per scope) | approx | 2x boundary | HSETEX FNX + HINCRBY |
Fixed window
k = ratelimit:user:42:<yyyy-mm-ddTHH:MM>
count = INCR k
if count == 1: EXPIRE k <window_s>
if count > limit: rejectINCR+EXPIRE race on first request -> on crash key leaks forever. Safer: SET k 0 NX EX <window>; INCR k or Lua.
Sliding window log
now_ms = <client>
cutoff = now_ms - window_ms
ZREMRANGEBYSCORE k -inf cutoff
ZCARD k
ZADD k now_ms "<now>-<rand>" # unique member per request
PEXPIRE k window_msWrap in Lua or MULTI/EXEC for atomicity. Timestamp MUST come from the client (not server.call('TIME')) for determinism.
Token bucket (requires Lua)
Hash state: {tokens, last_refill_ms}. Refill = (now - last_refill) * refill_rate, capped at capacity. Lua must be atomic: read state, refill, decide, write back. Under effects replication the replica sees only HMSET - deterministic.
Per-field rate limiting (9.0+)
One hash per user, one field per scope (/api/orders, /api/users, ...). Field TTL auto-expires the counter.
# Create-or-noop with 60s TTL:
HSETEX rate:user:42 FNX EX 60 FIELDS 1 /api/orders 1
# 1 if newly created, 0 if already present.
# If returned 0:
count = HINCRBY rate:user:42 /api/orders 1
if count > limit: reject
# For X-RateLimit-Reset:
HTTL rate:user:42 FIELDS 1 /api/ordersFootguns:
- `HINCRBY` preserves field TTL - counter ticks down toward original expiry.
- `HSET` strips field TTL. Use
HSETEX ... KEEPTTLfor in-place value updates that must keep TTL. - Post-increment rejection: counter already bumped by the time you reject. Fine for access control; unsafe for billing/SLA metrics - use token bucket (check-before-consume).
- Access-refreshed fixed-window trap:
HGETEX EX 60 ...; HINCRBY ...(restart TTL on every hit) is not sliding - it's a fixed window whose clock resets on use. A steady 1 req/s stream holds the counter open forever. Use sliding-window-log for true sliding.
Cluster
Pin rate-limit keys with a hash tag matching the limited identity: {user:42}:ratelimit:... puts all scopes for one user on one shard.
Hygiene
- Always set TTL on the rate-limit key (no orphaned counters).
- Limit by user/API key, not IP (shared NAT, spoofing).
- Multi-scope checks (per-user AND per-key) -> pipeline.
Queues
List-based
LPUSH key job+BRPOP key <timeout_s>- at-most-once. Consumer crash loses in-flight job.LPUSH key job+BLMOVE src dst LEFT RIGHT <timeout_s>+LREM dst 1 jobon success - at-least-once; requires a recovery job scanning the processing list for stuck items.
Lists lack: retry counts, dead-letter, priority, scheduling. Use Streams for anything non-trivial.
Stream-based (XADD / XREADGROUP)
Setup:
XGROUP CREATE queue:tasks workers $ MKSTREAM$= start from new messages. Use0to replay history.MKSTREAMcreates the stream if missing.- Duplicate create errors with
BUSYGROUP- catch and ignore.
Produce:
XADD queue:tasks * type email to "u@x" subject "Hi"
# * = auto-generate ID (ms-seq, monotonic)Consume:
XREADGROUP GROUP workers <consumer> COUNT 10 BLOCK 5000 STREAMS queue:tasks >
# > = only new messages, never-delivered to this group
# A specific ID replays pending (recovery mode) for this consumer
XACK queue:tasks workers <id>On failure, do NOT ack - message stays in pending list (PEL), reclaimable.
Pending / reclaim:
XPENDING queue:tasks workers # summary
XPENDING queue:tasks workers - + 10 [consumer] # detail
XCLAIM queue:tasks workers <new-consumer> <min_idle_ms> <id> [<id>...]
XAUTOCLAIM queue:tasks workers <new-consumer> <min_idle_ms> <cursor> [COUNT N] [JUSTID]XAUTOCLAIM uses SCAN-like cursor, returns three values: [next_cursor, claimed_entries, deleted_ids].
next_cursor == "0-0"-> sweep complete.deleted_ids- entries still in PEL but removed from the stream (usually by XDEL); ack them out-of-band to drop them from PEL scans.JUSTIDreturns IDs only and does not increment delivery counter (inspect without side effects).
Trimming:
XTRIM queue:tasks MAXLEN ~ 10000 # approximate, efficient
XTRIM queue:tasks MAXLEN 10000 # exact, slower
XTRIM queue:tasks MINID ~ <ms>-<seq> # time-based cutoff
XADD queue:tasks MAXLEN ~ 10000 * ... # trim inline with produce~ keeps at-least-N entries, may keep more - server skips full listpack boundaries for much lower cost.
Dead-letter: XPENDING stream group - + N exposes per-message delivery count. On threshold: XADD queue:tasks:dlq * ... then XACK the original.
Multiple consumer groups: independent offsets and PEL per group. One stream serves analytics + processing simultaneously.
Priority queue via ZSET
ZADD queue:priority 1 <job> # low score = high priority
ZPOPMIN queue:priority # returns [member, score]
BZPOPMIN queue:priority 30 # blocking, timeout_s; returns [key, member, score]BZPOPMIN accepts multiple keys for multi-queue polling; returns the first key that has a member.
Delivery guarantee summary
| Pattern | Delivery | ACK | Replay | Priority |
|---|---|---|---|---|
| LPUSH + BRPOP | at-most-once | - | - | no |
| LPUSH + BLMOVE + LREM | at-least-once | manual LREM | via processing list | no |
| Stream + XREADGROUP + XACK | at-least-once | XACK | full history | no |
| ZADD + BZPOPMIN | at-most-once | - | - | yes |
Atomic and sharded counters
INCR family correctness
INCR / INCRBY / DECR / DECRBY operate on int64 signed (LLONG_MIN .. LLONG_MAX). Crossing the boundary returns "increment or decrement would overflow" - no silent wrap. For long-running counters near int64, rotate windowed keys (events:<yyyy-mm-dd-HH> + TTL).
INCRBYFLOAT footguns
long doubleaccumulation -> drift (0.1 + 0.2 ≠ 0.3). Do not use for money. Store smallest currency unit (cents, satoshis) as integer andINCRBY.- Replicates as `SET <key> <final> KEEPTTL` in replication stream and AOF, not as
INCRBYFLOAT. AOF grep forINCRBYFLOATmisses it. - Errors only on NaN/Infinity, not on magnitude.
Windowed counters
INCR auto-creates key at 1. EXPIRE is separate; a crash between them leaves a permanent key. Use a pipeline:
[pipeline]
INCR events:2026-03-29T15
EXPIRE events:2026-03-29T15 7200EXPIRE on an existing key resets TTL - acceptable for rolling windows.
Sharded counters (hot-key bottleneck)
Single key at >10K writes/sec serializes on the same object + replication stream, even on one node.
Two concerns:
- Avoid hot object on one node: shard to N keys -> writes parallelize within the node.
- Distribute across cluster nodes: drop the shared hash tag -> cross-slot reads need client fan-out.
Default to the first. Shared tag keeps shards on one slot:
INCR counter:{pageviews}:7 # random shard 0..N-1
MGET counter:{pageviews}:0 ... counter:{pageviews}:15 # sum client-sideWithout the shared tag, MGET across shards yields CROSSSLOT - group shard keys by slot, issue one pipelined MGET per slot, sum. GLIDE cluster clients do this automatically.
Shard counts: 8-16 for 10-100K writes/sec; 32-64 above that + client batching.
Idempotency claim
SET idempotent:<op-id> "processing" NX EX 3600 - returns OK on first claim, nil on duplicate.
On success, replace with result: SET idempotent:<op-id> <json> XX EX 86400.
On failure:
- Valkey 9.0+:
DELIFEQ idempotent:<op-id> "processing"- only deletes if still the placeholder (avoids racing with a successor). - Pre-9.0:
DEL(best-effort) or wrap in Lua with GET-then-DEL-if-equal.
TTL is essential: a crashed process without TTL leaves a permanent claim blocking all retries.
Approximate counting and dedup
HyperLogLog
- 0.81% standard error. Up to ~12 KB per key at full cardinality.
- Sparse encoding at low cardinality (dozens of bytes for ~100 uniques). Auto-promotes to dense when sparse runs out of space.
- Memory: 1M uniques in a SET ~50 MB vs HLL 12 KB.
Commands:
PFADD key element [element...]- returns 1 if HLL estimate changed.PFCOUNT key [key...]- single or on-the-fly merged count across multiple HLLs (no dest key).PFMERGE dest src1 [src2...]- materialize merge intodest.
`PFCOUNT` is `READONLY` at command level but `RW` at the key-spec level - it mutates the HLL header to cache computed cardinality and replicates. Cluster read-routing and ACLs that forbid writes will reject it. Treat as write path.
BITFIELD packed counters
Fixed-width signed/unsigned integers packed into one string key.
- Type range:
i1..i64,u1..u63. - `u64` is not supported -
BITFIELD key INCRBY u64 #0 1errors: "Invalid bitfield type. Note that u64 is not supported but i64 is." RESP cannot reliably encode unsigned > INT64_MAX. For 64-bit counters usei64.
Positional syntax: #N = the Nth element of the specified width. u8 #14 = byte offset 14*8.
BITFIELD stats:page INCRBY u8 #14 1 # hour 14 of 24 hourly counters
BITFIELD stats:page GET u8 #0 GET u8 #1 ...Overflow modes:
OVERFLOW WRAP- default; modular wrap.OVERFLOW SAT- saturate at type min/max.OVERFLOW FAIL- return nil on overflow (INCRBY) instead of mutating.
Dedup patterns
SET dedup:<id> 1 NX EX 86400- exact, per-event key. OK = new, nil = duplicate.SADD processed:batch:<n> id1 id2 ...thenSMISMEMBER processed:batch:<n> id1 idX id2 ...- batch membership check; returns array of 0/1.
Bloom filter (valkey-bloom module)
BF.RESERVE key <error_rate> <capacity> [EXPANSION n] [NONSCALING]
- Default is scaling: past capacity, new sub-filters are added. The stated error rate applies only to the first sub-filter; effective FPR compounds across sub-filters, and memory is unbounded.
NONSCALING: filter rejects new adds past capacity; stated error rate holds for the filter's life.- Choose
NONSCALINGwhen the error-rate guarantee matters more than capacity elasticity; default scaling when capacity is uncertain.
Semantics:
BF.ADD key item- 1 if newly added, 0 if probably already present.BF.EXISTS key item- 0 means definitely not in set (no false negatives); 1 means probably in (false-positive rate per the RESERVE).- Bloom filters do not support TTL on individual items; recreate to reset.
Leaderboards
ZSET operations, all O(log N):
ZADD leaderboard 2500 "player:alice"
ZINCRBY leaderboard 100 "player:alice"
ZREVRANK leaderboard "player:alice" # 0-indexed top rank
ZRANGE leaderboard 0 9 REV WITHSCORES # top 10 (preferred since 6.2)
ZREVRANGE leaderboard 0 9 WITHSCORES # legacy, still supported
ZSCORE leaderboard "player:alice""Around me" window: fetch ZREVRANK, then ZRANGE leaderboard <rank-k> <rank+k> REV WITHSCORES.
Time-bucketed aggregation
ZUNIONSTORE merged 7 daily:Mon daily:Tue ... AGGREGATE SUM - combine daily buckets into weekly. Auto-clean with EXPIRE on bucket keys.
Composite score tiebreak
IEEE 754 doubles give ~15 significant digits. Pack primary + tiebreaker in one score:
score = points * 10^10 + (MAX_TIMESTAMP - timestamp_seconds)Higher points win; within a tie, earlier timestamp wins (subtract from MAX so earlier has a larger remainder).
Cluster
A sorted set is one key -> one slot -> one shard. For 100M+ members, shard by score range or by bucket (leaderboard:{tier:gold}, leaderboard:{tier:silver}) and merge client-side for global views.
Pub/Sub
Fire-and-forget (at-most-once). Disconnected subscribers miss everything published during the gap. For durable messaging use Streams.
Subscriber connections are monopolized - cannot run regular commands. Dedicate a connection or pool.
PSUBSCRIBE <pattern> is O(N) per publish, matched against all registered patterns across all clients. Prefer exact SUBSCRIBE when channel names are known.
Subscriber output buffer default hard limit 32 MB - slow consumers are disconnected. Tune client-output-buffer-limit pubsub <hard> <soft> <soft_seconds>.
Cluster: sharded pub/sub
Regular PUBLISH fans out across the cluster bus to every node - wastes bandwidth at scale.
SPUBLISH/SSUBSCRIBE/SUNSUBSCRIBE slot-hash the channel name (respects {tag}) - only the node owning the slot is involved.
SSUBSCRIBE orders:region:us-east
SPUBLISH orders:region:us-east '{"order_id":5678}'
# Co-locate related channels with hash tags
SSUBSCRIBE {user:1000}:notifications
SSUBSCRIBE {user:1000}:presencecluster-allow-pubsubshard-when-down yes (default): sharded pub/sub keeps serving channels whose slot is covered, even when some cluster slots are not.
Keyspace notifications
Disabled by default (per-op CPU overhead). Enable: CONFIG SET notify-keyspace-events Ex.
Channels:
__keyspace@<db>__:<key>- notifications about this key (payload = event name).__keyevent@<db>__:<event>- notifications about this event type (payload = key name).
Flags:
| Flag | Meaning |
|---|---|
K | keyspace channel |
E | keyevent channel |
g | generic: DEL, EXPIRE, RENAME |
$ | string commands |
l | list commands |
s | set commands |
h | hash commands |
z | sorted-set commands |
t | stream commands |
x | expired |
e | evicted |
m | key miss (must enable explicitly) |
n | new key creation (must enable explicitly) |
A | alias for g$lshzxetd - excludes `m` and `n` |
At least one of K or E must be present alongside event flags.
Cluster: notifications are local to each node - subscribe on every primary (via sharded pub/sub) to cover the keyspace.
PUBSUB introspection
PUBSUB CHANNELS [pattern]- currently subscribed regular channels.PUBSUB NUMSUB [ch...]- subscriber counts per channel.PUBSUB NUMPAT- number of clients using PSUBSCRIBE patterns.PUBSUB SHARDCHANNELS [pattern]/PUBSUB SHARDNUMSUB- sharded equivalents.
Search and autocomplete
Prefix autocomplete
Store terms with score 0 so sorted-set order is purely lexicographic. Query by prefix with ZRANGE ... BYLEX (canonical since 6.2; legacy ZRANGEBYLEX still works):
ZADD autocomplete 0 "apple"
ZADD autocomplete 0 "application"
ZRANGE autocomplete "[app" "[app\xff" BYLEX LIMIT 0 10Store terms lowercased for case-insensitive matching. For ranked results, keep a separate scored sorted set and join in application code.
Tag filtering
SINTER for AND, SUNION for OR. SINTERCARD (Redis/Valkey 7.0) returns count without fetching members - useful for "X results" UI counters with an early-stop LIMIT.
Cluster: multi-key set operations require all keys in same slot - use hash tags {ns}:tag:electronics.
When to use valkey-search instead
| Need | Approach |
|---|---|
| Simple prefix autocomplete | ZRANGE ... BYLEX |
| Tag AND/OR filtering | SINTER / SUNION |
| Full-text, fuzzy, stemming | valkey-search module (FT.SEARCH) |
| Relevance scoring with field weights | valkey-search module |
| Vector similarity | valkey-search module |
Cluster, replication, HA, persistence
Topology, failover, durability. Deployment and survival.
Cluster slot model
16384 slots, CRC16. Multi-key commands only work when all keys hash to the same slot.
Hash tag rule: if a key contains {...}, CRC16 is taken over the substring between the first { and the next }. Otherwise the full key is hashed.
Gotchas:
- Only the first
{...}pair counts.{a}.{b}hashes ona. - Empty tag (
{}or{}.foo) is treated as no tag - full key is hashed. - Verify with
CLUSTER KEYSLOT "<key>". - Hot-slot risk: co-locating too many keys under one tag pins that slot to one shard.
Co-location patterns:
{user:1000}.profile,{user:1000}.cart-> MGET user data.{order:5678}.header,{order:5678}.items-> atomic transactions.{ratelimit:api}.shard:0....shard:15-> MGET sum across shards.{tags}.electronics,{tags}.wireless-> SINTER across tag sets.
CROSSSLOT
Error: (error) CROSSSLOT Keys in request don't hash to the same slot.
Commands requiring same slot (server-enforced via clusterSlotByCommand in src/cluster.c - generic across commands):
MGET,MSET,MSETNXSINTER,SUNION,SDIFF+ their*STOREvariantsZINTER,ZUNION,ZDIFF+ their*STOREvariantsLMOVE,SMOVE,RENAME,RENAMENXEVAL,FCALLwith multiple KEYSCOPY
Client-side fan-out (cross-slot "works" because the client splits by slot):
Server always rejects a multi-slot command. Cluster-aware clients (valkey-glide, Redisson, cluster-mode ioredis, ...) group keys by slot and issue one request per slot:
DEL/UNLINKwith multiple keys.SCAN- per primary node.- Single-key commands hit the owning node directly.
Fixes: redesign keys with hash tags; replace multi-key with pipelined single-key calls; rely on cluster-aware client fan-out.
Redirects
-MOVED <slot> <host:port> - slot ownership changed permanently. Client updates slot->node map and retries on the new node.
-ASK <slot> <host:port> - key is mid-migration. Client sends ASKING + command to target once; future requests for the slot still go to the original node until migration completes.
9.0+ atomic slot migration: CLUSTER MIGRATESLOTS moves slots atomically - no ASK window. Legacy CLUSTER SETSLOT MIGRATING/IMPORTING still uses ASK during manual resharding. Which one surfaces depends on operator tooling. See valkey-features.md for command surface.
Read-from-replica
Default: all reads go to the primary that owns the slot.
On the replica connection: READONLY to accept reads, READWRITE to revert.
Client read-from modes:
- valkey-glide:
ReadFrom.Primary | PreferReplica | AZAffinity | AZAffinityReplicasAndPrimary. - ioredis cluster:
scaleReads: 'master' | 'slave' | 'all' | <fn>. - valkey-py cluster:
read_from_replicas=True.
Staleness: sub-ms under normal load; can reach seconds during heavy writes or full resync. Never assume zero lag.
Read-your-writes: WAIT <numreplicas> <timeout_ms> - blocks the writer until N replicas ACK (or timeout). Use before the immediate read-back. For AOF durability: WAITAOF <numlocal> <numreplicas> <timeout_ms>.
Pipelining in cluster
Cluster-aware clients group commands by target node and send one pipeline per node in parallel; reassemble in original order. Throughput depends on per-node batch size, not total pipeline depth (100 cmds across 3 nodes ≈ 33/node). GLIDE multiplexes internally; explicit pipelining usually not needed.
SCAN across cluster
SCAN iterates a single node. To cover the keyspace, loop CLUSTER NODES (or the client's primary list) and SCAN each primary until cursor=0.
Gotchas:
- SCAN on replicas works but may miss/dup keys due to replication lag.
- Topology change mid-scan (failover or slot move) may lose or duplicate keys. Pause resharding for critical scans.
KEYS *in cluster mode only hits the one node it was sent to, and blocks it. Never use.
Pub/Sub in cluster
Regular PUBLISH fans out across the cluster bus to every node - scales poorly. Use sharded pub/sub (SSUBSCRIBE / SPUBLISH / SUNSUBSCRIBE); channel name is slot-hashed (respects {tag}) so messages stay on one shard. See app-patterns.md for full pub/sub details.
Cluster pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| Multi-key without hash tag | CROSSSLOT | {tag} |
| All data under one tag | One node overloaded | Distribute tags per entity |
| Stale reads after write | Inconsistent read-back | Primary read or WAIT |
| SCAN missing keys | Partial list | Iterate every primary |
| Lua touching keys across slots | Rejected or slow | Keys must hash to one slot |
| Regular pub/sub | Bus storm | SPUBLISH/SSUBSCRIBE |
Replication internals
PSYNC2: partial vs full resync
Partial: replica sends last repl ID + offset; if both match and offset is still in primary's backlog, only missing commands stream.
Full-resync triggers:
- First-ever connection.
- Replication ID mismatch (primary restarted/replaced).
- Offset outside backlog (disconnected too long).
- Client-output-buffer-for-replica overflow killed the connection; backlog may have rotated by reconnect.
- Explicit
PSYNC ? -1.
Dual replication IDs
After failover, the new primary keeps the old primary's repl ID as a secondary ID. Replicas of the old primary partial-resync to the new primary if their offset is still in the backlog. Why Sentinel failovers usually don't cascade full resyncs.
Replication backlog
repl-backlog-sizedefault 10 MB (too small for production).repl-backlog-ttldefault 3600s - retention after last replica disconnects.- Sizing:
repl-backlog-size >= write_rate_bytes_per_sec * max_disconnect_seconds * 2. - Measure write rate: sample
master_repl_offsetfromINFO replicationtwice, take delta / seconds. - Production floor: 256 MB. Write-heavy: 1 GB+.
Client-output-buffer-for-replica
client-output-buffer-limit replica 256mb 64mb 60 (default). Buffers primary writes arriving during RDB transfer of a full resync.
Overflow -> connection killed -> replica reconnects -> another full resync -> more buffered data -> resync loop, replica never catches up.
Detect via INFO stats: sync_full climbing while same replica reconnects -> buffer too small. Also sync_partial_ok, sync_partial_err.
Fixes: raise the limit, enable diskless replication (shorter transfer), or dual-channel (8.0+, eliminates this buffer).
Diskless replication
repl-diskless-sync yes (Valkey default; Redis was no). Streams RDB from fork memory directly over socket, skipping disk.
repl-diskless-sync-delay 5- wait N s for more replicas; arriving replicas share one RDB stream.- Replica
repl-diskless-load swapdb- load RDB while serving old data, atomic swap; needs 2x dataset memory during swap.
Keep disk-based when you need the RDB file for backups, or when replicas connect at very different times.
Dual-channel replication (8.0+)
See valkey-features.md for setup. Eliminates primary output-buffer-replica overhead during full resync; no resync loop.
Replica priority / Sentinel selection
replica-priority default 100. Lower = higher priority. 0 = never promote (use for backup/analytics replicas).
Sentinel selection order: 1. Lowest replica-priority (excluding 0). 2. Most advanced repl offset (least data loss). 3. Smallest run ID (tiebreaker).
Replica-of-replica chains
Primary -> A -> B reduces primary egress but:
- Each hop adds lag.
- If A full-resyncs from primary, repl ID changes -> all downstream also full-resync.
- Sentinel does not auto-rewire chains when an intermediate fails.
<=3 replicas: direct from primary is simpler.
min-replicas safety
min-replicas-to-write 0 (default, disabled). Set N to require N ACKing replicas. min-replicas-max-lag 10 - replica counts as lagging if no REPLCONF ACK <offset> in this many seconds (replicas ACK 1/s).
Below threshold, writes return (error) NOREPLICAS Not enough good replicas to write. - app must catch and retry/fail.
During Sentinel failover the old primary may briefly reject writes - desirable (prevents split-brain).
High availability (Sentinel)
Client connects to Sentinels (default port 26379), asks for the current primary by group name (not hostname). Sentinel and Valkey auth are independent passwords.
Configure multiple Sentinel addresses; never hardcode the primary address - it changes on failover.
Failover timeline (tunable via down-after-milliseconds, failover-timeout): detection ~down-after-ms; quorum + promotion: seconds. Total outage: 5-30 s.
Replication is async by default: writes acked by the old primary but not yet replicated are lost. Bound with WAIT / WAITAOF.
Retry error signals
| Error | Meaning | Action |
|---|---|---|
READONLY | Connected to a replica (post-promotion or stale slot map) | Retry - client rediscovers primary |
LOADING | Server loading dataset from disk post-restart | Retry after delay |
ECONNREFUSED | Node down | Retry with backoff |
ECONNRESET | Connection dropped mid-command | Retry only idempotent commands |
CLUSTERDOWN | Cluster cannot serve (not enough nodes) | Alert ops, long backoff |
MASTERDOWN | Standalone with replica-serves-stale-data-no set | Retry; primary may be failing over |
Idempotency rule for retries
Safe to retry: SET, GET, HSET, ZADD (idempotent under same key+value).
Not safe (retrying double-counts or duplicates): INCR, DECR, INCRBY, DECRBY, LPUSH, RPUSH, XADD (without explicit ID), ZINCRBY. SADD is safe (set semantics).
For non-idempotent writes, use an idempotency key (see app-patterns.md Counters section).
WAIT - in-memory replication
WAIT <numreplicas> <timeout_ms> - blocks only the caller until N replicas ack all preceding writes on this connection. Returns count acked (0 on timeout).
timeout_ms = 0blocks forever.- Does not make replication globally sync - other clients can still read stale from replicas.
- In a pipeline, applies to all preceding writes; place after last critical write.
WAITAOF (7.2+) - disk durability
WAITAOF <numlocal> <numreplicas> <timeout_ms> - blocks until AOF fsync completes on N local instances (0 or 1) and N replicas. Returns [local_fsyncs, replica_fsyncs].
Stronger than WAIT (in-memory ack only). Required for durability across primary loss.
When to use
- Read-after-write from replica:
WAIT 1 100. - Financial / critical ledger:
WAITAOF 1 1 500. - Normal writes / cache: neither. Adds latency on every call.
Sentinel vs Cluster
- Sentinel: single primary, all multi-key commands work, simpler client. Pick when data fits in one node.
- Cluster: sharded, only same-slot multi-key, cluster-aware client required. Pick when data size or write throughput exceeds one primary.
Persistence
Fsync policies (appendfsync)
| Policy | Behavior | Worst-case loss |
|---|---|---|
everysec (default) | Background thread fsyncs once/sec | ~2 seconds (see below) |
always | Fsync in the write path | One command |
no | OS decides | ~30 seconds |
everysec 2-second trap: background fsync >1 s (disk contention, AOF rewrite) -> main thread delays new writes up to 1 additional second. Worst case 2 s lost, not 1 s. Signal: aof-write-pending-fsync in LATENCY LATEST.
always throughput: ~1000 writes/sec rotational; SSDs much better. Profile first.
Hybrid (recommended prod config)
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes # default yes - AOF base is RDB-format
save 3600 1 300 100 60 10000 # default snapshot scheduleaof-use-rdb-preamble lets startup load an RDB-formatted base then replay tail -> fast restart + high durability.
Startup rule: with both AOF and RDB, Valkey loads the AOF (more complete).
Fork pause (RDB save / AOF rewrite)
~1-2 ms per GB; 64 GB -> 64-128 ms pause during which all clients block. latest_fork_usec reports last fork time. Disable THP to prevent COW blowup.
Copy-on-write during snapshot
Parent + child share pages; each parent write duplicates a page.
- Read-heavy during save: ~0% extra.
- Moderate writes: +10-30%.
- Write-heavy: up to 2x dataset memory during save.
Plan RAM headroom or the OS will OOM-kill Valkey mid-snapshot.
Replica-backed durability
WAIT <N> <timeout_ms> confirms writes reached N replica memory (not disk). WAITAOF <local> <replicas> <timeout_ms> (7.2+) confirms fsync to disk.
Replica AOF + primary AOF multiplies the failure domain required for data loss.
Cache vs source-of-truth
Pure cache with a durable backing DB: persistence only affects restart warmup, not data safety. Tune for fork-pause amortization, not durability.
Performance (memory, latency, throughput)
Encoding/eviction, fragmentation/defrag, latency diagnosis, throughput, key sizing.
Encoding thresholds (compact -> full; conversion is one-way)
| Type | Compact | Threshold | Full | Config keys |
|---|---|---|---|---|
| Hash | listpack | entries <= 512 AND each field/value <= 64 B | hashtable | hash-max-listpack-entries 512, hash-max-listpack-value 64 |
| Sorted set | listpack | entries <= 128 AND each member/score <= 64 B | skiplist + hashtable | zset-max-listpack-entries 128, zset-max-listpack-value 64 |
| Set (strings) | listpack | entries <= 128 AND each member <= 64 B | hashtable | set-max-listpack-entries 128, set-max-listpack-value 64 |
| Set (integers) | intset | entries <= 512 | hashtable | set-max-intset-entries 512 |
| List | quicklist of listpacks (always) | node cap list-max-listpack-size -2 (8 KB) | - | - |
Conversion is permanent per key. Removing elements below threshold does NOT revert encoding. Delete+recreate to restore compact form.
Past threshold, whole-collection commands (HGETALL, SMEMBERS, unbounded ZRANGE, HKEYS) become O(N) and scale with collection size.
Inspect: OBJECT ENCODING key -> listpack / hashtable / skiplist / intset / quicklist / embstr / int / raw / stream.
String encoding (not configurable)
| Condition | Encoding | Alloc |
|---|---|---|
Value fits a C long | int | 8 B inline |
| String <= 44 bytes | embstr | single alloc (object + data) |
| String > 44 bytes | raw | two allocs (object + data) |
44-byte boundary = OBJ_ENCODING_EMBSTR_SIZE_LIMIT in src/object.c. Stay <= 44 B to avoid the extra pointer indirection.
Top-level key overhead
~70-80 bytes per top-level key (dictEntry + SDS + object header). Dominates at millions of tiny keys.
Hash-bucketing (Instagram: 21 GB -> 5 GB, 4x)
Bucket millions of top-level keys into listpack-sized hashes:
SET media:1234 <value> # original
HSET media:12 34 <value> # key = id/100, field = id%100Each bucket stays under ~100 fields -> listpack -> ~5-10x smaller than individual strings.
Same reason: consolidate same-entity fields into one hash (HSET user:1000 name ... email ...) rather than separate keys.
Size limits (rules of thumb)
| Type | Recommended max | Why |
|---|---|---|
| Hash | < 10K fields | HGETALL latency + encoding flip |
| Set / Sorted set | < 100K members | SMEMBERS / range cost |
| List | < 100K elements | LRANGE cost |
| String value | < 1 MB | Network + memory pressure |
Split larger collections by time bucket or id range (e.g. user:1000:events:2026-03).
TTL rules
- Set TTL at write time:
SET key val EX 3600(atomic).SET+ separateEXPIREleaks keys ifEXPIREfails. TTL key/PTTL keyreturns-1(no TTL),-2(key missing), otherwise remaining seconds/ms.EXPIRETIME/PEXPIRETIMEreturn absolute Unix expiry.- Jitter identical TTLs (
EX 3600 + rand(0, 300)) to avoid expiration storms on the main thread.
OBJECT introspection
| Command | Returns | Requires |
|---|---|---|
OBJECT ENCODING key | listpack / hashtable / skiplist / intset / quicklist / embstr / int / raw | - |
OBJECT FREQ key | LFU access frequency counter | maxmemory-policy = *-lfu |
OBJECT IDLETIME key | seconds since last access | maxmemory-policy = *-lru (or noeviction) |
OBJECT REFCOUNT key | refcount | - |
OBJECT HELP | subcommands | - |
valkey-cli --hotkeys also requires an LFU policy (uses OBJECT FREQ). --bigkeys and --memkeys have no policy requirement.
Drain-before-UNLINK for big keys
UNLINK on a multi-million-entry hash queues a large background free that still competes with other ops. Drain first:
HSCAN bigkey 0 COUNT 100; HDEL bigkey <fields> # repeat until cursor=0
UNLINK bigkeySame for SSCAN+SREM, ZSCAN+ZREM, or LPOP/RPOP in batches for lists.
Eviction policies (triggered when used_memory > maxmemory)
| Policy | Evicts | Use |
|---|---|---|
allkeys-lru | LRU across all keys | General cache default |
allkeys-lfu | LFU across all keys | Power-law access |
volatile-lru | LRU among keys with TTL | Mixed cache + persistent |
volatile-lfu | LFU among keys with TTL | Mixed + power-law |
volatile-ttl | Shortest remaining TTL first | Priority-by-TTL |
volatile-random / allkeys-random | Random | Last resort |
noeviction | Nothing; rejects writes with OOM error | Must-not-lose data |
Footguns
volatile-*with no TTL-bearing keys -> no eviction candidates -> behaves likenoeviction(writes rejected).maxmemoryunset -> Valkey grows until OS OOM-kills the process. Set to ~75% of available RAM.- Monitor
evicted_keysinINFO stats: sudden spikes mean working set > memory. OBJECT FREQand--hotkeysrequire an LFU policy;OBJECT IDLETIMErequires LRU (or noeviction).
Bitmaps for boolean populations
100M users as bits = 12 MB per key (100e6 / 8 bytes).
SETBIT active:2026-03-29 <user_id> 1
GETBIT active:2026-03-29 <user_id>
BITCOUNT active:2026-03-29
BITOP AND active:both active:2026-03-28 active:2026-03-29BITCOUNT SIMD (8.1+): ~6x @ 1 MB / ~10x @ 10 MB on AVX2/NEON.
Large values
- < 100 KB: fine.
- 100 KB - 1 MB: compress client-side (clients don't auto-compress).
- > 1 MB: externalize to object store, keep only the reference in Valkey.
Memory fragmentation
INFO memory fields
used_memory- bytes allocated for data.used_memory_rss- OS-level RSS.mem_fragmentation_ratio= RSS / used_memory.mem_fragmentation_bytes= absolute overhead.allocator_frag_ratio/allocator_frag_bytes- jemalloc internal (allocated vs active). Defrag fixes this.allocator_rss_ratio/allocator_rss_bytes- RSS vs jemalloc resident. Kernel has not reclaimed pages yet. Defrag cannot fix; resolves on its own or on restart.
Ratio interpretation
| Ratio | Meaning | Action |
|---|---|---|
| < 1.0 | Swapping. Severe. | Increase RAM or reduce maxmemory now. |
| 1.0 - 1.1 | Healthy | - |
| 1.1 - 1.5 | Normal | Monitor |
| 1.5 - 2.0 | Significant | Consider active defrag |
| > 2.0 | Severe | Enable defrag or restart |
Driver: delete/churn workloads leave live allocations scattered, so pages can't return to OS.
Active defrag config
Default off. All runtime-tunable via CONFIG SET.
| Key | Default |
|---|---|
activedefrag | no |
active-defrag-threshold-lower | 10 (%) - start |
active-defrag-threshold-upper | 100 (%) - full effort |
active-defrag-cycle-min | 1 (% CPU) |
active-defrag-cycle-max | 25 (% CPU) |
active-defrag-ignore-bytes | 104857600 (100 MB) - don't start below this overhead |
CPU scales linearly between lower and upper thresholds. Below either -> no run. Pauses during RDB save / AOF rewrite (avoids inflating COW). Runs on main thread in slices; requires jemalloc.
INFO stats defrag fields
active_defrag_running- current % CPU used (0 when idle).active_defrag_hits- allocations relocated.active_defrag_misses- allocations scanned, already optimal.active_defrag_key_hits/active_defrag_key_misses- same per-key.
Per-key diagnosis
MEMORY USAGE key [SAMPLES N]- bytes incl overhead. DefaultSAMPLES 5(approximate);SAMPLES 0= every element (exact, slowest).valkey-cli --bigkeys- largest key per data type.MEMORY DOCTOR- auto-checks fragmentation, peak vs current, defrag effectiveness.MEMORY MALLOC-STATS- jemalloc dump; in bins, highnslabsrelative tocurregs= fragmented size class.
Skip defrag when
- Instance < 1 GB used_memory - overhead negligible.
- Low-churn / stable key population.
- CPU-bound deployments - defrag competes on the main thread.
- Short-lived instances (daily restart resets fragmentation).
- Fragmentation is in
allocator_rss_ratio(notallocator_frag_ratio) - defrag can't help.
Version notes
- 8.1 new hashtable (64 B buckets, 7 entries/bucket, chain, SIMD presence scan) -> fewer small allocations -> less fragmentation under churn. Many workloads no longer need defrag after 8.1; re-measure first.
- 9.0 Reply Copy Avoidance reduces alloc churn on read-heavy workloads marginally.
Latency diagnosis
Baseline tools
valkey-cli --intrinsic-latency N(run on server host) - OS scheduling floor over N seconds. >1 ms = noisy env (VM contention, throttling, NUMA). Bare metal < 100 us.valkey-cli --latency -h host -p 6379- PING RTT, continuous min/max/avg.valkey-cli --latency-history ...- 15 s windows.valkey-cli --latency-dist ...- distribution spectrum.
Network RTT >10x intrinsic -> network, not server.
LATENCY monitor
Disabled by default (latency-monitor-threshold = 0). Enable: CONFIG SET latency-monitor-threshold 5 (ms). Overhead negligible.
Subcommands: LATENCY LATEST | HISTORY <event> | GRAPH <event> | DOCTOR | RESET [event...] | HISTOGRAM [cmd...].
LATENCY HISTORY keeps up to 160 samples/event. LATENCY HISTOGRAM <cmds...> = per-command us distribution.
Event types:
command- slow command; cross-reference COMMANDLOG.fast-command- O(1) cmd exceeded threshold; system-level (not query) issue.fork- RDB/AOFrewrite fork; ~1-2 ms per GB dataset.expire-cycle- active expiration burst.active-defrag-cycle- defrag CPU.aof-fsync-always- fsync-always blocking main thread.aof-write-pending-fsync- write delayed by in-flight fsync -> disk contention.
Run LATENCY DOCTOR first - auto-checks THP, fork speed, disk contention.
COMMANDLOG
See valkey-features.md for full command surface (8.1+, replaces SLOWLOG). Use during latency diagnosis:
COMMANDLOG GET N slow # execution time outliers
COMMANDLOG GET N large-request # payload bytes
COMMANDLOG GET N large-reply # response bytesSet CLIENT SETNAME per service to trace entries back.
Fork pause sources
INFO persistence:
latest_fork_usec- last fork duration.rdb_last_cow_size- approachesused_memorywhen THP inflates COW.
THP: COW operates on 2 MB pages instead of 4 KB - one-byte write copies 2 MB. Disable: echo never > /sys/kernel/mm/transparent_hugepage/enabled.
Fork blocks clients ~1-2 ms/GB; 64 GB -> 64-130 ms pause.
Expiration storms
Active expiration loops a DB while sampled-stale > ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE (10%, tuned by active-expire-effort). Identical TTLs -> bursts. Fix: jitter EX 3600 + rand(0, 300).
AOF / swap
appendfsync everysec: background fsync. If fsync >1 s (disk contention), main thread stalls up to 1 additional second; signal = aof-write-pending-fsync events.
Swap: 10-100 ms per swapped page. Keep maxmemory <= ~75% RAM.
I/O threads (8.0+)
Default ON. High p99 + normal p50 + clean COMMANDLOG -> queue latency; raise io-threads.
INFO latencystats
eventloop_duration_sum, eventloop_duration_cmd_sum. Cmd dominates -> COMMANDLOG; otherwise loop time on I/O/persistence/periodic tasks.
CLIENT LIST
Flag b = blocked; high omem = output buffer backing up.
Slow-command replacements
| Slow | Use |
|---|---|
| KEYS [pattern] | SCAN cursor MATCH |
| HGETALL (large) | HSCAN / HMGET specific fields |
| SMEMBERS (large) | SSCAN |
| SORT (large) | pre-sort in app or use ZSET |
| DEL (large key) | UNLINK |
| LRANGE 0 -1 | paginate explicit ranges |
Throughput
Lazyfree defaults flipped in Valkey 8.0 (were no in Redis)
All default to yes:
lazyfree-lazy-user-del-DELruns async on background thread.lazyfree-lazy-user-flush-FLUSHDB/FLUSHALLasync.lazyfree-lazy-eviction- eviction frees async.lazyfree-lazy-expire- expiration frees async.lazyfree-lazy-server-del- implicit deletes (e.g.RENAMEoverwrite) async.
Prefer explicit UNLINK in code - intent-visible and unaffected if someone flips lazyfree-lazy-user-del no.
SCAN semantics
SCAN cursor [MATCH pattern] [COUNT hint] [TYPE t]
COUNTis a hint, not a hard limit; actual returned count may be more or fewer.- Same key may appear across multiple iterations -> dedupe client-side.
- Empty pages are valid; keep iterating until cursor returns
0. - Type variants:
HSCAN,SSCAN,ZSCANfor inside a single key;SCAN TYPE <t>for top-level filter.
Pipelining
Reduces syscall overhead (one read()/write() per batch), not just RTT. ~5-10x on loopback; ~10x non-pipelined baseline before plateau.
Batch sweet spot: ~10,000 commands per flush, read replies, next batch. Larger batches grow the server reply buffer.
Auto-pipelining:
- ioredis:
enableAutoPipelining: truebatches within one event-loop tick. - valkey-glide: default via multiplexed connection design.
MULTI/EXEC inside a pipeline = atomicity + one round-trip.
Pipeline for independent commands. Lua / FUNCTION when cmd B depends on cmd A's result.
Connection pooling
Start at num_cores * 2. Idle 30-60 s. Connect timeout 2-5 s.
Rules:
- Dedicated pool for pub/sub (subscribed connections can't serve regular commands).
- Dedicated connection for blocking ops (
BLPOP,BRPOP,XREAD BLOCK) - same reason. - RESP2 client-side caching: dedicate the invalidation connection.
- All-busy -> raise pool size or fix slow commands.
GLIDE: one multiplexed connection per cluster node with auto-pipelining - no pool needed.
I/O threading
io-threads N - N includes the main thread. Max 256. Command execution stays single-threaded regardless of N.
Starting points: 2-4 cores -> 2; 6-8 -> 4; 12-16 -> 6-8; 32+ -> 8 and benchmark.
Hidden/legacy knobs:
events-per-io-thread(default 2) - epoll events per cycle before yielding. Raise to amortize more; lower only for tail-latency investigation.- `io-threads-do-reads` is silently ignored. Reads are always on I/O threads when
io-threads > 1. Safe to leave in valkey.conf for Redis migration.
Does NOT help: small payloads + few connections; CPU-bound Lua/EVAL; single pipelined client; Unix sockets.
Command-selection quick table
| Slow | Use |
|---|---|
KEYS <pat> | SCAN + MATCH |
DEL <bigkey> | UNLINK (after optional HSCAN/HDEL drain) |
HGETALL (large hash) | HMGET known fields or HSCAN |
SMEMBERS (large set) | SSCAN |
SORT (large) | Pre-sort via ZSET or sort client-side |
LRANGE 0 -1 | Paginate explicit ranges |
Individual SET loop | Pipeline or MSET |
Hot-key mitigation
- Shard the key:
counters:{0}...counters:{N-1}; client picks shard byhash(id) % N; aggregate at read time. - Read replicas for read-heavy hot keys (
WAITif read-after-write needed). CLIENT TRACKINGfor cache-friendly hot reads (see app-patterns.md).
valkey-benchmark
valkey-benchmark -t SET,GET # cmd list
-c 50 # concurrent connections (default 50)
-n 100000 # total requests (default 100000)
-P 16 # pipeline depth (default 1)
--threads 4 # client-side threads
-d 64 # payload size in bytes (default 3)
--tls # enable TLS
-q # quiet: ops/sec onlyPitfalls:
- Default
--threads 1+ serverio-threads 8-> client is the bottleneck. Match--threadsto serverio-threads. - One connection + deep pipeline saturates the socket buffer. Prefer
-c 100 -P 16over-c 1 -P 1000. - Warm-up pass first, then measure.
- Bench your real R/W mix, not GET-only.
- Pipeline plateau: linear P1->P16, flattens by P64-128; deeper only adds per-batch latency.
For realistic mixed workloads: memtier_benchmark --ratio 4:1 (80/20 R/W) validates io-threads / MPTCP / TLS-offload changes better than single-command benches.
Lua scripting and FUNCTIONs
EVAL/EVALSHA vs FCALL
EVAL "<src>" N keys... args...- inline. First call compiles; subsequent calls useEVALSHA <sha1> N keys... args....SCRIPT LOAD "<src>"-> sha1. Cache is volatile: NOSCRIPT after restart or primary failover; client must be ready to reload.FUNCTION LOAD "#!lua name=<lib>\n <src>"- persists in RDB/AOF, survives restart/failover. Call viaFCALL <fname> N keys... args....- Production scripts that must survive restarts/failover ->
FUNCTION LOAD, notSCRIPT LOAD.
Replication
Effects replication always on. redis.replicate_commands() is no-op. Replicas receive write commands, not Lua source.
Read-only variants
EVAL_RO, FCALL_RO reject writes.
- Route to replicas in cluster mode; plain EVAL/FCALL require primary.
- ACL
@read-only users can call them. FCALL_ROrequires function registered withflags = {'no-writes'}.- KEYS[] must hash to same slot.
Script timeout / BUSY
busy-reply-threshold default 5000 ms (legacy alias lua-time-limit).
Exceeded -> BUSY state, commands reject with -BUSY ...:
- EVAL/EVALSHA:
SCRIPT KILLorSHUTDOWN NOSAVE. - FCALL/FCALL_RO:
FUNCTION KILLorSHUTDOWN NOSAVE. - Kill works only before any write. After first write:
SHUTDOWN NOSAVEonly (partial writes not rolled back).
Lua memory counts against maxmemory (not separately capped) - accumulating script can OOM the server.
Native replacements (prefer over Lua)
| Pattern | Old Lua | Native |
|---|---|---|
| CAS | GET + conditional SET | SET key v IFEQ old (8.1+) |
| Safe lock release | GET + DEL | DELIFEQ key token (9.0+) |
No VM overhead, auditable, single-key (no hash-tag juggling). Full command surface in valkey-features.md.
Determinism
Avoid in write scripts (even under effects replication):
server.call('TIME')-> pass timestamp as ARGV.server.call('RANDOMKEY'),math.random()-> generate client-side.server.call('SRANDMEMBER', ...)in write paths -> FCALL_RO or accept explicitly.
Library registration
Shebang required on first line: #!lua name=<libname>.
FUNCTION LOAD "#!lua name=mylib\n
local function cas(keys,args) ... end
server.register_function('cas', cas)"server.register_function:
- Positional:
server.register_function('cas', cas). - Table form (required for flags/description):
server.register_function{function_name='getprofile', callback=getprofile, flags={'no-writes'}, description='...'}.
no-writes flag required to expose via FCALL_RO.
Management: FUNCTION LIST | DELETE <lib> | DUMP | RESTORE <bin>. FUNCTION LOAD REPLACE "..." atomic overwrite. Versioning: embed in name (mylib_v2) or REPLACE.
server API
redis.* is an alias of server.* (pre-Valkey compat). Prefer server.*.
server.call(...)- raises on error, aborts script.server.pcall(...)- returns single value: on error a Lua table{err = '...'}, NOT a(ok, val)tuple:
local r = server.pcall('get', KEYS[1])
if type(r) == 'table' and r.err then
return server.error_reply(r.err)
end
return rserver.error_reply("ERR ...")/server.status_reply("OK").
Cluster rules
All KEYS[] must hash to one slot; use {tag} to co-locate.
Anti-patterns
- Large aggregation returns -> serialization/timeout; paginate or aggregate client-side.
- Business logic / heavy compute -> blocks main thread for all clients.
- Iterating large collections in Lua -> SCAN-family in app code.
Auth / ACL / TLS
AUTH forms
AUTH <password>- default user.AUTH <username> <password>- named ACL user.- Server-side:
requirepass <pw>sets the default-user password. Named users viaACL SETUSER.
ACL syntax atoms
| Atom | Meaning |
|---|---|
on / off | enable / disable user |
>password | add password (hashed #hash form also accepted) |
<password | remove password |
nopass | allow empty-password login (testing only) |
resetpass | remove all passwords |
+cmd / -cmd | allow / deny a specific command |
+@category / -@category | allow / deny a command category |
| `+cmd\ | subcommand` |
~pattern | key pattern (e.g. ~app:*) |
%R~pattern | read-only access to pattern |
%W~pattern | write-only access to pattern |
allkeys / resetkeys | all keys / remove key patterns |
&channel-pattern | pub/sub channel pattern |
allchannels / resetchannels | all channels / remove channel patterns |
Command categories
@read @write @string @hash @list @set @sortedset @stream @pubsub @connection @transaction @scripting @admin @dangerous @slow @fast @geo @keyspace @bitmap @hyperloglog
Runtime: ACL CAT - list categories; ACL CAT <cat> - commands in category; ACL WHOAMI, ACL LIST, ACL GETUSER <name>.
DB isolation footgun
ACLs do NOT restrict by database number. A user with +@write ~* writes in any numbered DB the server has. Use separate Valkey instances or separate cluster deployments for true DB-level isolation. Valkey 9.0+ cluster multi-DB does not change this.
Canonical user templates
# Read-write application
ACL SETUSER appuser on >pw ~app:* +@read +@write +@connection -@admin -@dangerous
# Read-only replica reader
ACL SETUSER reader on >pw ~* +@read +@connection
# Cache-only, narrow command set
ACL SETUSER cacheuser on >pw ~cache:* +GET +SET +DEL +UNLINK +EXPIRE +TTL +@connection
# Queue worker (streams + blocking list ops)
ACL SETUSER worker on >pw ~queue:* +XREADGROUP +XACK +XADD +BLPOP +RPUSH +@connection
# Separate read and write surfaces within one user
ACL SETUSER dual on >pw %R~read:* %W~write:* +@read +@write +@connection-@admin + -@dangerous exclude FLUSHALL/CONFIG/DEBUG/KEYS etc. - apply to any application user.
Connection stdlib knobs
CLIENT SETNAME <name>- trace client in COMMANDLOG / CLIENT LIST. Set per service.CLIENT NO-EVICT ONon long-lived pub/sub subscriber / tracking invalidation connections to prevent server eviction under memory pressure.
TLS
tls-port (commonly 6380) + certs on the server. Client provides CA (and cert+key for mTLS). 6379 stays plaintext unless tls-port replaces it.
8.1+ offloads TLS handshakes to I/O threads - connection bursts no longer block main thread. Tune io-threads for TLS-heavy connection churn. Per-command TLS cost after handshake is minimal.
Valkey-specific features and version matrix
Version-gated commands, per-release changelog, Redis compatibility, module gap.
Release history
| Version | Highlights |
|---|---|
| 7.2.4 | Fork baseline (March 2024). Wire-compatible with Redis OSS 7.2. |
| 8.0 | I/O threading default-on (~3x: 360K -> 1.2M RPS), dual-channel replication, all lazyfree-lazy-* defaults flipped to yes, repl-diskless-sync default yes. |
| 8.1 | SET IFEQ, new cache-line hashtable (64 B buckets, 7 entries/bucket, SIMD presence scan, incremental rehash), COMMANDLOG (replaces SLOWLOG), TLS handshake offload to I/O threads (~10% gain), iterator prefetch (~3.5x SCAN/KEYS/HGETALL/HSCAN/SSCAN/ZSCAN), ZRANK ~45% faster on unique scores, BITCOUNT SIMD (~6x @ 1 MB / ~10x @ 10 MB, AVX2/NEON), PFMERGE/PFCOUNT SIMD (~12x multi-HLL dense merge). |
| 9.0 | DELIFEQ, hash-field TTL (11 new commands), numbered DBs in cluster mode, atomic slot migration (CLUSTER MIGRATESLOTS), GEOSEARCH BYPOLYGON, MPTCP replication, Reply Copy Avoidance, additional SIMD (BITCOUNT, HLL/hash findBucket), pipeline memory prefetch, un-deprecated 23 commands. RDB magic -> VALKEY at version 80+. |
SET IFEQ (8.1+) - conditional update
SET key new_value IFEQ expected_value [EX s | PX ms | EXAT unix-s | PXAT unix-ms | KEEPTTL] [GET]Returns OK on match-and-store; nil on mismatch or missing key.
- `IFEQ never creates a missing key`: a CAS retry that treats
nilas "lost the race" must also handle deleted-since-read. CheckEXISTSafter or re-bootstrap viaSET ... NX. - `GET` ambiguity: returns old value before IFEQ is evaluated. Caller cannot distinguish match+set from mismatch+no-change from the GET reply alone. Use non-GET form for the outcome.
- Mutually exclusive with
NXandXX(syntax error if combined). - Byte-exact comparison:
"100"!="100 "(trailing space). - String-only; non-string key returns
WRONGTYPE.
DELIFEQ (9.0+) - conditional delete
DELIFEQ key expected_valueReturns 1 if matched and deleted; 0 if missing OR mismatch; no change.
String-only; WRONGTYPE on non-string.
Redlock unlock: 0 from any of the N instances is expected, not a failure (lock expired or different owner). Continue; don't retry the 0.
IFEQ/DELIFEQ replication rewrite
SET ... IFEQ and DELIFEQ are rewritten to plain `SET` / `DEL` in the replication stream and AOF. Conditional is evaluated once on the primary.
- Replicas don't re-run the comparison -> deterministic under replay/reconnect.
- AOF grep for
DELIFEQ/SET ... IFEQmisses them - grepSET/DEL. MONITORon primary shows original command; on replica shows the rewritten one.
Native replacements for Lua
| Pattern | Lua | Native |
|---|---|---|
| CAS | if call('get',K)==V then call('set',K,V') end | SET K V' IFEQ V (8.1+) |
| Safe lock release | if call('get',K)==V then call('del',K) end | DELIFEQ K V (9.0+) |
Native: no Lua VM, no script caching, single-key (no hash-tag slot constraints).
Hash field TTL (9.0+) - 11 new commands
Setters on existing fields
HEXPIRE key <s> [NX|XX|GT|LT] FIELDS n field [field...]
HPEXPIRE key <ms> [NX|XX|GT|LT] FIELDS n field [field...]
HEXPIREAT key <unix-s> [NX|XX|GT|LT] FIELDS n field [field...]
HPEXPIREAT key <unix-ms> [NX|XX|GT|LT] FIELDS n field [field...]NX = only if no TTL; XX = only if TTL; GT = only if new > current; LT = only if new < current. Mutually exclusive. Per-field.
Inspection
HTTL key FIELDS n field... - remaining seconds per field
HPTTL key FIELDS n field... - remaining ms per field
HEXPIRETIME key FIELDS n field... - absolute Unix s
HPEXPIRETIME key FIELDS n field... - absolute Unix msClear TTL
HPERSIST key FIELDS n field...Combined set-and-expire, read-and-refresh
HSETEX key [FNX|FXX] [EX s|PX ms|EXAT unix-s|PXAT unix-ms|KEEPTTL]
FIELDS n field value [field value...]
HGETEX key [EX s|PX ms|EXAT unix-s|PXAT unix-ms|PERSIST]
FIELDS n field [field...]FNX/FXX on HSETEX apply to the whole op: FNX requires none of the fields exist, FXX requires all exist. Condition failure -> nothing written (atomic).
FIELDS n count must match the number of field names - mismatch = syntax error.
Return codes
HEXPIRE/HEXPIREAT/HPEXPIRE/HPEXPIREAT (array, per field):
1- TTL applied.2- TTL was 0 or absolute time in the past - field deleted immediately.0- NX/XX/GT/LT condition not met; no change.-2- field (or hash key) does not exist.- `-1` is NOT a setter code - checking for it is a common mistake;
-1is an HTTL/HPTTL code.
HTTL/HPTTL/HEXPIRETIME/HPEXPIRETIME (array, per field):
- Positive - remaining TTL / absolute expiry.
-1- field exists, no TTL.-2- field does not exist.
HPERSIST (array, per field):
1- TTL removed.-1- field exists, had no TTL.-2- field does not exist.
HSETEX (scalar): 1 all written; 0 FNX/FXX failed; nothing written.
HGETEX (array, same shape as HMGET): values in field order; nil for missing or already-expired fields.
HEXPIRE key 0 = conditional delete
TTL 0 (or past absolute time) deletes the field immediately and returns 2. Existence-conditional multi-field delete pattern.
Visibility semantics
- Lazy + active expiration, same as key-level TTL.
HGETALL,HSCAN,HKEYS,HVALSskip expired fields.HLENdoes NOT filter expired fields - physical count until next sweep. Accurate live count:len(HKEYS)or sumHEXISTS.- Just-expired window: field may return once, then disappear. Strict aliveness ->
HTTL. - Keyspace notifications fire when
notify-keyspace-eventsenabled.
Footguns
1. `HSET` strips field TTL. Use HSETEX ... KEEPTTL FIELDS n field value for in-place value updates that must preserve TTL. 2. HEXPIRE is field-level, not key-level. Set a key EXPIRE as safety net - all-persistent fields + no key TTL -> session lives forever. 3. `HSETEX` creates the key if missing. "Refresh CSRF token" on a logged-out session silently re-creates it. Gate refresh writes on EXISTS <key>. 4. HINCRBY replication rewrite: on a hash with volatile fields, HINCRBY replicates as HSETEX ... PXAT ... FIELDS 1 <field> <new>, not as HINCRBY. AOF grep for HINCRBY misses it.
Interaction with key-level TTL
Key EXPIRE still works; whichever triggers first wins. Key expiration drops all fields including those with field TTL. PERSIST key affects only key TTL - field TTLs unchanged.
Memory
Only TTL-bearing fields pay metadata cost. Adding TTLs promotes hash to volatile-capable encoding (small per-field overhead). Per-field metadata via an ebuckets structure alongside the hash.
Key-level TTL reference
| Command | Positive | -1 | -2 |
|---|---|---|---|
TTL key | seconds remaining | key has no TTL | key missing |
PTTL key | ms remaining | key has no TTL | key missing |
EXPIRETIME key | absolute Unix seconds | key has no TTL | key missing |
PEXPIRETIME key | absolute Unix ms | key has no TTL | key missing |
PERSIST key returns 1 if a TTL was removed, 0 for both "no TTL" and "key missing" - use EXISTS to distinguish.
EXPIRETIME/PEXPIRETIME (Redis 7.0+) - prefer the absolute form when passing expiration to another service (relative TTL decays in transit).
COMMANDLOG (8.1+, replaces SLOWLOG)
Commands
COMMANDLOG GET <count> <slow|large-request|large-reply>
COMMANDLOG LEN <slow|large-request|large-reply>
COMMANDLOG RESET <slow|large-request|large-reply>
COMMANDLOG HELPCOMMANDLOG GET requires explicit count. SLOWLOG GET (legacy) defaults count to 10.
Thresholds / retention (all CONFIG SET-able)
| Config | Default | Type |
|---|---|---|
commandlog-execution-slower-than | 10000 | microseconds |
commandlog-request-larger-than | 1048576 | bytes |
commandlog-reply-larger-than | 1048576 | bytes |
commandlog-slow-execution-max-len | 128 | entries |
commandlog-large-request-max-len | 128 | entries |
commandlog-large-reply-max-len | 128 | entries |
Threshold -1 disables that log type. 0 logs every command of that type (rarely intended).
Legacy config aliases
| Legacy | Canonical |
|---|---|
slowlog-log-slower-than | commandlog-execution-slower-than |
slowlog-max-len | commandlog-slow-execution-max-len |
No legacy alias for -request-larger-than or -reply-larger-than (new in 8.1). SLOWLOG GET/LEN/RESET still work and read the same slow log.
Entry shape
1) id - unique entry ID
2) timestamp - Unix seconds
3) duration/size - us (slow) or bytes (large-*)
4) command_args - command + args
5) "client_addr" - IP:port
6) "client_name" - CLIENT SETNAME valueCluster
COMMANDLOG GET/LEN/RESET carry REQUEST_POLICY: ALL_NODES; LEN also RESPONSE_POLICY: AGG_SUM. Cluster-aware clients (valkey-cli -c, smart SDKs) dispatch to every shard and aggregate - don't loop per-node manually.
GEOSEARCH BYPOLYGON (9.0+)
GEOSEARCH key
BYPOLYGON <num-vertices> lon1 lat1 lon2 lat2 ... lonN latN
[ASC | DESC]
[COUNT count [ANY]]
[WITHCOORD] [WITHDIST] [WITHHASH]num-vertices >= 3.- Polygon is auto-closed - do NOT repeat the first vertex.
- Must be simple (non-self-intersecting). Winding (CW/CCW) doesn't matter.
FROMMEMBER/FROMLONLATare rejected with BYPOLYGON (syntax error); they're mandatory for BYRADIUS/BYBOX.
WITHDIST quirk
With BYPOLYGON, WITHDIST returns distance from the polygon's computed centroid, not from any user-supplied point. For distance from a known location, compute client-side or issue a separate GEODIST.
Coordinate storage
- Longitude: -180 to 180.
- Latitude: -85.05112878 to 85.05112878 (Web Mercator limits).
- Internally a sorted set (score = geohash); sorted-set commands work on geo keys.
Footguns
- Antimeridian wrap: bounding box is min/max lon/lat; a polygon spanning 170° to -170° through the Pacific produces a box covering the whole globe, over-matches. Split into two polygons (east/west of 180°), union client-side.
- Self-intersecting polygons produce even-odd fill. Keep simple.
- Very small polygons (< 100 m) hit geohash grid precision - verify with
GEODIST.
GEOSEARCHSTORE
Supports BYPOLYGON with same geometry, but rejects `WITHCOORD`, `WITHDIST`, `WITHHASH` with an error. Store-and-count XOR annotated-results.
Complexity
GEOSEARCH is O(N + log M) where N = elements in the grid-aligned bounding box around the shape, M = matches actually inside. For large geo sets, always pass COUNT.
Numbered DBs in cluster mode (9.0+)
Pre-9.0: only DB 0 in cluster; SELECT errored.
cluster-databases <N> enables DBs 0..N-1. Default 1.
- Immutable at runtime - set in valkey.conf or cmd line;
CONFIG SETis rejected; change requires restart. - Hash slot is computed from the key name alone -> same slot across DBs (but namespaces are independent).
MOVE reply semantics
MOVE key db returns:
1- moved.0- ambiguous: either source key missing OR destination already holds that key. Caller must disambiguate (EXISTS before/after).- Cluster redirect error if the slot is currently mid-migration on this node.
Per-node scope
FLUSHDB, SCAN, DBSIZE operate on this node only - not cluster-wide. Iterate every primary.
Isolation caveat
No resource isolation across DBs - shared memory/CPU/connections. ACL granularity is coarse. For real isolation use separate instances or clusters.
Atomic slot migration (9.0+)
CLUSTER MIGRATESLOTS SLOTSRANGE <start> <end> NODE <target-node-id> [SLOTSRANGE ... NODE ...]
CLUSTER GETSLOTMIGRATIONS
CLUSTER CANCELSLOTMIGRATIONSRanges inclusive. Node-id = 40-hex from CLUSTER NODES. Multiple ranges/targets in one call. CANCELSLOTMIGRATIONS is a safe no-op after cutover.
Protocol
1. Source snapshots the slot (AOF-format rewrite internally). 2. Streams snapshot + live writes to target. 3. At catch-up, source briefly pauses writes on that slot (milliseconds). 4. Ownership transfers atomically. 5. Next hit on the old owner gets a single MOVED.
No per-key ASK redirects. Other slots unaffected.
CLUSTER GETSLOTMIGRATIONS fields
operation-IMPORT/EXPORT.slot_ranges- e.g."0-4095".source_node,target_node- 40-char node IDs.state- snapshotting / streaming / paused / failover / cleaning up / finished / cancelled / failed.last_update_time- detect stalls.
Footgun
`valkey-cli --cluster reshard` still uses the legacy per-key path in 9.0 - does NOT call MIGRATESLOTS. Call CLUSTER MIGRATESLOTS directly for atomic behavior.
Latency-sensitive: schedule migrations outside peak traffic (ms-scale cutover pause).
Dual-channel replication (8.0+)
Two TCP connections during full resync - one for RDB, one for live repl stream. Eliminates primary output-buffer-replica overhead -> no resync loop.
dual-channel-replication-enabled yeson replica (default no).- Primary must have
repl-diskless-sync yes. - Negotiated in PSYNC handshake; falls back to single-channel if either side lacks it.
MPTCP (9.0+)
Kernel 5.6+ required. Both keys are immutable (startup-only, not CONFIG SET):
mptcp yes # listener accepts MPTCP from clients
repl-mptcp yes # replica opens MPTCP to primaryStartup fails with MPTCP is not supported on this platform if kernel lacks it. Enable at runtime: sysctl -w net.mptcp.enabled=1.
Clients do not need MPTCP support - server MPTCP socket negotiates in the handshake; non-MPTCP clients fall back to standard TCP, no failure.
Benefit: loss resilience / latency-variance reduction, not bandwidth. Near-zero delta on clean single-path. Pays off with packet loss + ≥2 reachable paths.
Verify: ss -M shows active MPTCP subflows.
Valkey 9.0 RDB magic change
RDB version 80+ uses VALKEY header; earlier versions keep REDIS. Both load in Valkey. Redis OSS cannot load version 80+.
Valkey 9.0 pipeline memory prefetch
Parser reads multiple commands from query buffer; keys prefetched in batches. prefetch-batch-max-size default 16, max 128. Also helps MGET/MSET/DEL when pipelined or under I/O threads.
Valkey 9.0 Reply Copy Avoidance
Skips a payload copy for large bulk-string replies under I/O threads. Internal configs min-io-threads-avoid-copy-reply, min-string-size-avoid-copy-reply.
Redis compatibility
Baseline
Compatible with Redis OSS 2.x through 7.2.x.
- RESP2 and RESP3 wire protocol identical.
- All Redis 7.2 commands work unchanged.
- Existing Lua scripts work unchanged.
- Module API compatible.
- RDB (pre-9.0 magic) and AOF files from Redis <= 7.2 load directly.
- Port defaults: 6379 client, 26379 Sentinel, cluster bus = port + 10000.
Incompatible: Redis CE 7.4+
Redis Community Edition 7.4+ (post-license-change) is NOT compatible:
- Proprietary code paths.
- RDB versions in reserved foreign range 12-79; Valkey rejects these by default.
- Proprietary data formats.
Direct file copy or REPLICAOF will not work. Use third-party migration tools: RIOT or RedisShake.
Identity surfaces that differ
valkey-server,valkey-clibinaries.valkey.conf(same format asredis.conf).INFO serverreportsvalkey_version(notredis_version).- Default paths:
/var/lib/valkey,/var/log/valkey, uservalkey.
extended-redis-compatibility yes
Runtime-modifiable (CONFIG SET, no restart). Valkey reports as "redis" with REDIS_VERSION 7.2.4 in HELLO, INFO, LOLWUT, CLIENT SETNAME. Use for clients that string-match the server identity.
Client libraries
Generic Redis clients work unchanged: ioredis, node-redis, redis-py, Jedis, Lettuce, Redisson, go-redis, rueidis, StackExchange.Redis.
Valkey-specific forks/ports with extra features: valkey-py, valkey-glide (official, 7 languages).
Module gap vs Redis 8+ built-ins
Redis 8+ bundles full-text search, vector search, time series, extended probabilistic structures. Valkey splits these into separate modules:
- valkey-search - full-text and vector search.
- valkey-bloom - Bloom filter, cuckoo filter.
- valkey-json - JSON document type.
- Time series - no Valkey equivalent today.
Valkey identity: forked from Redis 7.2.4 (March 2024), BSD 3-clause, Linux Foundation.
Related skills
FAQ
What patterns does the valkey skill cover?
Application idioms including caching, sessions, locks, rate limiting, queues, counters, leaderboards, pub/sub, and search, plus performance, cluster/HA, scripting, and security.
Is this skill for Valkey server internals?
No. It is for building apps against Valkey; use valkey-dev for server internals and valkey-ops for operations.