Tooling & Monitoring
Verified on Redis 8.10.2 (
redis:8.10.2-alpine),redis-cliandredis-benchmark8.10.2, ioredis 6.0.0, Node 24, Apple-silicon Mac (Docker). The# cli/...transcript is one session in document order, replayed by the course harness. Every other output below is real output from my own throwaway container:--bigkeysand--keystatsran on 126 529 keys (aredis-benchmark -t set -rload plus one key of each type), theSLOWLOG,MONITORand latency runs on about 1 million keys. Numbers are from this Mac and will differ on yours.
redis-cli is more than a REPL
Section titled “redis-cli is more than a REPL”redis-cli ships diagnostic modes that need no extra software: --bigkeys, --keystats, --memkeys, --scan, --latency, --latency-history, --latency-dist, plus the server-side INFO, SLOWLOG, MEMORY, LATENCY, MONITOR and CLIENT commands. Below, each one with what it actually prints.
INFO: server statistics by section
Section titled “INFO: server statistics by section”INFO takes a section name (server, memory, stats, commandstats, keyspace, clients, threads, …). The fields worth watching, real values from my container:
# INFO serverredis_version:8.10.2redis_mode:standaloneos:Linux 6.12.76-linuxkit aarch64tcp_port:6379
# INFO memoryused_memory_human:61.33Mused_memory_peak_human:83.19Mmaxmemory_human:0B # 0B = no limitmem_fragmentation_ratio:1.42number_of_cached_scripts:0
# INFO statstotal_commands_processed:8284131instantaneous_ops_per_sec:1keyspace_hits:3506741keyspace_misses:2expired_keys:0evicted_keys:0
# INFO commandstats (one line per command)cmdstat_config|get:calls=18,usec=127,usec_per_call=7.06,rejected_calls=0,failed_calls=0cmdstat_hset:calls=1,usec=17,usec_per_call=17.00,rejected_calls=0,failed_calls=0
# INFO keyspacedb0:keys=994505,expires=0,avg_ttl=0,subexpiry=0keyspace_hits and keyspace_misses give the cache hit ratio, evicted_keys and expired_keys show memory pressure, commandstats tells you which command is hot and how long each call costs on average, and mem_fragmentation_ratio above about 1.4 is what MEMORY DOCTOR complained about below.
INFO server
INFO memory
INFO keyspaceSLOWLOG: find slow commands
Section titled “SLOWLOG: find slow commands”Redis records commands whose execution exceeds slowlog-log-slower-than microseconds (default 10000 = 10 ms) in a ring of slowlog-max-len entries (default 128). Time spent waiting in the queue or writing the reply to the socket is not counted. I forced an entry by running KEYS * against about a million keys:
$ redis-cli SLOWLOG RESET$ redis-cli KEYS '*' > /dev/null$ redis-cli SLOWLOG GET 11) 1) (integer) 1 <- entry id 2) (integer) 1790753365 <- unix time 3) (integer) 150614 <- execution time in microseconds (150 ms) 4) 1) "KEYS" <- the command and its arguments 2) "*" 5) "127.0.0.1:57960" <- client address 6) "" <- client name (CLIENT SETNAME) 7) (integer) 2 <- number of original argumentsSLOWLOG LEN counts entries and SLOWLOG RESET clears them. On this Mac KEYS * on 126 529 keys stayed under the 10 ms threshold and was not logged at all, which is how a “harmless” KEYS in development becomes an incident once the dataset grows.
The transcript: sizes, encodings, client names
Section titled “The transcript: sizes, encodings, client names”127.0.0.1:6379> CLIENT SETNAME reporterOK127.0.0.1:6379> CLIENT GETNAME"reporter"127.0.0.1:6379> SET user:2:name adaOK127.0.0.1:6379> SET user:2:age 36OK127.0.0.1:6379> MEMORY USAGE user:2:name(integer) 40127.0.0.1:6379> MEMORY USAGE user:2:age(integer) 29127.0.0.1:6379> HSET user:1 name ada age 36(integer) 2127.0.0.1:6379> OBJECT ENCODING user:1"listpack"127.0.0.1:6379> MEMORY USAGE user:1(integer) 50127.0.0.1:6379> OBJECT FREQ user:1(error) ERR An LFU maxmemory policy is not selected, access frequency not tracked. Please note that when switching between policies at runtime LRU and LFU data will take some time to adjust.127.0.0.1:6379> CONFIG GET slowlog-log-slower-than1) "slowlog-log-slower-than"2) "10000"127.0.0.1:6379> CONFIG GET slowlog-max-len1) "slowlog-max-len"2) "128"127.0.0.1:6379> CONFIG GET busy-reply-threshold1) "busy-reply-threshold"2) "5000"CLIENT SETNAMEtags a connection so it is recognisable inCLIENT LISTand inSLOWLOG(field 6).MEMORY USAGE keyis the bytes a key costs, including its overhead. The two-field object as a hash cost 50 bytes; as two separate string keys it cost 40 + 29 = 69. That is the measured basis for “hashes are cheaper than one key per field” for small objects (the hash is a compactlistpackhere).OBJECT FREQneeds an LFUmaxmemory-policy; on the default policy it errors, as shown.
CLIENT LIST
CLIENT GETNAME
CLIENT INFO—bigkeys and —keystats: find memory-hungry keys
Section titled “—bigkeys and —keystats: find memory-hungry keys”Both run from your shell and walk the keyspace with SCAN, so they do not block the server (they still cost CPU and network; add -i 0.1 to sleep between batches). Real --bigkeys output:
# Scanning the entire keyspace to find biggest keys as well as# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec# per 100 SCAN commands (not usually needed).
[00.00%] Biggest string found so far "key:000000142688" with 3 bytes[08.20%] Biggest hash found so far "user:profile:1" with 4 fields[65.20%] Biggest string found so far "session:heavy" with 5000 bytes[81.40%] Biggest stream found so far "events" with 1 entries
-------- summary -------
Sampled 126529 keys in the keyspace!Total key length in bytes is 2024420 (avg len 16.00)
Biggest list found "log:events" has 10 itemsBiggest hash found "user:profile:1" has 4 fieldsBiggest stream found "events" has 1 entriesBiggest string found "session:heavy" has 5000 bytesBiggest set found "tags" has 3 membersBiggest zset found "board" has 4 members
1 lists with 10 items (00.00% of keys, avg size 10.00)126524 strings with 384569 bytes (100.00% of keys, avg size 3.04)(The progress lines and per-type table are abridged.) Note the format: Biggest string found "session:heavy" has 5000 bytes (double-quoted key, size in bytes for strings, item counts for the others), which differs from the sample output an earlier version of this lesson showed. --bigkeys measures length/cardinality, not memory; a set of 3 members can be larger than a string. Redis 8’s --keystats adds the memory view and a distribution:
--- Top 10 key sizes --- 1 5.03K string "session:heavy" 2 286B stream "events" 3 66B list "log:events" 4 60B hash "user:profile:1" 5 51B zset "board" ...
--- Top size per type ---string "session:heavy" is 5.03Kstream "events" is 286B
Key size Percentile Total keys-------- ---------- ----------- 39B 0.0008% 1 48B 99.9960% 126524 5.03K 100.0000% 126529
Type Total keys Keys % Tot size Avg size Total length/card Avg ln/card--------- ------------ ------- -------- -------- ------------------ -----------string 126524 100.00% 5.80M 48B 375.56K 3Bredis-cli --memkeys is the same idea using MEMORY USAGE per key (Biggest string found "session:heavy" has 5152 bytes). Use --keystats when you want the size percentiles.
—scan: iterate the keyspace without blocking
Section titled “—scan: iterate the keyspace without blocking”# keys matching a pattern (SCAN under the hood)redis-cli --scan --pattern 'session:*'# count matching keysredis-cli --scan --pattern 'key:0000001*' | wc -lReal output: session:heavy for the first, and 86436 for the second, on the ~1M-key dataset. Each SCAN call touches a bounded slice of the keyspace (Keys and SCAN).
Measuring the cost of KEYS
Section titled “Measuring the cost of KEYS”KEYS is O(N) in the number of keys in the database and runs in the single command thread, so everything else waits. I measured how long another client waited while KEYS * ran against 994 505 keys, versus the same walk done with SCAN:
import { Redis } from 'ioredis';
const url = process.env.REDIS_URL!;const admin = new Redis(url);const probe = new Redis(url);
// A second connection pings every 5 ms and records the worst round trip while the admin connection works.async function worstPing<T>(work: () => Promise<T>) { let worst = 0; let running = true; const loop = (async () => { while (running) { const t = performance.now(); await probe.ping(); worst = Math.max(worst, performance.now() - t); await new Promise((r) => setTimeout(r, 5)); } })(); const t0 = performance.now(); const result = await work(); const took = performance.now() - t0; running = false; await loop; return { result, took, worst };}
const size = await admin.dbsize();
const k = await worstPing(() => admin.keys('*'));console.log(`KEYS * : ${size} keys, ${(k.result as string[]).length} returned in ${k.took.toFixed(0)} ms, worst PING from another client: ${k.worst.toFixed(0)} ms`);
const s = await worstPing(async () => { let n = 0; for await (const batch of admin.scanStream({ count: 1000 })) n += (batch as string[]).length; return n;});console.log(`SCAN COUNT 1000: ${s.result} keys in ${s.took.toFixed(0)} ms, worst PING from another client: ${s.worst.toFixed(0)} ms`);
admin.disconnect();probe.disconnect();Real output (this Mac, ~1M keys):
KEYS * : 994505 keys, 994505 returned in 583 ms, worst PING from another client: 210 msSCAN COUNT 1000: 994505 keys in 1902 ms, worst PING from another client: 3 msKEYS * returned faster overall (583 ms) but froze every other client for up to 210 ms; SCAN took longer (1902 ms, in 995 round trips) and never delayed the other client by more than 3 ms. That is the trade: SCAN gives you a bounded pause per call, at the price of total time and of a consistency model with duplicates (SCAN guarantees).
—latency: round-trip time
Section titled “—latency: round-trip time”redis-cli --latency # continuous min / max / avg / samplesredis-cli --latency-history # a new line every 15 s (change with -i)redis-cli --latency-dist # latency spectrumReal output of --latency (after 3 s) and --latency-history -i 2 (three lines of it):
0.048 0.250 0.068 75 <- --latency: min max avg (milliseconds) and number of samples, redrawn in place0.125 0.698 0.223 61 <- --latency-history: a fresh line per interval, same columns0.125 0.698 0.229 620.125 0.698 0.230--latency-dist is not an ASCII histogram: it prints a legend and a colour spectrum (ANSI 256-colour backgrounds with the characters . - * #), a row per time slice, so it needs a colour terminal. --latency measures the round trip of PING, so it includes network and queueing, not just command time.
Inside the server, LATENCY DOCTOR reports latency spikes, but latency monitoring is off by default:
$ redis-cli LATENCY DOCTORI'm sorry, Dave, I can't do that. Latency monitoring is disabled in this Redis instance. You may use "CONFIG SET latency-monitor-threshold <milliseconds>." in order to enable it. ...$ redis-cli CONFIG SET latency-monitor-threshold 100 # record events slower than 100 msOK$ redis-cli --no-raw LATENCY LATEST(empty array)The report is disabled until you set a threshold. Once it is on, LATENCY LATEST and LATENCY HISTORY event list the spikes it saw (empty here: nothing had exceeded 100 ms yet). Redis documents the whole feature in its latency monitor guide.
MEMORY DOCTOR
Section titled “MEMORY DOCTOR”$ redis-cli MEMORY DOCTORSam, I detected a few issues in this Redis instance memory implants:
* High total RSS: This instance has a memory fragmentation and RSS overhead greater than 1.4 (this means that the Resident Set Size of the Redis process is much larger than the sum of the logical allocations Redis performed). ...It turned a mem_fragmentation_ratio of 1.42 (after bulk loads into this container) into a plain-English diagnosis. Treat it as a hint, not a verdict: it points at what to check in INFO memory.
MONITOR: real-time command stream
Section titled “MONITOR: real-time command stream”$ redis-cli MONITOROK1790753481.575992 [0 127.0.0.1:54300] "SET" "foo" "bar"1790753481.662979 [0 127.0.0.1:54316] "GET" "foo"1790753481.732532 [0 127.0.0.1:54318] "HSET" "h" "a" "1"Each line is a Unix timestamp with microseconds, the database and the client address, then the command. It shows what an application is really sending, which is invaluable for a short debugging session. It also has a cost. The Redis documentation warns that “a single MONITOR client can reduce the throughput by more than 50%” in its (unscientific) benchmark (MONITOR). I measured it with redis-benchmark -n 300000 -c 50 -t set,get inside the container, with and without one redis-cli MONITOR connected (output discarded):
baseline SET 211 714 / 226 757 req/s GET 229 007 / 230 061 req/s p50 0.119 msone MONITOR client SET 188 323 / 166 389 req/s GET 196 979 / 158 311 req/s p50 0.151-0.183 msafter disconnecting SET 227 790 req/s GET 225 903 req/s p50 0.119 msOn this setup one MONITOR client cost roughly 10-30 % of throughput and raised p50 latency from 0.119 ms to 0.15-0.18 ms; throughput came straight back after it disconnected. The earlier claim that it “roughly doubles the write load” is not what either the docs or this measurement say. The cost is real, it grows with more monitors and with a slow reader (whose output buffer then grows), and the size depends on your workload, so never leave it running in production.
CLIENT LIST
Section titled “CLIENT LIST”$ redis-cli CLIENT LISTid=97 addr=127.0.0.1:46948 laddr=127.0.0.1:6379 fd=20 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=26 qbuf-free=20448 argv-mem=10 multi-mem=0 rbs=16384 rbp=16384 obl=0 oll=0 omem=0 ...One line per connection: id, addr, name (set with CLIENT SETNAME), age and idle seconds, flags, current db, sub/psub (subscriptions), multi (queued commands in a MULTI), buffer sizes (qbuf, omem) and the last cmd. Kill a stale connection with CLIENT KILL ID 97.
Redis Insight
Section titled “Redis Insight”Redis Insight is the official desktop client; the documentation describes it as a tool for visualising and optimising data with both GUI and CLI interactions. It is a convenient front end for what this lesson does from the shell. Download and feature details are on the Redis Insight page (both links resolved when I checked).
Reading AI-generated code
Section titled “Reading AI-generated code”An assistant writes an admin “how many sessions are active?” endpoint:
🐛 Spot the bug
app.get('/admin/active-sessions', async (_req, res) => {const keys = await redis.keys('session:*'); // "simple and correct"res.json({ active: keys.length });});Show the answer
KEYS is O(N) over the whole database and blocks the command thread until it finishes: in the run above it took 583 ms on a million keys and made a concurrent PING wait 210 ms, and at 150 ms it appears in SLOWLOG as a slow command (on a dataset one-eighth the size, 126 529 keys, it did not even reach the 10 ms threshold, which is why it “worked” in development). Iterate with SCAN (worst concurrent latency 3 ms), or better, keep the answer in a counter or a set so no scan is needed:
let n = 0;for await (const batch of redis.scanStream({ match: 'session:*', count: 1000 })) n += (batch as string[]).length;res.json({ active: n });Mental Model
Section titled “Mental Model”Redis diagnostics come in two kinds: instruments that read a gauge the server already keeps (INFO, SLOWLOG, MEMORY USAGE) and probes that make the server do extra work (KEYS, MONITOR, big --scan runs). Instruments are safe to read on a schedule; probes are like putting a hand in a running machine, so keep them short, targeted and off the hot path.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
SLOWLOG + INFO stats | Always-on, near-zero overhead | Only records execution time above the threshold; no live stream |
MONITOR | See every command as it arrives | Measured 10-30 % throughput loss here (docs: more than 50% in theirs); grows with monitors and slow readers |
--bigkeys / --keystats / --scan | Keyspace-wide picture without blocking | CPU and network while running; --bigkeys counts items, not bytes |
KEYS pattern | One command, everything at once | Froze another client for 210 ms on 1M keys |
LATENCY monitoring | Names the events that cause spikes | Off by default; needs a threshold |
| Redis Insight | Visual, includes a CLI | An extra install and network access |
Common Misconceptions
Section titled “Common Misconceptions”- “MONITOR doubles the write load.” The docs measure more than a 50% throughput drop in their test; here it was 10-30%. Neither is “doubling”, and both are too costly to leave on.
- “
--bigkeysshows the biggest keys by memory.” It shows length and cardinality;--memkeysand--keystatsshow bytes. - “
--latency-distdraws an ASCII histogram.” It prints a colour spectrum for a colour terminal. - “A command that is fast on my laptop is fast in production.”
KEYS *on 126 529 keys did not even reach the 10 ms slowlog threshold; on a million keys it took 150 ms of execution. - “The slowlog includes network time.” It records execution time only.
Common mistakes
Section titled “Common mistakes”KEYS *in application code — it blocks everyone (210 ms stall measured).- Leaving
MONITORon — a permanent throughput tax. - Never looking at
SLOWLOGorINFO statsuntil an outage — read them routinely (commandstatsnames the hot command). - Reading
--bigkeysas bytes — use--keystats/MEMORY USAGE. - Trusting
LATENCY DOCTORwithout enabling latency monitoring — it is off by default.
Real-world example
Redis’s own documentation has a latency diagnosis guide built around the latency monitor, and warns about
MONITORoverhead on the MONITOR page.