Building a Redis Memory Fragmentation Ratio Tool
Building a poller that reads INFO memory, computes mem_fragmentation_ratio, and gates active-defrag tuning before Redis RSS outgrows the heap.

In this review
Table of Contents
Table of contents
A Redis instance reporting used_memory of 4GB while consuming 9GB of resident set size is not leaking data, it is fragmenting the underlying allocator’s page map. This is the operational reality of redis memory fragmentation: a divergence between logical key-space size and physical memory footprint that no amount of key eviction will fix, because the problem lives one layer below the data structures Redis manages. Left unaddressed, it forces premature OOM kills, triggers cgroup throttling in Kubernetes, and inflates cloud memory bills for workloads that haven’t actually grown. This article walks through building a purpose-built diagnostic calculator that polls Redis INFO output, computes fragmentation trend lines, and gates active-defrag configuration changes before the allocator digs itself into an unrecoverable hole.
#Why Redis Memory Fragmentation Happens at the Allocator Layer
Redis delegates heap management to jemalloc on Linux builds (the default since Redis 2.6+), replacing glibc’s malloc specifically because jemalloc’s size-class binning and per-thread arenas reduce fragmentation under Redis’s allocate-heavy, free-heavy workload pattern. jemalloc buckets allocations into fixed size classes; a 130-byte object gets rounded up into a 160-byte slot. When millions of small hash-field or sorted-set-member allocations churn through DEL/expire cycles, the freed slots aren’t reliably reused by subsequent allocations of a slightly different size class, leaving holes in otherwise reserved pages. The operating system cannot reclaim those pages because jemalloc still holds them mapped, even though logical Redis data no longer occupies them.
This is why redis memory fragmentation is measured as a ratio rather than an absolute figure. The canonical formula, exposed directly in INFO memory, is:
1mem_fragmentation_ratio = used_memory_rss / used_memoryA ratio near 1.0 is healthy. Anything sustained above 1.5 means the process is holding roughly 50% more physical memory than its logical dataset requires. Below 1.0 (which happens under heavy swapping) indicates a different pathology entirely — memory has been swapped out and RSS undercounts the true working set.
#Arena-Level Behaviour Under Write-Heavy Workloads
jemalloc assigns each Redis worker thread (and the main event loop) its own arena to avoid lock contention on allocation. Under a workload dominated by short-lived objects — session caches with aggressive TTLs, pub/sub buffers, or Lua script temporaries — arenas accumulate what jemalloc calls “dirty pages”: pages that have been freed by the application but not yet returned to the OS via madvise(MADV_DONTNEED). Redis’s own activedefrag mechanism exists specifically to compact these arenas incrementally without blocking the main thread, but it is disabled by default and frequently misconfigured when enabled.
#Implementation Logic: Building the Diagnostic Calculator
The tool needs three functional layers: a poller that samples INFO memory at fixed intervals, a rolling calculator that derives fragmentation velocity (not just the instantaneous ratio), and a decision engine that maps observed trends onto specific remediation actions. Instantaneous ratio checks produce false positives during transient spikes (e.g. a large BGSAVE fork); trend-based sampling over a 5-10 minute rolling window is what actually separates transient churn from structural fragmentation.

1import redis
2import time
3from collections import deque
4
5class FragmentationCalculator:
6 def __init__(self, host="localhost", port=6379, window=30):
7 self.client = redis.Redis(host=host, port=port, decode_responses=True)
8 self.window = deque(maxlen=window)
9
10 def sample(self):
11 info = self.client.info("memory")
12 ratio = info["mem_fragmentation_ratio"]
13 used = info["used_memory"]
14 rss = info["used_memory_rss"]
15 self.window.append((time.time(), ratio, used, rss))
16 return ratio, used, rss
17
18 def trend_slope(self):
19 if len(self.window) < 2:
20 return 0.0
21 t0, r0, _, _ = self.window[0]
22 t1, r1, _, _ = self.window[-1]
23 return (r1 - r0) / max((t1 - t0), 1)
24
25 def recommend(self, ratio, slope):
26 if ratio > 1.5 and slope > 0.001:
27 return "ESCALATE: enable activedefrag or schedule rolling restart"
28 if ratio > 1.5:
29 return "WATCH: fragmented but stable, monitor next cycle"
30 if ratio < 0.95:
31 return "INVESTIGATE: possible swap activity, check vm.swappiness"
32 return "OK"
33
34if __name__ == "__main__":
35 calc = FragmentationCalculator()
36 while True:
37 ratio, used, rss = calc.sample()
38 slope = calc.trend_slope()
39 print(f"ratio={ratio:.3f} used={used} rss={rss} slope={slope:.5f} -> {calc.recommend(ratio, slope)}")
40 time.sleep(20)The trend_slope method is the differentiator from a naive threshold check. A ratio of 1.6 that has been flat for an hour is a configuration decision; a ratio climbing from 1.1 to 1.6 over ten minutes is an active incident, likely correlated with a specific write pattern (bulk deletes, TTL sweeps, or a schema migration touching large hash objects).
#Tuning Active Defragmentation Against the Calculator’s Output
Once the calculator flags sustained redis memory fragmentation above the 1.5 threshold, the remediation path runs through Redis’s built-in defrag cycle rather than a blunt restart. The relevant directives control how aggressively jemalloc arenas get compacted without blocking client commands:
1# redis.conf — active defrag tuning
2activedefrag yes
3
4# Minimum fragmentation ratio before defrag cycles start
5active-defrag-threshold-lower 10
6
7# Fragmentation ratio (percentage above optimal) that triggers max effort
8active-defrag-threshold-upper 100
9
10# CPU percentage floor and ceiling for defrag cycles
11active-defrag-cycle-min 5
12active-defrag-cycle-max 75
13
14# Skip defragging keys smaller than this — avoids wasted cycles on tiny objects
15active-defrag-ignore-bytes 100mbThese values matter because active-defrag-cycle-max sets a hard ceiling on how much CPU time defrag work can consume per cycle. Setting it too aggressively on a latency-sensitive instance (sub-millisecond p99 targets) will show up as tail-latency regressions during defrag windows — the calculator’s polling loop should therefore log defrag cycle activity via INFO stats (active_defrag_hits, active_defrag_misses) alongside the fragmentation ratio so operators can correlate defrag load against latency dashboards rather than treating them as independent signals. Full parameter semantics are documented in the official Redis memory optimisation reference.
#Decision Matrix: Ratio Bands and Remediation
| Fragmentation Ratio | Diagnosis | Recommended Action | Risk if Ignored |
|---|---|---|---|
| 0.90 – 1.05 | Healthy allocator state | No action; continue baseline polling | None |
| < 0.90 | RSS undercounted vs used_memory — likely swap activity | Check vm.swappiness, verify no OOM pressure on host | Latency spikes on page-in, silent data loss risk if OOM killer triggers |
| 1.05 – 1.50 | Normal churn-driven fragmentation | Enable activedefrag with conservative cycle-max | Gradual RSS creep, minor |
| 1.50 – 2.00 | Structural fragmentation, arena compaction lagging writes | Increase defrag cycle aggressiveness, review TTL/eviction patterns | Node approaches memory ceiling, eviction storms begin |
| > 2.00 | Allocator effectively unrecoverable via defrag alone | Schedule rolling restart or replica promotion; export/reload dataset | OOM kill, unplanned failover, cascading replication lag |
#Diagnostic Flow Across the Poller and Redis Node
Rendering diagram...
#Failure Modes and Edge Cases
The calculator itself introduces failure surfaces worth accounting for. Polling INFO too frequently (sub-second intervals) on a large keyspace adds measurable command overhead on the single-threaded Redis event loop — INFO is O(1) for memory stats specifically, but aggressive polling still competes for the same event loop slot as production traffic under high QPS.
A more subtle edge case involves Redis Cluster deployments: fragmentation ratios are per-node, not cluster-wide, and a single hot shard fragmenting heavily can be masked entirely if the calculator aggregates averages across nodes rather than tracking each shard independently. Any implementation extending beyond a single node must key metrics by node ID, not just cluster name, or a single degrading shard becomes invisible in an averaged dashboard until it OOMs.

Active defrag also interacts poorly with maxmemory-policy settings using LRU or LFU eviction. Defrag cycles temporarily increase allocator overhead while consolidating pages, which can itself trigger eviction on memory-constrained instances mid-cycle — a case where the remediation for fragmentation causes a secondary symptom (unexpected key eviction) that looks unrelated unless the calculator’s timeline is cross-referenced against eviction counters (evicted_keys) in the same polling window.
Finally, fork-based persistence (BGSAVE, BGREWRITEAOF) causes RSS to spike independently of fragmentation, because copy-on-write pages inflate used_memory_rss during the fork’s lifetime. A calculator that doesn’t suppress sampling during known fork windows will log false ESCALATE events every time a scheduled snapshot runs. Cross-referencing rdb_bgsave_in_progress and aof_rewrite_in_progress flags from the same INFO payload before evaluating the ratio removes this entire class of false positive.
#Scaling and Security Trade-offs
Deploying this kind of diagnostic tooling across a fleet rather than a single node introduces trade-offs that should be weighed explicitly against the broader architectural patterns already governing the observability stack:
- Polling granularity vs event-loop contention: 20-30 second intervals are safe for most workloads; sub-5-second polling on high-QPS clusters measurably competes with production commands for event loop time.
- Centralised vs sidecar collection: a sidecar-per-node model isolates blast radius if the collector misbehaves, but a centralised poller scales operational simplicity at the cost of a single point of metric loss during network partitions.
- Credential exposure:
INFOrequires only standard client access, but if the calculator also issuesMEMORY PURGEor triggers restarts programmatically, that write path needs a scoped ACL user (Redis 6+ ACLs) rather than the default superuser, limiting blast radius if the automation logic misfires. - Automated remediation vs human gating: auto-triggering rolling restarts above the 2.0 threshold removes operator latency from incident response, but risks restart loops if the underlying write pattern causing fragmentation hasn’t actually changed — a circuit breakeron restart frequency is non-negotiable before enabling fully automated remediation.The KBY LexiconCircuit Breaker (Distributed Systems)A stateful control-flow construct that wraps a remote call and trips to a fail-fast state after a configured error threshold, preventing a client from hammering a degraded or unreachable dependency.
- Metric retention cost: storing raw fragmentation samples at fleet scale (hundreds of nodes, 20-second intervals) generates non-trivial time-series volume; downsampling to 1-minute rollups after 24 hours keeps long-term trend analysis viable without unbounded storage growth.
Treating redis memory fragmentation as a first-class metric — sampled, trended, and gated against explicit thresholds rather than discovered during an OOM postmortem — shifts the operational posture from reactive firefighting to scheduled maintenance. The calculator described here is deliberately minimal; the value is in the decision logic layered on top of a metric Redis already exposes for free, not in any exotic instrumentation.
Evidence trail
Sources and verification
Primary documentation and external technical references used in this article.
Comments
Add a thoughtful note on Building a Redis Memory Fragmentation Ratio Tool. Comments are checked for spam and held for moderation before appearing.
Related articles
The IT Toolkit
Building a DNSSEC Chain-of-Trust Validator
Walking the DS-DNSKEY-RRSIG delegation chain node by node to build a DNSSEC validation tool that pinpoints exactly where trust breaks.
The IT Toolkit
Building a Bandwidth-Delay Product Calculator
How BDP maths, RFC 1323 window scaling, and Mathis-model loss correction combine into a diagnostic tool for tuning throughput on long-fat networks.
The IT Toolkit
Building a Webhook Retry Storm Calculator
How exponential backoff without jitter synchronises failed webhook retries into a collision spike, and the maths behind a calculator that predicts it.
The IT Toolkit
Building a Load Average Diagnostic Calculator
Parsing /proc/loadavg, nproc and iowait into a load average calculator that separates CPU contention from disk-bound stalls.
Learn More About KBY
About KBY
Learn about our mission, editorial standards, and commitment to trusted engineering knowledge.
Why Trust KBY
Explore the processes and policies that ensure our publications are accurate, useful, and responsible.
Newsletter
Get our latest editorial publications, research and practical insights sent directly to your inbox.
Was this useful?
New engineering tools, ready to use.
Receive new calculators, diagnostic tools, Engineering Labs and technical reference systems.