Skip to content

What Redis monitoring reads and what returns nothing

Health, performance, slow log and clients all derive from Redis introspection commands, while the table and index panels come back empty because Redis has neither.

An empty monitoring panel and a broken one look identical. Redis monitoring is INFO, SLOWLOG, CLIENT LIST, DBSIZE and nothing else; two of its panels come back blank on every connection, and a reader who cannot tell which command produced a figure has no way to know whether a blank is a missing feature, a permission problem, or a fact about the engine. So every figure on the Redis dashboard is traceable to the command it came from, and the blanks are traceable to the absence of a command rather than to the failure of one.

Redis monitoring is INFO, SLOWLOG, CLIENT LIST and DBSIZE

The provider extends the shared base class directly, and inherits the base class’s getMonitoringData(), which fans the individual monitoring methods out in parallel. Each of those methods is a Redis introspection command and a parse:

Method Source command What the panel shows
getHealth() INFO connected_clients, used_memory_human, hit ratio
getOverview() INFO + DBSIZE version, uptime, clients, maxclients, memory, key count
getPerformanceMetrics() INFO hit ratio, instantaneous_ops_per_sec as queries per second
getSlowQueries() SLOWLOG GET 10 per entry: id, command text, duration
getActiveSessions() CLIENT LIST one session per client: id, addr, db, flags, cmd, idle
getStorageStats() INFO memory used_memory_human, and a usage percent when maxmemory is set
getTableStats() none []
getIndexStats() none []

parseRedisInfo() turns the INFO bulk string into a flat key to value map, and every INFO-derived number above is a lookup in that map rather than a computation. The slow-log durations are the one conversion: SLOWLOG reports microseconds and the panel reports milliseconds.

CLIENT LIST is parsed the same way, per line, into key=value pairs - and every field in that reply is optional, so the parser substitutes a default for anything absent. The session’s user reads name falling back to default, its pid id falling back to 0, its state flags falling back to N, its command cmd falling back to idle. Worth knowing before you read the user column: the name field is empty on a client that never set one, so default is what most sessions show. It is the parser’s fallback, not a login.

The queries panel’s empty state says what is empty: Redis lists what SLOWLOG holds, and nothing has yet run slower than slowlog-log-slower-than. That sentence used to be PostgreSQL’s advice about pg_stat_statements on every engine, which is worse than no sentence, because it sends the reader to look for an extension that does not exist here.

Cache hit ratio, and what it is a ratio of

The hit ratio is keyspace_hits / (keyspace_hits + keyspace_misses) * 100, both counters read from INFO. Two things follow from that formula and both matter more than the number.

It is cumulative over the counters the server has kept, not a rate over the refresh interval. A server that spent a long stretch hitting nearly every read and the last ten minutes missing every one still shows a healthy figure: the panel refreshes, the counters it divides do not.

And when there has been no traffic at all, both counters are zero, the division is undefined, and the ratio defaults to 100.0. A freshly started Redis that has served nothing reports a perfect cache hit ratio. That default is deliberate - the alternative is a zero, which reads as a catastrophe rather than as silence - but it means the figure answers “how have reads gone so far”, and on an idle server the honest answer is “they have not”.

This surfaces sharply on one of the Redis-compatible relatives. Garnet 2.1.5 publishes neither keyspace_hits nor keyspace_misses in its INFO, so the ratio takes the 100 fallback and the dashboard rates it Excellent on a server that is doing work. Same for memory: Garnet publishes no used_memory, so the overview’s database size and the memory storage panel read 0 B on a populated instance. These are absences wearing a value, and they are recorded as such rather than presented as measurements.

Panels that are empty because Redis has no such object

Table statistics and index statistics return empty on Redis, because Redis has neither. getTableStats() and getIndexStats() return [] - not an error, not a partial answer, an empty list - and the indexes array on every synthetic schema entry is empty for the same reason. There is no query to write that would fill them.

That is also why the “tables” those panels would describe are not tables. Schema discovery runs a cursor-based SCAN with COUNT 100, capped at 1000 keys, and groups the key names it found by everything before the first :, so user:123 and user:456 collapse into a row called user:*. That row is this server’s own summary of a bounded scan. It is not a key any command can be given, and it has no statistics of its own to report, because Redis does not keep any.

The stated limit on the monitoring feature is that what each panel can show is bounded by what the engine reports, and Redis is that sentence at its plainest: the tab renders the list the provider returned, and the list is empty, so nothing fills it with plausible zeros. The engine’s line on the databases page makes the same point one level up: no SQL, and none is pretended.

Degrading under a restricted ACL instead of breaking

The connection dialog’s Username field is the Redis 6 ACL user, passed to the driver as username and omitted entirely when empty. A least-privilege user is the normal case for a monitoring connection, and it splits the dashboard in two.

The SLOWLOG and CLIENT LIST reads are wrapped in try/catch and degrade to []. An ACL that forbids either command produces an empty queries panel and an empty sessions panel, not a failed dashboard. That is the same empty list the table and index panels return, arrived at for a different reason - which is precisely why the panel has to name its command rather than just render blank.

The INFO-derived surfaces behave differently on purpose. getHealth(), getOverview() and getPerformanceMetrics() all read INFO, and for a user without +info they raise the server’s own NOPERM sentence rather than answering with fabricated zeros. The connection still opens and keys still browse: the driver’s own ready check calls INFO, logs that it is skipping the ready check on NOPERM, and connects anyway. POST /api/db/test-connection reports that combination as degraded - amber, not a green tick and not a failure. The connection is real. The health read is not.

Measured against redis:latest with a second ACL user defined on >probepw ~* +@all -info:

{host, port, password}            -> ACL WHOAMI = default | INFO succeeds
{host, port, username, password}  -> ACL WHOAMI = probe   | INFO refused: NOPERM

To reproduce the restricted arm:

docker run --rm -d --name redis-acl -p 6389:6379 redis:latest
docker exec redis-acl redis-cli ACL SETUSER probe on '>probepw' '~*' +@all -info

Grant +info, or expect those three panels to stay unanswered for that user.

Reading the storage figure for what it is

getStorageStats() reads INFO memory. It reports used_memory_human, and a usage percent only when maxmemory is set - because a percentage needs a denominator, and an unbounded Redis has none. No maxmemory, no percentage.

The same rule catches the connection count. maxConnections comes from maxclients and falls back to 0. Redis, Valkey and KeyDB each publish maxclients:10000; neither DragonflyDB’s nor Garnet’s INFO carries the line at all, so the panel shows 0 - an absent limit rendered as a zero rather than a measured one. On Garnet, connected_clients stays 0 with a client attached while CLIENT LIST answers correctly, so the session list is right and the count above it is not.

All four of those relatives connect through the same redis type id; there is no per-engine branch and nothing renames the server, because the alternative is asserting a product the INFO reply never claimed. What the overview shows is the compatibility level the server published in redis_version, which is why a Valkey 9.1.1 instance reads 7.2.4.