Skip to content

Tooling & Monitoring

Verified on Redis 8.10.2 (redis:8.10.2-alpine), redis-cli and redis-benchmark 8.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: --bigkeys and --keystats ran on 126 529 keys (a redis-benchmark -t set -r load plus one key of each type), the SLOWLOG, MONITOR and latency runs on about 1 million keys. Numbers are from this Mac and will differ on yours.

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 takes a section name (server, memory, stats, commandstats, keyspace, clients, threads, …). The fields worth watching, real values from my container:

# INFO server
redis_version:8.10.2
redis_mode:standalone
os:Linux 6.12.76-linuxkit aarch64
tcp_port:6379
# INFO memory
used_memory_human:61.33M
used_memory_peak_human:83.19M
maxmemory_human:0B # 0B = no limit
mem_fragmentation_ratio:1.42
number_of_cached_scripts:0
# INFO stats
total_commands_processed:8284131
instantaneous_ops_per_sec:1
keyspace_hits:3506741
keyspace_misses:2
expired_keys:0
evicted_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=0
cmdstat_hset:calls=1,usec=17,usec_per_call=17.00,rejected_calls=0,failed_calls=0
# INFO keyspace
db0:keys=994505,expires=0,avg_ttl=0,subexpiry=0

keyspace_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 keyspace

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 1
1) 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 arguments

SLOWLOG 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”
cli/tsl-tooling-1.redis
127.0.0.1:6379> CLIENT SETNAME reporter
OK
127.0.0.1:6379> CLIENT GETNAME
"reporter"
127.0.0.1:6379> SET user:2:name ada
OK
127.0.0.1:6379> SET user:2:age 36
OK
127.0.0.1:6379> MEMORY USAGE user:2:name
(integer) 40
127.0.0.1:6379> MEMORY USAGE user:2:age
(integer) 29
127.0.0.1:6379> HSET user:1 name ada age 36
(integer) 2
127.0.0.1:6379> OBJECT ENCODING user:1
"listpack"
127.0.0.1:6379> MEMORY USAGE user:1
(integer) 50
127.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-than
1) "slowlog-log-slower-than"
2) "10000"
127.0.0.1:6379> CONFIG GET slowlog-max-len
1) "slowlog-max-len"
2) "128"
127.0.0.1:6379> CONFIG GET busy-reply-threshold
1) "busy-reply-threshold"
2) "5000"
  • CLIENT SETNAME tags a connection so it is recognisable in CLIENT LIST and in SLOWLOG (field 6).
  • MEMORY USAGE key is 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 compact listpack here).
  • OBJECT FREQ needs an LFU maxmemory-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 items
Biggest hash found "user:profile:1" has 4 fields
Biggest stream found "events" has 1 entries
Biggest string found "session:heavy" has 5000 bytes
Biggest set found "tags" has 3 members
Biggest 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.03K
stream "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 3B

redis-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”
scripts/tsl-tooling-scan.sh
# keys matching a pattern (SCAN under the hood)
redis-cli --scan --pattern 'session:*'
# count matching keys
redis-cli --scan --pattern 'key:0000001*' | wc -l

Real 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).

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:

src/tsl-tooling-keys-latency.ts
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 ms
SCAN COUNT 1000: 994505 keys in 1902 ms, worst PING from another client: 3 ms

KEYS * 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).

scripts/tsl-tooling-latency.sh
redis-cli --latency # continuous min / max / avg / samples
redis-cli --latency-history # a new line every 15 s (change with -i)
redis-cli --latency-dist # latency spectrum

Real 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 place
0.125 0.698 0.223 61 <- --latency-history: a fresh line per interval, same columns
0.125 0.698 0.229 62
0.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 DOCTOR
I'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 ms
OK
$ 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.

$ redis-cli MEMORY DOCTOR
Sam, 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.

$ redis-cli MONITOR
OK
1790753481.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 ms
one MONITOR client SET 188 323 / 166 389 req/s GET 196 979 / 158 311 req/s p50 0.151-0.183 ms
after disconnecting SET 227 790 req/s GET 225 903 req/s p50 0.119 ms

On 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.

$ redis-cli CLIENT LIST
id=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 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).

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 });

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.

ChoiceBenefitCost
SLOWLOG + INFO statsAlways-on, near-zero overheadOnly records execution time above the threshold; no live stream
MONITORSee every command as it arrivesMeasured 10-30 % throughput loss here (docs: more than 50% in theirs); grows with monitors and slow readers
--bigkeys / --keystats / --scanKeyspace-wide picture without blockingCPU and network while running; --bigkeys counts items, not bytes
KEYS patternOne command, everything at onceFroze another client for 210 ms on 1M keys
LATENCY monitoringNames the events that cause spikesOff by default; needs a threshold
Redis InsightVisual, includes a CLIAn extra install and network access
  • “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.
  • “--bigkeys shows the biggest keys by memory.” It shows length and cardinality; --memkeys and --keystats show bytes.
  • “--latency-dist draws 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.
  • KEYS * in application code — it blocks everyone (210 ms stall measured).
  • Leaving MONITOR on — a permanent throughput tax.
  • Never looking at SLOWLOG or INFO stats until an outage — read them routinely (commandstats names the hot command).
  • Reading --bigkeys as bytes — use --keystats/MEMORY USAGE.
  • Trusting LATENCY DOCTOR without 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 MONITOR overhead on the MONITOR page.

Which command shows the N most recent commands whose execution time exceeded the threshold?
What did one attached MONITOR client cost in the redis-benchmark run?
Which redis-cli flag reports the biggest key of each type by length or cardinality?
Why is redis.keys('session:*') in a request handler a bug on a large keyspace?
The hash HSET user:1 name ada age 36 used 50 bytes while the same data as two string keys used 40 + 29. What does that show?
What does redis-cli --latency-dist print?