Why Is My Redis Slow?

Necco Ceresani
Necco Ceresani··37 min read

Fifteen years building engineering platforms, currently focused on advanced AI infrastructure at yeet. I love turning the deeply complex topics into something everyone can understand. I relate deeply with the core yeet philosophy that you can just build things.

Quick answer. Check SLOWLOG GET 10 and INFO commandstats first, because they cost nothing and they settle every case where one command is individually slow. When they come back clean and Redis is still slow, stop trusting the log: slowlog only records commands over a threshold someone else chose, excludes the reply write from its timing by documented design, and cannot tell you what it discarded, so an empty log and a healthy server look identical. What is left is demand, and no server-side tool can attribute it, because Redis counts commands by type and never by key. Read the traffic instead, which exists whether or not anything decided to log it: yeet run gh:yeet-src/redissnoop ranks the key patterns your app is actually sending, live, without Redis participating.

I spend my days writing kernel probes that read protocol traffic off Linux boxes, which means I get shown a lot of Redis instances that are slow for reasons their owners cannot name. The pattern is consistent enough to predict: slowlog is empty, the dashboard shows commands per second climbing, and the next step is a guess. What is missing is never the server's opinion of itself, and the reason the investigation stalls is that an empty log is being read as an answer when it is the absence of one.

My dashboards and logs say Redis is fine. Why does it obviously not feel fine?

Because a log is a record something chose to write about itself, and traffic is what actually happened. Those are different kinds of evidence, and almost every stalled Redis investigation is a case of reaching for the first when the question needed the second. A log is downstream of four decisions that someone else already made for you: a threshold, a sampling rate, a set of fields worth keeping, and a boundary that stops at the edge of the process doing the writing. None of those decisions knew anything about your workload.

Redis's slowlog is a well-built log with all four properties, which makes it the cleanest example of the problem. Its threshold is slowlog-log-slower-than, and the default is 10,000 microseconds: a number chosen by a maintainer years ago, with no knowledge of your traffic, that now silently defines what counts as a problem on your box. Ten thousand commands at 90 microseconds are, by that definition, not a problem, and they are also the reason your instance is at capacity. The log is not lying to you. It is answering a narrower question than the one you asked.

The property that makes this expensive is that a log's blind spot is invisible from inside the log. An empty slowlog and a healthy Redis produce exactly the same artifact: nothing. There is no field that says "47,000 commands fell below the threshold during this window", no marker for what was discarded, and no way to distinguish "I looked and found nothing" from "I was never able to see this class of thing." So an empty log reads as reassurance when it should read as an unanswered question, and that misreading is where the hours go.

Traffic inverts every one of those properties, for better and for worse:

A log (slowlog, app log, APM trace)The traffic (RESP on the wire)
Exists becauseSomething decided it was worth recordingIt happened
ThresholdSomeone else's default, applied silentlyNone, every command is present
CompletenessThresholded and sampledEvery command that crossed the socket
Knows its own blind spotNo, absence looks like healthNo, but the blind spots are structural and enumerable
AttributionWhatever the writer chose to includeThe sending process, from the kernel's view
Carries interpretationYes: severity, SLOs, error budgetsNone, you supply the question
Survives a restartOnly if written somewhere durableNo, it is a live stream

Read that table in both directions, because traffic is not the better evidence, it is the other evidence. Traffic has no notion of your SLO, cannot tell you a pattern is anomalous because it has nothing to compare against, keeps no history past the process you are running, and will happily show you twenty thousand perfectly healthy commands. A log tells you what a system thought was worth mentioning; traffic tells you what the system did. When those two disagree, the log is rarely wrong. It is scoped, and the work is finding what fell outside the scope.

That is not a Redis observation, which is why it is worth making before any of the Redis specifics. Any protocol on a Linux box can be read the same way, because the traffic exists whether or not something decided to log it: httpwatch does it for HTTP, grpcsnoop for gRPC and h2c, and yeet is the runtime all of them are written against, in a few hundred lines of JavaScript each. Redis is simply the clearest place to demonstrate the gap, because its log excludes the reply write by documented design and its counters aggregate by command type and never by key.

Applied to Redis specifically, that split is what tells you which tool to reach for. Redis's own latency documentation is the best inventory of the server-side causes, and if your cause is on that list, a log will find it and you can stop reading: slow O(N) commands blocking the single thread, fork() for RDB or AOF rewrite, transparent huge pages amplifying copy-on-write after that fork, swap moving Redis pages to disk, AOF fsync behavior, many keys expiring in the same second, and the intrinsic latency of the machine underneath.

ShapeExamplesWhat finds it
The server is blockedOne KEYS *, a fork() stall, THP after fork, swap, an expiration waveSLOWLOG GET, INFO, redis-cli --intrinsic-latency, vmstat
The server is saturated by demandA hot key pattern, a chatty loop, an N+1 read, a missing TTL, no pipeliningReading the traffic your app sends
The server is fine and the wire is slowNetwork round trip, no pipelining, connect churnPer-command round trip measured client-side

Row one is well served by what already ships with Redis. Rows two and three are where investigations stall, and the reason is the same in both: every server-side tool answers in units of command type, and those questions are about keys, callers and round trips. INFO commandstats will tell you cmdstat_get:calls=9182736,usec=..., which is nine million GETs and no hint of what they were fetching. Nobody forgot to build that view. A counter keyed by command cannot express it.

My Redis SLOWLOG is empty but Redis is still slow. What am I missing?

Run it first regardless, because it costs nothing and it ends the investigation whenever one command is individually slow:

redis-cli SLOWLOG GET 10                        # ten most recent slow entries
redis-cli CONFIG GET slowlog-log-slower-than    # threshold, in microseconds

Three documented reasons the log can be empty while Redis is struggling, in the order they bite:

  1. Volume never appears in it. This is the threshold problem from the opening, in its concrete form: a hundred thousand GET session:* calls at 90 microseconds each sit far below the default and are collectively the reason you are at capacity.
  2. The reply write is excluded from the timing. Redis states the execution time "does not include I/O operations like talking with the client, sending the reply and so forth, but just the time needed to actually execute the command", so an HGETALL against a fifty-thousand-field hash executes fast, logs as fast, then spends real wall-clock time serializing a huge reply.
  3. It does not survive a restart. The slowlog is a transient in-memory ring, so a failover or a restart empties it and the evidence goes with it.

An entry, when you do get one, holds seven values: a progressive id, a timestamp, the duration in microseconds, the argument array, the client IP and port, the client name if set, and since Redis 8.8 the pre-truncation argument count. The argument array truncates at slowlog-max-argc, 32 by default. Useful, and entirely about individual commands.

So the next place to look is the traffic, because the two things slowlog structurally cannot report are exactly the two things that describe demand: how many commands of what shape, and how long the client actually waited. The second number is the one almost nobody has. Redis's docs note a 1 Gbit/s network typically adds around 200 microseconds of latency and a Unix domain socket about 30, and that "a client performing many roundtrips to the server will have to pay for these network and system related latencies". Pairing the request with its reply at the kernel gives you the round trip including that cost, which is the number your application experienced rather than the number Redis spent executing.

What is actually hammering my Redis, and which key pattern is responsible?

Rank your traffic by key pattern, because Redis counts by command type and the answer you need is keyed by data. That means collapsing the variable segment before counting, so user:1839 and user:204 both become user:* and stop being two facts about nothing. No server-side tool does this, which is why the question stalls investigations that have already checked everything Redis will tell them:

curl -fsSL https://yeet.cx | sh    # install yeet, once
yeet run gh:yeet-src/redissnoop    # clone, build, run

With illustrative numbers, on a session-heavy cache, the answer has this shape:

 redissnoop    1.4k cmd/s    3.1k encrypted  +  18.7k plaintext    214 footguns
  key pattern              share                  ops   r / w        keys
▸ user:*                   31.2% ██████████       6812  64r/36w      487
▸ session:*                22.8% ███████          4974  41r/59w       50
▾ cart:*                   11.4% ████             2488  22r/78w      312
    HGETALL 1204 · HSET 806 · HDEL 478
    cart:8831 214 · cart:1207 118 · cart:4402 96
▸ pageviews                 9.7% ███              2117   0r/100w       1
▸ ratelimit:*               6.1% ██               1331   9r/91w       894

The keys column is what makes this a diagnosis and not a leaderboard. session:* spreads 22.8% of traffic across 50 keys, a small hot set behaving exactly as a session tier should. cart:* spreads 11.4% across 312 keys and then, expanded, shows cart:8831 absorbing 214 of its 2,488 operations alone: one key absorbing 8.6% of the pattern's traffic, hiding inside an unremarkable row. pageviews is one key taking 9.7% of all traffic at 100% writes, which is a counter that wants pipelining. ratelimit:* spans 894 keys at 91% writes, which is the shape of a pattern that needs a TTL audit. None of those five conclusions is available from INFO commandstats, and none of them requires asking Redis anything.

The heuristic that makes it work is also where it breaks, and you should know the boundary before you trust a grouping. A segment collapses to * when it is all digits, an 8-or-more character hex string, or a 6-or-more character token containing a digit, and keys split on :, / and . only. A key scheme with no separator does not split at all and lands as a single pattern, and an unusual scheme can group in ways you did not intend. That shows up as a suspiciously dominant row, never as an error, so read a surprising top row skeptically.

Is the latency in Redis or in the network between us?

Ask what the server spent against what the client waited, because those are different numbers and Redis only publishes one of them. INFO commandstats gives usec_per_call, which is CPU time inside the command, and slowlog's duration explicitly excludes the reply write. Both describe the server. Neither contains the network, the client's own scheduling, or the time a reply spent being copied out. Redis's latency documentation is direct about the size of what is missing: a 1 Gbit/s network typically adds around 200 microseconds and a Unix domain socket around 30, and "a client performing many roundtrips to the server will have to pay for these network and system related latencies."

That gap is why an instance can look healthy from the inside while your p99 is bad. If Redis reports usec_per_call=45 for GET and your application measures 4 milliseconds against the same commands, the 3.95ms difference is not in Redis and no server-side tool will show it to you. It is round trips: connection churn, a client that issues sequentially where it could pipeline, an N+1 read pattern, or simply more hops than the work requires.

Measuring it means timing the request against its reply from outside the server, which is what pairing tcp_sendmsg with tcp_cleanup_rbuf does. The timing starts when the request leaves the client and stops on the first reply carrying payload, so it includes the network for a remote Redis and reflects what the application actually waited for. Two honest caveats attach to that number. It is a kernel-side round trip, so it excludes time your own process spent before calling write() or after the data landed. And it is plaintext-only: the TLS path fires on the request and emits immediately with lat_us set to 0, because there is no reply-side pairing for encrypted connections in this build.

Two Redis-side commands are worth running alongside it, because they bound the problem from the other end. redis-cli --latency samples round-trip PING time from wherever you run it, which separates "the link is slow" from "this command is slow". And redis-cli --intrinsic-latency 100, which must run on the server, measures the machine's own scheduling floor rather than Redis at all; Redis's docs show that floor at 115 microseconds on an entry-level physical server and 9.7 milliseconds on a busy Linode instance, and note they have measured up to 40 milliseconds on systems "otherwise apparently running normally". If your intrinsic latency is 9ms, no amount of Redis tuning gets you below it and the answer is the host, not the workload.

Which service, container or pod is sending all the Redis traffic?

Read the sending process, not the connection. Redis can tell you a client IP and port, through CLIENT LIST or the fifth field of a slowlog entry, and on a Kubernetes cluster that is frequently a pod IP that has already been recycled by the time you look. A host-wide kernel probe sits on the other side of the problem: it sees the comm and pid of whatever called tcp_sendmsg, as the host sees them, attached to the command that was sent.

Because the kprobes are host-wide, not per-connection, container topology mostly stops mattering. A client in one container talking to Redis in another is captured with nothing installed in either, since loopback and veth traffic goes through tcp_sendmsg like anything else. On a Kubernetes node you get that node's traffic with the sending process named, which is usually enough to identify the workload even when the IP is meaningless.

Two boundaries here, and both produce silence instead of an error, so you want them in advance. A client connecting over a Unix domain socket produces nothing at all, because the TCP path is the only path, and this is the single most common cause of an empty table; force TCP with redis-cli -h 127.0.0.1 when you are testing. And this is one instance per host with no aggregation layer, so a node tells you about that node. Fleet-wide questions stay with your metrics stack, and that is a real limit, not a roadmap item.

Every Redis command is fast on its own. Why is my app still slow?

Because the count is the problem, not the commands, and this is the failure mode that hides best from every server-side tool. A page that issues 300 sequential GETs at 45 microseconds each spends 13.5 milliseconds inside Redis and far more than that in round trips, and every one of those commands is far too fast for slowlog, perfectly ordinary in INFO commandstats, and invisible as a problem anywhere except in the aggregate. Redis's own guidance is explicit that this is the client's cost to avoid: "an efficient client will therefore try to limit the number of roundtrips by pipelining several commands together", along with using aggregate commands like MGET and MSET, and keeping connections long-lived instead of connecting and disconnecting per request.

What makes this diagnosable from the traffic and not from the server is the ratio, not the rate. A pattern taking a large share of commands with a very small distinct-key count is a read loop that wants MGET. A pattern at nearly 100% writes against one key is a counter that wants pipelining, which is what pageviews at 9.7% and 0r/100w in the table above actually is. A high-cardinality pattern with a low share per key and a heavy write skew is usually a TTL question, not a throughput one. Those are three different fixes, and the thing that distinguishes them is the shape of the traffic rather than any individual command's duration.

The reset key matters more than it sounds for this specific question. Shares are cumulative since the process started or since the last reset, so pressing r, triggering one page load or one job, and reading the result gives you that operation's Redis footprint in isolation instead of a number diluted by an hour of background traffic. That turns "the app is chatty" into "this endpoint sends 312 commands across 4 patterns", which is a sentence you can take to a code review.

Is anything in my code running Redis KEYS against production?

Redis is unambiguous about the command and still it keeps reaching production. The docs say "use extreme care when using this command in production environments", "it may ruin performance when it is executed against large databases", and "don't use KEYS in your regular application code". The same page explains why it survives review anyway: "Redis running on an entry level laptop can scan a 1 million key database in 40 milliseconds." Forty milliseconds against a dev keyspace reads as fine. The same line of code against production blocks the single thread for seconds, and Redis's latency documentation calls KEYS in production "a VERY common source of latency".

Grep does not settle whether your code obeys the rule, because the call can live in a dependency, a hand-run migration script, a Lua script, or behind a wrapper named something innocuous. What settles it is watching for the verb on the wire and reading off which process issued it, which turns a policy question into an observation: worker-3 did it four seconds ago. A live tail of only the flagged commands stays quiet under clean traffic, which for a linter is the correct output.

The flagging is by verb, not by data, and that cuts both ways. KEYS, FLUSHALL, FLUSHDB, SMEMBERS, HGETALL, SORT and SAVE are flagged on sight, so an HGETALL against a three-field hash flags identically to one against fifty thousand fields; the kernel side never sees the reply size. Treat a flag as a candidate, not a verdict, and pair it with redis-cli --bigkeys when you need to know whether the target is large. When you find one, SCAN is the fix: O(1) per call against KEYS's single blocking O(N), at the cost of possible duplicate elements and a COUNT that is "just a hint" and not a guarantee.

Will watching the traffic make my slow Redis slower, or need a restart or sidecar?

No, and the mechanism is checkable, not a slogan. The probes attach to tcp_sendmsg and tcp_cleanup_rbuf, functions the kernel was already calling on every Redis request and reply. Each one copies a fixed 64 bytes, parses the verb and first argument, and returns. Nothing is buffered on the request path, no hop is added, and Redis is never contacted, so the server's share of the cost is zero rather than small. That is the structural difference from MONITOR, where the entire cost is the Redis process formatting and relaying every command it executes to a subscribed client.

Redis publishes what that costs, and the numbers are the reason this distinction matters during an incident. Its own benchmark, which the page labels "totally unscientific", run as redis-benchmark -c 10 -n 100000:

OperationWithout MONITORWith one MONITOR client
GET104,275.29 req/s45,330.91 req/s
SET95,419.85 req/s41,823.50 req/s
INCR93,283.58 req/s41,771.09 req/s

The docs summarize it as "running a single MONITOR client can reduce the throughput by more than 50%" and add that more clients reduce it further. Nobody attaches MONITOR to a healthy instance out of curiosity; you attach it because Redis is slow, which means the resource you are short of is exactly the one you are about to spend.

Filtering in the kernel pushes the cost down further, and in the direction people find surprising. The slow-query floor is a patchable global inside the BPF program, so commands that completed faster than the floor are dropped before they reach the ring buffer. Raising the bar makes the tool do less work, not more, and cost scales with matched commands, not with total traffic. Userspace then batches: events accumulate into plain maps and a timer publishes a snapshot every 500ms, so a busy ring buffer produces one repaint per frame instead of one per command.

Nothing restarts and nothing is deployed either. The probes attach to kernel functions that already exist in a running system, so there is no library to preload, no LD_PRELOAD, no agent per pod, no proxy in the request path and no configuration change on either the client or the Redis server. Detaching leaves nothing behind. That property is worth more during an incident than it sounds on a feature list: the alternatives that give you comparable detail all require changing the thing you are investigating, since instrumentation means a code change and a deploy, an OpenTelemetry Java agent attaches at JVM startup so adding it means a restart, and a proxy is a path change. Restarting a misbehaving process destroys the state that would have explained it, which is why "can I attach to what is already running" is usually the question that decides the tool.

Can I see the traffic if my Redis connection is encrypted?

Yes, by reading the plaintext before it becomes ciphertext. A uprobe on SSL_write in libssl.so fires when the application hands OpenSSL a buffer, and that buffer is plaintext RESP. This is the only way to make an encrypted connection legible without terminating it somewhere, which is what a proxy does and what this specifically avoids.

The limit is the most important caveat in this post, because it fails silently in both directions at once. A client that does not route its TLS through a dynamically linked OpenSSL has no symbol to hook, and the TCP path sees only ciphertext, so the command is invisible on both paths and nothing reports that anything was missed. Go's standard library crypto/tls is the common case, since it is pure Go and links no libssl at all; the same applies to Rust with rustls and anything statically linking its TLS. Measured on Debian 13 with kernel 6.12: a Go client and redis-cli both sent commands to the same TLS Redis in one capture window, and only redis-cli's were captured, with the Go client's commands executing normally on the server.

This is the log problem from the opening wearing different clothes, and it is worth naming as the same failure: an empty encrypted counter means "cannot see it", not "nothing happened", and nothing in the output distinguishes the two. The difference here is that the blind spots are structural and countable, listed below, where a threshold's discards are not. There is also a one-command way to tell this apart from the Unix-socket cause. Run redis-cli -h <host> --tls against the same server: if its commands appear and your application's do not, the difference is how your client links TLS.

When should I use Redis MONITOR, SLOWLOG or Datadog instead?

Use SLOWLOG for individually slow commands, Redis MONITOR when you need argument values, and Datadog or Prometheus for retention and the fleet. Each answers a question a kernel-side traffic probe cannot, and a fair reading of where they win is what makes the rest of this post worth anything:

ToolReadsCosts the serverArgument valuesGroups by key patternRetention
redissnoopKernel and TLS library, client hostNothingVerb and first argumentYes, liveNone
SLOWLOGInside RedisNegligibleYes, truncated at 32NoTransient ring
MONITORInside RedisOver 50% of throughputYes, allNoNone
INFO commandstatsInside RedisNegligibleNoNo, by command typeSince reset
redis-cli --hotkeysKeyspace, via SCANLight, throttleableNot applicableConcrete keys onlyNone
Datadog redisdbINFO, commandstats, slowlogLight, polledNoOnly keys named in advanceYes

Which one to run, in the order a diagnosis usually needs them:

  • SLOWLOG first, always, because it is already recording and it ends the investigation whenever one command is individually slow.
  • INFO commandstats for the per-command-type shape, plus latencystats for p50, p99 and p999 per command type.
  • redissnoop when the counts are ordinary and the question is which keys and which caller, which is the case those two cannot express.
  • MONITOR when you need the values rather than the verbs, on an instance with headroom, briefly. It is the only tool here that shows what was actually written into session:9f2a.
  • redis-cli --bigkeys and --hotkeys for the state of the keyspace rather than the traffic hitting it, remembering that --hotkeys "only works when maxmemory-policy is *lfu".
  • Datadog or your metrics stack for retention, alerting and the fleet view, none of which any per-host tool has. Its per-key metrics need the keys option with patterns named in advance, which is the structural difference from reading traffic.

Can I write my own Redis traffic probe instead of using a fixed tool?

Write it. This is the part that matters more than any single script, and it is the reason to look at yeet rather than just at redissnoop. The tool is a few hundred lines of JavaScript against a runtime that exposes eBPF, kernel probes and a reactive terminal UI, so the things you would otherwise file as feature requests are edits:

  • The footgun list is a lookup table. Adding SINTERSTORE, or removing HGETALL because your hashes are small and the flag is noise, is a line in lib/classify.js.
  • The key-pattern heuristic is a regex per segment. If your keys are tenant-4821-cart with no separator, teach it your scheme rather than accepting one giant pattern.
  • The slow-query floor is a patchable global. Bind a key to it and sweep the threshold live to see where your latency distribution actually sits.
  • The aggregation is plain JavaScript over a ring buffer. Counting by client comm instead of by key pattern, or emitting JSON for a CI check instead of a TUI, is a different reducer over the same events.
  • The same shape covers other protocols. httpwatch for HTTP, grpcsnoop for gRPC and h2c, on the same host with the same runtime.

That is the difference between a tool and a runtime. A fixed tool answers the questions its author anticipated, and every Redis keyspace is a little idiosyncratic, so the question you have at 2am is frequently one nobody anticipated. Writing a probe against a live system is a normal afternoon's work here rather than a vendor conversation, which is also why the limits below bound this script rather than the approach.

What can't eBPF Redis monitoring see, and when is it the wrong tool?

Stated plainly, because trusting an empty screen is the expensive mistake:

  • Unix domain sockets are invisible. The TCP path hooks tcp_sendmsg, so a client on /var/run/redis/redis.sock produces nothing. The most common cause of an empty table.
  • Non-OpenSSL TLS clients are invisible on both paths, silently. Go crypto/tls, Rust rustls, anything statically linked. Verified by measurement, not inferred.
  • Verb and first argument only. 64 bytes are copied; later arguments, values and all reply bodies are never read. You see that SET session:9f2a happened, not what was stored.
  • No latency on the TLS path. The uprobe fires on the request and emits immediately with lat_us set to 0. Round-trip timing is plaintext-only.
  • Counters are in memory and cumulative. No retention, no history, no export; a restart loses everything.
  • One host, no fleet view. No aggregation across machines, at all.
  • Connection chatter is filtered out. PING, AUTH, HELLO, SELECT, INFO, CONFIG and similar are dropped before counting, so this is the wrong tool for hunting health-check volume.
  • Footguns fire on the verb, not the data, and a pattern's distinct-key count saturates at 2000, past which the top-keys drill-down is drawn from the first 2000 keys seen.
  • It sees traffic, not the server. A fork() stall, THP after fork, swap or an expiration wave are real causes of latency that leave no trace in the traffic. Those are Redis's list, and redis-cli --intrinsic-latency, vmstat and INFO are the tools.

The bottom line: slowlog for blocked, traffic for saturated, MONITOR for values

Run SLOWLOG GET 10 and INFO commandstats first, every time, because they cost nothing and they resolve the whole "server is blocked" half of the problem. If slowlog names a command, you are done. If it is empty and Redis is still slow, read that as an unanswered question and not as an answer: a log records what something decided was worth recording, so slowlog holds nothing about volume, excludes the reply write from its timing by design, does not survive a restart, and has no way to report what it discarded. At that point the question is demand, and no server-side tool answers it, because Redis counts by command type and never by key. That is when redissnoop is the right thing to run, and what it can't see is the section to read before you trust a quiet screen, because a Unix socket and a Go TLS client both produce silence rather than an error. Keep MONITOR for the argument values on an instance with headroom, redis-cli --bigkeys for keyspace shape, and Datadog or Prometheus for retention and the fleet. And when the view you need is not one of these, yeet is the runtime the probe is written in, which is a shorter path than finding another vendor. The failure mode that costs the most hours is the one this post exists to break: reading a clean slowlog as evidence that Redis is fine.

Frequently asked questions

Why is my Redis slow?

Redis latency has two shapes and they need different tools. Either the server is blocked, by a slow O(N) command, a fork for RDB or AOF, transparent huge pages, swap, or many keys expiring in the same second, or the server is fine and your application is asking too much of it. Redis documents the first set thoroughly and slowlog plus INFO commandstats will find them. For the second, nothing on the server tells you which key pattern or which client is responsible, because Redis counts commands by type and never by key. Reading the traffic itself is what closes that gap.

What is the difference between a log and traffic when debugging a database?

A log is a record something chose to write about itself, so it inherits that writer's threshold, sampling and blind spots. Traffic is what actually crossed the wire, so it exists whether or not anything decided to record it. Redis's slowlog only holds commands over a configured duration and excludes the reply write from its timing; the traffic contains every command at full count regardless. When a log and the observed behavior disagree, the log is usually not wrong so much as scoped, and reading the traffic is how you find what fell outside its scope.

How do I find out what is hammering my Redis?

Count what your application sends, grouped by key pattern, not by command. Redis itself cannot tell you: INFO commandstats reports calls and usec per command type, so you learn that GET was called nine million times and nothing about which keys. Collapsing user:1839 and user:204 into user:* before counting is what turns a command log into an answer. redissnoop does this from a kernel probe on the client host, so Redis is never asked and never pays for the question.

How do I see what commands are being sent to Redis?

MONITOR is the documented way and Redis publishes its cost: throughput falls from 104,275 to 45,330 GET per second with one MONITOR client attached, which the page describes as more than 50%. It is the right tool when you need full argument values. If you want the commands without the server doing the work, a kprobe on tcp_sendmsg reads the RESP request as it leaves the client, which costs the server nothing because Redis is never contacted.

Why does my application report higher database latency than the database does?

Because the two numbers measure different spans. A server reports the time it spent executing, and your client measures the round trip, which also contains the network, the reply write and the client's own scheduling. Redis documents that slowlog's duration excludes I/O like sending the reply, and that a 1 Gbit/s link adds roughly 200 microseconds per round trip against about 30 for a Unix socket. A large gap between the two is not a contradiction, it is the cost of the round trips, and it usually points at a chatty client, not a slow server.

How do I attribute network traffic to a specific process on Linux?

Read it at the syscall, not at the socket. A connection gives you an IP and a port, which on a container host or a Kubernetes node is frequently an address that has already been reused. A kprobe on tcp_sendmsg fires in the context of the calling task, so the sending comm and pid are available at the moment the data is written, attached to the payload that was sent. That is how a tool can say which process issued a specific command instead of which address held a connection.

Why does static analysis miss dangerous database calls that reach production?

Because the call often is not in the code you searched. It can sit in a dependency, in a hand-run migration script, in a Lua script evaluated at runtime, or behind a wrapper whose name does not contain the command. Grep answers a question about source text; what reaches production is a question about behavior. Observing the wire settles it, because a command either appeared or it did not, and the sending process is attached to it.

Can I watch Redis traffic without slowing Redis down?

Yes, if the tool never asks Redis anything. A kprobe attaches to tcp_sendmsg, a function the kernel was already calling, copies a fixed 64 bytes and returns. Nothing is added to the request path and the server is not contacted, so its share of the cost is zero, not merely small. This is the structural difference from MONITOR, where the cost is the Redis process itself formatting and relaying every command.

Can eBPF see Redis commands over TLS?

Through the client's TLS library, yes. A uprobe on SSL_write in libssl reads the plaintext buffer the application hands OpenSSL before encryption. A client that does not route TLS through a dynamically linked OpenSSL is invisible on both paths at once, because there is no symbol to hook and the wire carries ciphertext. Go's standard library crypto/tls is the common case since it is pure Go and links no libssl, and it fails silently, so an empty encrypted counter means cannot see, not nothing happened.

Can I add observability to a running process without restarting it?

With kernel-side probes, yes. A kprobe or uprobe attaches to a function that already exists in a running process, so nothing restarts and nothing is preloaded. Most alternatives cannot do this: an OpenTelemetry Java agent attaches through a JVM startup flag, so a process begins instrumented or it does not, and a proxy or sidecar is a deploy and a path change. This matters most during an incident, since restarting the process you are investigating destroys the state that would have explained it.

What is the lowest-overhead way to monitor Redis traffic on Linux?

Reading it from the kernel and filtering there. redissnoop enforces a slow-query floor inside the BPF program as a patchable global, so commands faster than the floor are dropped before they reach userspace and cost scales with matched commands, not with total traffic. Raising the floor makes the tool cheaper instead of busier, which is the opposite of how a sampling agent behaves.

Do I need root to trace Redis traffic with eBPF?

The BPF load needs privilege, but your shell does not. With yeet the daemon performs the privileged load, so yeet run is unprivileged and never takes sudo. Doing it yourself requires CAP_BPF and CAP_PERFMON or root, plus a kernel built with BTF for CO-RE and uprobe support for the TLS path, which are defaults on current Ubuntu, Debian and Fedora.

What language can I write eBPF programs in without learning C?

JavaScript, if you use yeet as the runtime. Writing eBPF traditionally means C plus libbpf plus a build toolchain and a CO-RE story for kernel differences. yeet loads the compiled BPF object and exposes ring buffers, maps and probe attachment to JavaScript, so the program logic, the aggregation and the terminal UI are all JS. redissnoop is a few hundred lines of it, which is why changing what counts as a footgun or tracking a different field is an edit and not a feature request.

Sources

  • Diagnosing latency issues (Redis docs, 2026) — the canonical inventory of server-side latency causes, and the source for the "server is blocked" column here: slow O(N) commands on the single thread, fork() cost scaling with page-table size (a 24GB instance needs a 48MB page table, forked at roughly 9 to 13ms per GB on physical hardware but up to 424ms per GB on Linode's Xen), transparent huge pages causing copy-on-write of "almost the whole process memory" after fork, swap, AOF fsync behavior, and expiration waves when over 25% of sampled keys expire in one second; names KEYS in production as "a VERY common source of latency"; documents that a 1 Gbit/s network adds about 200 microseconds and a Unix socket about 30, and that "a client performing many roundtrips to the server will have to pay for these network and system related latencies".
  • SLOWLOG GET (Redis docs, 2026) — the source for slowlog's structural blind spot: execution time "does not include I/O operations like talking with the client, sending the reply and so forth, but just the time needed to actually execute the command"; entries are written only when a command exceeds slowlog-log-slower-than and the ring is bounded by slowlog-max-len; seven values per entry including a progressive id, timestamp, microsecond duration, argument array, client IP and port, client name, and since Redis 8.8 the pre-truncation argument count; the argument array truncates at slowlog-max-argc, 32 by default, replacing the final element with ... (N more arguments).
  • MONITOR (Redis docs, 2026) — documents its own cost: "because MONITOR streams back all commands, its use comes at a cost", and "running a single MONITOR client can reduce the throughput by more than 50%", from a benchmark the page calls "totally unscientific" run as redis-benchmark -c 10 -n 100000 showing GET 104,275.29 req/s falling to 45,330.91, SET 95,419.85 to 41,823.50, INCR 93,283.58 to 41,771.09 and PING_BULK 102,880.66 to 59,136.61; more MONITOR clients reduce throughput further; administrative commands are excluded and AUTH redacted, QUIT is not logged, and since 6.2.4 AUTH, HELLO, EVAL, EVAL_RO, EVALSHA and EVALSHA_RO appear.
  • INFO (Redis docs, 2026) — the source for what Redis does and does not aggregate: commandstats reports per command type only, as cmdstat_XXX:calls=,usec=,usec_per_call=,rejected_calls=,failed_calls= plus slowlog_count, slowlog_time_ms_sum and slowlog_time_ms_max added in Redis 8.8; latencystats gives p50, p99 and p999 per command type, configurable through latency-tracking-info-percentiles; keysizes reports key-size distribution per data type and keyspace reports per-database counts, so nothing in INFO attributes traffic to a key or a key pattern.
  • KEYS (Redis docs, 2026) — O(N) in the number of keys; "use extreme care when using this command in production environments", "it may ruin performance when it is executed against large databases", "this command is intended for debugging and special operations" and "don't use KEYS in your regular application code", recommending SCAN or sets; supplies the counterweight that explains why it survives code review, noting Redis "running on an entry level laptop can scan a 1 million key database in 40 milliseconds"; in Redis Cluster a pattern implying a single slot iterates only that slot.
  • SCAN (Redis docs, 2026) — O(1) per call and O(N) for a full iteration, against KEYS's single blocking O(N) call; a full iteration returns every element present from start to end and never one absent throughout, but an element may be returned multiple times so callers must be idempotent, and elements not constantly present may or may not appear; COUNT is "just a hint" defaulting to 10, and the server may return fewer, zero or more elements than asked.
  • redis-cli (Redis docs, 2026) — --hotkeys "only works when maxmemory-policy is *lfu", so ranking keys by access frequency costs an eviction-policy change on production; --bigkeys, --memkeys, --keystats and --hotkeys all sample the keyspace through SCAN and so "can be executed against a busy server without impacting the operations", with -i 0.01 throttling load "to a negligible amount"; --latency samples round-trip PING time in milliseconds and --latency-percentiles reports p50/p99/p99.9; --intrinsic-latency must run on the server and measures the machine's own scheduling floor rather than Redis.
  • Datadog redisdb integration (Datadog docs, 2026) — polls the server for performance, memory, blocked clients, replica connections, persistence, and expired and evicted key metrics; per-command counts are opt-in through command_stats and tagged by command, and per-key element counts are opt-in through the keys option and tagged by key, requiring the keys to be named in configuration ahead of time; the Redis 6+ ACL needs +config|get, +info and +slowlog|get.
  • Redis Insight (Redis docs, 2026) — its Profiler "analyze[s] every command sent to Redis in real time", which is the MONITOR-class cost behind a GUI, and a separate Slow Log tool renders SLOWLOG entries; Database analysis reports data-type distribution, memory allocation and expiry summaries but "will only analyze up to 10,000 keys" and past that "will attempt to use extrapolation in its analysis", so on a million-key database it is a sample presented as a summary.
  • redissnoop (yeet-src, 2026) — two kprobes (tcp_sendmsg, tcp_cleanup_rbuf) plus a uprobe/SSL_write in libssl, one 256KB ring buffer and a 16384-entry inflight hash keyed by sock *; copies the first 64 bytes and lifts verb and first argument only; the slow-query floor is a volatile __u64 in .data patched live from userspace so faster commands are dropped in-kernel; keys collapse to patterns on :, / and . with a 2000-distinct-key cap per pattern; requires only a real reply (copied > 0) to time a round trip, guarding against the ACK-time tcp_cleanup_rbuf call; emits lat_us = 0 on the TLS path; verifier-checked in CI across kernel lines 6.1, 6.6, 6.12 and bpf-next.

Related resources


Built with yeet, a JS runtime for writing eBPF programs on Linux machines. Join us on discord.