Skip to main content
The Toolchain

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.

Building a Redis Memory Fragmentation Ratio Tool
Marcus ThorneMarcus Thorne29 July 202611 min read

In this review

Share

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_memory

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

redis memory 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 100mb

These 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 RatioDiagnosisRecommended ActionRisk if Ignored
0.90 – 1.05Healthy allocator stateNo action; continue baseline pollingNone
< 0.90RSS undercounted vs used_memory — likely swap activityCheck vm.swappiness, verify no OOM pressure on hostLatency spikes on page-in, silent data loss risk if OOM killer triggers
1.05 – 1.50Normal churn-driven fragmentationEnable activedefrag with conservative cycle-maxGradual RSS creep, minor
1.50 – 2.00Structural fragmentation, arena compaction lagging writesIncrease defrag cycle aggressiveness, review TTL/eviction patternsNode approaches memory ceiling, eviction storms begin
> 2.00Allocator effectively unrecoverable via defrag aloneSchedule rolling restart or replica promotion; export/reload datasetOOM 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.

Building a Redis Memory Fragmentation Ratio Tool architecture diagram 2

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: INFO requires only standard client access, but if the calculator also issues MEMORY PURGE or 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 breaker on restart frequency is non-negotiable before enabling fully automated remediation.
  • 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.

  1. 01official Redis memory optimisation referenceredis.io
Marcus Thorne

Marcus Thorne

Toolchain Reviewer

Marcus Thorne is a pragmatic software architect focused on highly concurrent, distributed transactional systems. With a robust background in designing eventual consistency models and event-driven microservices in Rust and Go, he has successfully transitioned monolithic architectures to event-sourced paradigms at scale. His technical philosophy prioritises mechanical sympathy, performance profiling, and rigorous domain-driven design.

View Profile
Reader Interaction

Comments

Add a thoughtful note on Building a Redis Memory Fragmentation Ratio Tool. Comments are checked for spam and held for moderation before appearing.

Loading comments...
Comment submission is disabled until Cloudflare Turnstile keys are configured.

Learn More About KBY

Was this useful?

New engineering tools, ready to use.

Receive new calculators, diagnostic tools, Engineering Labs and technical reference systems.