Skip to main content

Measuring Redis: latency, SLOWLOG and memory per key

The client measured 238 ms per command, the server 1.28 microseconds: how to measure latency, slow commands and bytes per key on Redis 8.0.5 and 7.4.7.

What do you think about this article?

At a Glance

The client measured 238 ms per command, the server 1.28 microseconds: how to measure latency, slow commands and bytes per key on Redis 8.0.5 and 7.4.7.

In short. "Redis is slow" and "Redis uses too much" are not measurements. There are three measurements. redis-cli --intrinsic-latency times the operating system, not the database: here the peak was 1,218 microseconds, a delay Redis did not cause. The SLOWLOG gives the slow commands: a SORT on 200,000 elements with LIMIT 0 10 blocked the server for 255,950 microseconds. The difference in used_memory gives the memory per key: 100,000 strings of 100 bytes cost 202.54 bytes each, while MEMORY USAGE declared 184 and the site's estimator 160. But the measurement that teaches the most is another: while one client ran SORT in a loop, a second one measured 238.24 ms on average on PING, and the SLOWLOG has no trace of those PINGs.

Method. Every command was run on 8 November 2026 and every number comes from that run. Where the output was long I cut it, and the SLOWLOG arguments are on a single line: the numbers are the ones that came out. At the end, the numbers that are not transferable.

The environment, before the numbers

The machine and the versions

$ redis-server --version
Redis server v=8.0.5 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=d47fc40702b1fad8
$ redis-server --port 6392 --save "" --appendonly no --daemonize yes --dir /tmp/redis6392
$ redis-cli -p 6392 info server | grep -E "redis_version|^os|io_threads_active"
redis_version:8.0.5
os:Linux 7.0.0-42-generic x86_64
io_threads_active:0
$ nproc
12
$ grep -m1 "model name" /proc/cpuinfo
model name  : AMD Ryzen 5 5600H with Radeon Graphics
$ cat /proc/loadavg
1.37 11.43 21.43 3/2377 763499

Three lines must be read together. It is a laptop with 12 threads, not a server. The fifteen-minute load average is 21.43: other heavy processes were using the CPU. And io_threads_active:0 says the commands run in a single thread.

Three instances, and why three

  • Native Redis 8.0.5, port 6392, without persistence: the latency does not pick up the cost of the disk. This is where I write.
  • Redis Stack 7.4.7 in a container, on 127.0.0.1:6398: it repeats what depends on the version.
  • The site's development instance, port 6379, 2,439 keys: here I only read, because it is real data.

The three entries are not equivalent environments: they answer different objections. The first isolates Redis from the disk, the second the version, the third brings real data. The third weighs the most, and its rule holds for you too: a measurement that alters the data it is measuring is not a measurement, it is damage.

The toolbox, and what each piece answers

  • --intrinsic-latency: the minimum the hardware grants, Redis excluded.
  • --latency and --latency-history: the cost of a client-server round trip.
  • redis-benchmark: how many requests per second it sustains.
  • SLOWLOG GET: which commands exceeded the execution threshold.
  • LATENCY LATEST and LATENCY DOCTOR: when the server stalled.
  • INFO commandstats and INFO latencystats: server-side cost per command.
  • INFO memory: memory in use, resident, fragmented.
  • MEMORY USAGE: how much a key weighs, for comparison.
  • --bigkeys and --memkeys: which has the most elements inside and which takes the most bytes.

The order goes from the outside in, from the hardware to the single key. The criterion that matters is that none of these commands answers more than one question, and the two most common mistakes come from mixing them up: reading --latency as the cost of a command, and adding up MEMORY USAGE as if it were a balance sheet. The sections that follow take both apart.

First measurement: the machine, without Redis

--intrinsic-latency does not measure Redis

It does not even open a connection: it runs a tight loop and times how much the operating system steals from it. The documentation warns about the detail that ruins most measurements: it must be run on the server.

$ redis-cli --intrinsic-latency 10
Max latency so far: 1 microseconds.
Max latency so far: 104 microseconds.
Max latency so far: 1218 microseconds.

211012952 total runs (avg latency: 0.0474 microseconds / 47.39 nanoseconds per run).
Worst run took 25701x longer than the average latency.

The average, 47 nanoseconds, interests nobody. The peak is 1,218 microseconds: a process stands still for 1.2 ms without Redis existing, and the worst run is 25,701 times the average.

An earlier run, at a lower load, had given a peak of 83 microseconds: fifteen times better, same machine, same command. If the requirement is "p99 under a millisecond", here you do not meet it even with Redis switched off.

PING as a probe, and who pays the price

--latency-history sends PING and closes a window every -i seconds, printing minimum, maximum, average and samples. Next to it, the same interval as seen by the server:

$ redis-cli -p 6392 config resetstat
OK
$ timeout 12 redis-cli -p 6392 --latency-history -i 5
0 8 0.19 484
 -- 5.00 seconds range
$ redis-cli -p 6392 info commandstats | grep cmdstat_ping
cmdstat_ping:calls=1157,usec=1503,usec_per_call=1.30,rejected_calls=0,failed_calls=0
$ redis-cli -p 6392 info latencystats | grep latency_percentiles_usec_ping
latency_percentiles_usec_ping:p50=1.003,p99=3.007,p99.9=8.031

The client says 190 microseconds on average; the server 1.30 microseconds per call, p50 at 1.003. More than 99 percent of what the client measures is not Redis: it is network, scheduler, library.

So --latency tells you whether the path between application and Redis is healthy, not whether Redis executes commands well. For the second question you need commandstats and latencystats.

redis-benchmark: the same server, four different numbers

$ redis-benchmark -p 6392 -q -n 50000 -t set,get
SET: 82918.74 requests per second, p50=0.311 msec
GET: 82918.74 requests per second, p50=0.311 msec
$ redis-benchmark -p 6392 -q -n 50000 -t set -c 1
SET: 38372.98 requests per second, p50=0.023 msec
$ redis-benchmark -p 6392 -q -n 50000 -t set -P 16
SET: 1282051.25 requests per second, p50=0.367 msec

# through Docker's port forwarding (127.0.0.1:6398 -> container:6379), Redis 7.4.7
$ redis-benchmark -p 6398 -q -n 50000 -t set -c 1
SET: 17205.78 requests per second, p50=0.055 msec
# the same server, measured from inside the container
$ docker exec art9-redis-stack redis-benchmark -q -n 50000 -t set -c 1
SET: 38314.18 requests per second, p50=0.023 msec

With one client: 38,373 requests per second, median 0.023 ms. With the default 50: 82,919, median 0.311 ms, thirteen times as much. With the pipeline of 16: 1.28 million. Three questions, not three truths.

The last two lines nearly made me write that line 7 is slower. The same server gives 17,206 requests per second from outside the container and 38,314 from inside: 55.1 percent was going into the port forwarding.

Second measurement: which commands are slow, and for whom

The thresholds that decide what is slow

$ redis-cli -p 6392 config get slowlog-log-slower-than
1) "slowlog-log-slower-than"
2) "10000"
$ redis-cli -p 6392 config get slowlog-max-len
1) "slowlog-max-len"
2) "128"
$ redis-cli -p 6392 config get latency-monitor-threshold
1) "latency-monitor-threshold"
2) "0"

Identical on 8.0.5, on 7.4.7 and on the site's instance: only what exceeds 10,000 microseconds ends up in the SLOWLOG, the queue keeps 128 entries, the monitor is off.

Four O(N) commands on realistic data

$ redis-cli -p 6392 config set slowlog-log-slower-than 1000
OK
$ redis-cli -p 6392 slowlog reset
OK
$ redis-cli -p 6392 llen coda ; redis-cli -p 6392 hlen sessione
200000
50000
$ redis-cli -p 6392 lrange coda 0 -1 > /dev/null
$ redis-cli -p 6392 hgetall sessione > /dev/null
$ redis-cli -p 6392 keys 'key:0*' > /dev/null
$ redis-cli -p 6392 sort coda alpha limit 0 10 > /dev/null
$ redis-cli -p 6392 --no-raw slowlog get 4
1) 1) (integer) 4
   3) (integer) 255950
   4) 1) "sort"  2) "coda"  3) "alpha"  4) "limit"  5) "0"  6) "10"
2) 1) (integer) 3
   3) (integer) 22837
   4) 1) "keys"  2) "key:0*"
3) 1) (integer) 2
   3) (integer) 14278
   4) 1) "hgetall"  2) "sessione"
4) 1) (integer) 1
   3) (integer) 8331
   4) 1) "lrange"  2) "coda"  3) "0"  4) "-1"

The ranking is surprising. LRANGE coda 0 -1, 200,000 elements, costs 8.3 ms; HGETALL on 50,000 fields 14.3 ms; KEYS key:0* 22.8 ms. And SORT with LIMIT 0 10, which returns ten, costs 256 ms.

The documented complexity of SORT is O(N+M·log M), with N equal to the elements of the collection: the LIMIT reduces M, not N, so Redis sorts everything and then cuts. What you need is a sorted set maintained at write time: the first article in the series measures it.

The victim does not show up in the SLOWLOG

One client repeats SORT for about five seconds; at the same time a second client measures PING:

$ ( for i in $(seq 1 25); do redis-cli -p 6392 sort coda alpha limit 0 10 > /dev/null; done ) &
$ sleep 0.3
$ timeout 12 redis-cli -p 6392 --latency-history -i 5
196 248 238.24 21
 -- 5.20 seconds range
0 243 1.90 414
 -- 5.01 seconds range
$ redis-cli -p 6392 slowlog len
29
$ redis-cli -p 6392 slowlog get 128 | grep -E "^(ping|sort|keys|hgetall|lrange)$" | sort | uniq -c
      1 hgetall
      1 keys
      1 lrange
     26 sort
$ redis-cli -p 6392 info commandstats | grep cmdstat_ping
cmdstat_ping:calls=4977,usec=6392,usec_per_call=1.28,rejected_calls=0,failed_calls=0

In the first window the PING costs 238.24 ms on average, minimum 196: not a peak, the regime. Once the loop is over it goes back to 1.90 ms. But in the twenty-nine SLOWLOG entries there are zero PINGs.

The 4,977 PINGs were executed in 1.28 microseconds each. The documentation says why: the recorded time is only "the time needed to actually execute the command, the only phase in which the thread is blocked". Whoever waited 238 ms was in a queue, not slow.

One sample is not a measurement

$ redis-cli -p 6392 --no-raw slowlog get 12 | grep -E "^\s+3\) \(integer\)"
    3) (integer) 296149
    3) (integer) 302254
    3) (integer) 529203
    3) (integer) 613418
    3) (integer) 611342
    3) (integer) 620495
    3) (integer) 612992
    3) (integer) 612236
    3) (integer) 403879
    3) (integer) 296428
    3) (integer) 526522
    3) (integer) 418588
$ redis-cli -p 6392 info latencystats | grep latency_percentiles_usec_sort
latency_percentiles_usec_sort:p50=528482.303,p99=620756.991,p99.9=620756.991

Twelve runs of the same command on the same data: from 296 to 620 milliseconds, average 487 ms. An earlier group of 26, at a lower load, gave 246 ms. The honest number is "between 0.3 and 0.6 seconds on this machine".

On a SORT timed from outside I read 0.63 s, the SLOWLOG 627,420 microseconds, LATENCY LATEST 627 ms: three instruments, one fact. If they do not agree, "I am measuring two different things".

The queue that forgets, and the monitor that is off

$ redis-cli -p 6392 config set slowlog-max-len 3
OK
$ redis-cli -p 6392 slowlog reset
OK
$ for i in 1 2 3 4 5; do redis-cli -p 6392 hgetall sessione > /dev/null; done
$ redis-cli -p 6392 slowlog len
3

$ redis-cli -p 6392 latency latest        # with threshold 0: no rows
$ redis-cli -p 6392 config set latency-monitor-threshold 100
OK
$ for i in 1 2 3; do redis-cli -p 6392 sort coda alpha limit 0 10 > /dev/null; done
$ redis-cli -p 6392 latency latest
command
1794121372
607
627
$ redis-cli -p 6392 latency doctor
1. command: 2 latency spikes (average 617ms, mean deviation 10ms, period 0.50 sec). Worst all time event 627ms.

Five slow commands, three entries kept: with the default of 128 and tens per minute, the SLOWLOG covers a few minutes and has to be collected, not consulted the next day.

The monitor answers another question: when the server stalled, even for causes that are not commands. Among the documented events are fork and aof-fsync-always, invisible to the SLOWLOG.

What the site's instance recorded

$ redis-cli -p 6379 slowlog len
3
$ redis-cli -p 6379 --no-raw slowlog get 3
1) 3) (integer) 23310    4) 1) "SCAN" 2) "0" 3) "COUNT" 4) "1000"
2) 3) (integer) 15598    4) 1) "SCAN" 2) "0" 3) "MATCH" 4) "article:*" 5) "COUNT" 6) "1000"
3) 3) (integer) 53605    4) 1) "SCAN" 2) "0" 3) "COUNT" 4) "1000"
$ redis-cli -p 6379 info commandstats | sed 's/^cmdstat_//' | head -4
hget:calls=4852,usec=72620,usec_per_call=14.97,rejected_calls=0,failed_calls=0
ttl:calls=2444,usec=987,usec_per_call=0.40,rejected_calls=0,failed_calls=0
memory|usage:calls=362,usec=59713,usec_per_call=164.95,rejected_calls=0,failed_calls=0
scan:calls=51,usec=114154,usec_per_call=2238.31,rejected_calls=0,failed_calls=0

Three entries, all SCAN, from 15.6 to 53.6 ms. And they are not application traffic: they are diagnostic commands, mine and those of whoever measured before.

On an instance that serves two operations per second, the only things slow enough for the SLOWLOG are the measuring tools. MEMORY USAGE costs 164.95 microseconds per key: three minutes for a million.

Third measurement: what a key costs

The difference in used_memory is the only one that does not cheat

$ redis-cli -p 6392 flushall
OK
$ redis-cli -p 6392 info memory | grep -E "^(used_memory|mem_fragmentation_ratio):"
used_memory:1083720
mem_fragmentation_ratio:2.65
$ python3 -I -c "v='v'*100; print('\n'.join('SET key:%06d %s'%(i,v) for i in range(1,100001)))" | redis-cli -p 6392 --pipe
All data transferred. Waiting for the last reply...
errors: 0, replies: 100000
$ redis-cli -p 6392 info memory | grep -E "^(used_memory|used_memory_human|used_memory_rss|mem_fragmentation_ratio):"
used_memory:21338024
used_memory_human:20.35M
used_memory_rss:30908416
mem_fragmentation_ratio:1.45
$ redis-cli -p 6392 memory usage key:000001 ; redis-cli -p 6392 strlen key:000001
184
100

The difference is 20,254,304 bytes for 100,000 keys: 202.54 bytes per key, with a value of 100 bytes and a key of 10 characters. Double what you wrote. On 7.4.7: 194.98 bytes.

Look also at the mem_fragmentation_ratio: 2.65 when empty, 1.45 when full. On a tiny dataset that ratio means nothing.

MEMORY USAGE compares, it does not add up

$ redis-cli -p 6392 info memory | grep -E "^used_memory:"
used_memory:28001808
# sum reported by --memkeys: 18400000 (strings) + 2941138 (list) + 2924400 (hash)
$ python3 -I -c "print(28001808 - (18400000+2941138+2924400))"
3736270
$ redis-cli -p 6392 memory usage coda ; redis-cli -p 6392 memory usage coda samples 0
2941138
3311440
$ redis-cli -p 6392 memory usage sessione ; redis-cli -p 6392 memory usage sessione samples 0
2924400
2924400

On the single key 18.54 bytes of the real 202.54 are missing, 9.2 percent: the share of the key table, attributed to nobody. Adding up all the keys, 3,736,270 bytes are missing, 13.3 percent of used_memory.

And on aggregate types it samples five nested values, as its page declares. On the hash, with equal fields, the two numbers coincide; on the list, where the length varies, the default underestimates by 11.2 percent compared with SAMPLES 0.

This site's estimator is off by 21 percent

The site has a tool that estimates Redis latency, throughput and memory. The formula declared in its comment is "keys × (key + value + 50 bytes) / 1,048,576". With my parameters:

$ python3 -I -c "
keys=100000; estimate=keys*(10+100+50); actual=20254304
print('estimate  = %d bytes = %.2f MB' % (estimate, estimate/1048576))
print('actual    = %d bytes = %.2f MB' % (actual, actual/1048576))
print('underestimate = %.2f%%' % (100*(1-estimate/actual)))"
estimate  = 16000000 bytes = 15.26 MB
actual    = 20254304 bytes = 19.32 MB
underestimate = 21.00%

The gap is 21.00 percent, in the dangerous direction: the estimate promises you will fit, the measurement says you will not. Reading the code instead of the comment, there is worse, and I state it because the flaw is mine.

At line 143 the calculation multiplies the bytes per key by input.opsPerSec, and in RedisInput a "number of keys" field does not exist: the model uses operations per second as if they were keys. It is a heuristic estimate, declared as such in its header, and a capacity decision is closed by measurement.

--bigkeys and --memkeys do not measure the same thing

$ redis-cli -p 6392 --bigkeys | grep -E '^Biggest|strings with'
Biggest   list found "coda" has 200000 items
Biggest   hash found "sessione" has 50000 fields
Biggest string found "key:033846" has 100 bytes
100000 strings with 10000000 bytes (100.00% of keys, avg size 100.00)
$ redis-cli -p 6392 --memkeys | grep -E '^Biggest|strings with'
Biggest   list found "coda" has 2941138 bytes
Biggest   hash found "sessione" has 2924400 bytes
Biggest string found "key:033846" has 184 bytes
100000 strings with 18400000 bytes (100.00% of keys, avg size 184.00)

--bigkeys counts elements: for it coda, with 200,000 entries, is four times sessione. --memkeys counts bytes: 2,941,138 against 2,924,400, equal. On the string, 100 bytes against 184.

Both use SCAN, so they do not block the server like KEYS, but to size you need --memkeys and to find the structure that degrades an O(N) command you need --bigkeys.

This site's keys: 2,920 bytes for 1,536 of vector

$ redis-cli -p 6379 dbsize
2439
$ redis-cli -p 6379 object encoding article:1416:en ; redis-cli -p 6379 hstrlen article:1416:en embedding
hashtable
1536
$ redis-cli -p 6379 memory usage article:1416:en
2880
$ redis-cli -p 6379 --scan --pattern 'article:*' --count 1000 | head -200 \
    | awk '{print "MEMORY USAGE "$0}' | redis-cli -p 6379 | python3 -I -c "
import sys; v=sorted(int(x) for x in sys.stdin)
print('n =',len(v),'min =',v[0],'max =',v[-1],'mean = %.2f'%(sum(v)/len(v)),'median =',v[len(v)//2])"
n = 200 min = 2764 max = 3225 mean = 2920.09 median = 2918

Each article is a hash of six fields, one of which is a vector of 1,536 bytes (384 dimensions at 32 bits). MEMORY USAGE declares 2,880 bytes, 1.875 times the vector; over 200 keys the average is 2,920.09.

The projection onto the 2,426 keys of the index gives 6.76 MB, and used_memory says 7,465,736 bytes: consistent. This is how you answer "how much does it cost to index twice the articles".

The trade-off, and where this method does not reach

I could have measured better: a dedicated machine, ten repetitions per point, a client on another host, real values instead of a hundred identical bytes. I chose the measurement that takes half an hour: an imperfect but declared measurement changes a decision, a perfect one you do not make changes nothing.

The price is that the absolute value is not transferable. The gain is that the ratio between two numbers taken in the same minute remains valid: 1 microsecond of execution against 190 of latency seen by the client, 202.54 real bytes against 160 estimated. It is the step between an intermediate and a senior: no longer commands, but the environment next to every number.

The numbers in this article that you CANNOT carry to production

  • The throughputs (82,919, 38,373, 1,282,051, 17,206 per second): laptop, client and server on the same machine.
  • The absolute latencies (0.023 ms, 0.311 ms, 0.19 ms): without a real network.
  • The 256 and 627 ms of SORT: what transfers is the order of magnitude relative to a GET, not the value.
  • The 202.54 bytes per key: they hold for those keys and those values. The method does, the number does not.
  • Production is not verified: without access to the VPS I am talking about the development instance, and production might not be 8.0.5.

The criterion that holds the five entries together is one: what is absolute falls, what is a ratio stays. The first four you cancel by redoing the tests on your machine. The last one you cannot: until somebody reads the production configuration, no production number exists.

Frequently asked questions

Why is the SLOWLOG empty if users see timeouts?

Because it records execution time, not waiting in the queue. In my test 4,977 PINGs cost 238 ms on average as seen by the client without producing any entry: the server executed them in 1.28 microseconds.

Is MEMORY USAGE enough to size a Redis instance?

No. On the single key it declared 184 bytes against 202.54 real, and the sum over the whole instance left out 13.3 percent of used_memory. On my list it also underestimated by 11.2 percent compared with SAMPLES 0.

What is the difference between --latency and --intrinsic-latency?

--latency measures the full round trip of a PING: network, client and server. --intrinsic-latency connects to nothing, times how much the operating system steals from a tight loop and must be run on the server: here it gave a peak of 1.2 ms without Redis.

What you can do now

You have a three-step protocol. You know that the SLOWLOG reports the culprit and not the victim, and that to see the victim you need commandstats, latencystats and the latency monitor. And that the memory of a key is measured with a difference, not with a formula.

The rest of the series applies these tools to concrete decisions. Persistence, RDB and AOF explains why the appendfsync I removed changes all the latency numbers; maxmemory and eviction is the decision you take with the bytes per key in hand; Lua shows a deliberate blocking command; Pub/Sub or Streams is where "how slow is it" becomes "how many messages are left behind". For the vector index there is the RediSearch setup, which does not measure the cost per key. Before this article comes fail-open and fail-closed; after it, HNSW in Redis, with recall and latency measured.

If you are sizing a Redis instance, or hunting a latency that cannot be explained, and you want to do it with someone who shows you the commands instead of handing you a number, you can book a call.

Sources

  • Redis, Diagnosing latency issues - the single-threaded nature ("a single process serves all the client requests"), the list of slow commands with SORT among the examples, and the warning that --intrinsic-latency must be run "in the server not in the client", consulted on 2026-11-08.
  • Redis, SLOWLOG GET - the recorded time "does not include I/O operations ... but just the time needed to actually execute the command", the role of slowlog-log-slower-than and slowlog-max-len, and the structure of the entry: seven values in the current page, of which the seventh "Added in Redis 8.10", while my 8.0.5 returns six, consulted on 2026-11-08.
  • Redis, Redis latency monitoring - the monitor "is turned off by default (threshold set to 0)", the list of events (command, fast-command, fork, aof-fsync-always) and the advice to use the maximum acceptable latency as the threshold, consulted on 2026-11-08.
  • Redis, MEMORY USAGE - the sampling on nested types, "By default, this option is set to 5" and the use of SAMPLES 0 to look at them all, consulted on 2026-11-08.
  • Redis, Redis benchmark - "the golden rule of a useful benchmark is to only compare apples and apples" and the fact that iterating over synchronous commands measures the network and the client, not Redis, consulted on 2026-11-08.
  • Redis, SORT - the O(N+M·log(M)) complexity "where N is the number of elements in the list or set to sort, and M the number of returned elements", which explains the cost with LIMIT, consulted on 2026-11-08.
  • Redis, redis-cli - the --bigkeys and --memkeys modes, both based on SCAN, and the different meaning of their totals, consulted on 2026-11-08.
  • Redis, KEYS - "Use extreme care when using this command in production environments ... Don't use KEYS in your regular application code", consulted on 2026-11-08.
  • Own measurements: all the commands in the blocks above, run on 8 November 2026 on native Redis 8.0.5 (port 6392, --save "", --appendonly no), on Redis Stack 7.4.7 in a container (redis/redis-stack-server:latest) and read-only on the site's development instance (port 6379, 2,439 keys).
  • This site's code: frontend/src/app/pages/dev-tools/redis-latency-tester/redis-latency.service.ts, comment at lines 20-23 and calculation at lines 143-147; the RedisInput interface in redis-latency.models.ts, lines 48-58, read on 8 November 2026.

The first eight entries are pages that change, and that is why they carry the date: if you cannot find a sentence, it is the page that has moved, not the citation. The last two are better to verify, because they are commands and lines of code that you rerun yourself. Where the two families contradict each other, the command wins.

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects