named RSS trending upward after warmup is the leading indicator before an OOM kill. The hard part is distinguishing three conditions that look identical on an RSS chart: cache-driven growth that will plateau under max-cache-size, allocator fragmentation that inflates RSS without a real leak, and genuine unbounded growth from a bug or misconfiguration.
RSS that climbs during a traffic burst or cache warmup and stays high is normal allocator behavior. BIND’s allocator (jemalloc on most builds) holds freed blocks for reuse rather than returning them to the OS via munmap. The operational question is not “is RSS high?” but “is RSS still growing, and how much runway remains?”
If RSS is growing linearly: time-to-OOM = (memory_limit - current_RSS) / growth_rate_per_day. A runway measured in weeks is a capacity planning problem. A runway measured in hours is an active incident.
What this means
Cache-driven growth (normal, bounded): The cache fills during warmup and after traffic bursts. RSS rises until the cache hits max-cache-size and LRU eviction begins (DeleteLRU increments). Growth should decelerate and plateau.
Allocator fragmentation (sticky but stable): The allocator holds freed blocks for reuse. RSS stays elevated even after cache entries expire or are evicted. Normal as long as RSS stabilizes. The gap between BIND’s internal accounting (InUse from the statistics channel) and OS-reported RSS (VmRSS from /proc) is the fragmentation overhead.
Unbounded growth (real problem): RSS grows monotonically without stabilizing. TreeMemInUse or HeapMemInUse trend upward without a corresponding increase in CacheNodes. This indicates a genuine leak, max-cache-size not effectively limiting cache memory, or non-cache consumers (RPZ datasets, zone data, ADB) growing outside the cache budget.
flowchart TD
A["named RSS trending up"] --> B{"Still warming up?
uptime < 30 min"}
B -->|Yes| C["Normal cache fill
Wait and re-check"]
B -->|No| D{"RSS plateaued
or still growing?"}
D -->|Plateaued| E["Normal: allocator
holds freed pages"]
D -->|Still growing| F{"TreeMemInUse rising
without CacheNodes?"}
F -->|Yes| G["Suspected leak
Check BIND version and CVEs"]
F -->|No| H{"DeleteLRU
incrementing?"}
H -->|Yes| I["Cache at capacity
Bound with max-cache-size"]
H -->|No| J["Check non-cache memory:
RPZ, zone data, ADB"]Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Cache warming (normal) | RSS climbs for 30-60 min after restart, then plateaus. CacheHits rising, CacheMisses falling. | Uptime. If under 1800 seconds, wait. |
max-cache-size not configured | RSS grows toward 90% of physical RAM (the default for recursive views). No eviction pressure visible. | named-checkconf -p | grep max-cache-size |
max-cache-size ineffective on specific versions | RSS grows past the configured cap. DeleteLRU not incrementing despite cache exceeding limit. | BIND version. See version-specific notes below. |
| Allocator fragmentation | RSS high but stable. BIND internal InUse much lower than OS VmRSS. No monotonic growth trend. | Compare internal vs OS memory over time. |
| CVE-2026-3104 memory leak | Unbounded RSS growth on a recursive resolver. Assertion failure on shutdown or reload. | BIND version: affects 9.20.0 through 9.20.20, 9.21.0 through 9.21.19. Fixed in 9.20.21 / 9.21.20. |
| jemalloc dirty-page glitch | RSS grows slowly over days without stabilizing. No corresponding cache growth. | MALLOC_CONF environment variable. BIND patch level. |
| RPZ datasets | RSS grows with RPZ zone load or after feed update. Memory is outside the cache budget. | RPZ zone count and total entry count. |
| Zone data | RSS grows with authoritative zone count or large zone transfers. Not subject to max-cache-size. | Zone count and total zone data size. |
Quick checks
# Version and uptime
rndc status | head -5
# Current RSS in MB from /proc
awk '/VmRSS/{print $2/1024 " MB"}' /proc/$(pidof named)/status
# Peak RSS for context
awk '/VmPeak/{print $2/1024 " MB"}' /proc/$(pidof named)/status
# ISC-recommended RSS measurement: pmap Dirty column total (last line)
pmap -x $(pidof named) | tail -1
# Cache memory stats per view (TreeMemInUse, HeapMemInUse, CacheNodes, DeleteLRU)
# Adjust port to match your statistics-channels configuration (8053 and 8653 are common)
curl -s http://localhost:8653/json/v1/server | \
python3 -c "import sys,json; d=json.load(sys.stdin); \
[print(f'{v}: TreeMemInUse={cs.get(\"TreeMemInUse\",\"N/A\")} HeapMemInUse={cs.get(\"HeapMemInUse\",\"N/A\")} CacheNodes={cs.get(\"CacheNodes\",\"N/A\")} DeleteLRU={cs.get(\"DeleteLRU\",\"N/A\")}') \
for v,vd in d.get('views',{}).items() if (cs:=vd.get('resolver',{}).get('cachestats',{}))]"
# Verify max-cache-size configuration
named-checkconf -p /etc/named.conf 2>/dev/null | grep -i "max-cache-size"
# Check for prior OOM kills of named
dmesg | grep -i "oom.*named\|named.*oom" | tail -10
# Check systemd restart count (OOM-kill loop indicator)
systemctl show named --property=NRestarts
# BIND internal memory context (total InUse vs Malloced)
curl -s http://localhost:8653/json/v1/mem | python3 -m json.tool | head -40
How to diagnose it
1. Confirm RSS is still growing. Sample RSS twice with a known interval and compute the daily rate. A single snapshot tells you nothing.
T1=$(date +%s); R1=$(awk '/VmRSS/{print $2}' /proc/$(pgrep -x named)/status); sleep 300
T2=$(date +%s); R2=$(awk '/VmRSS/{print $2}' /proc/$(pgrep -x named)/status)
# Daily growth rate in KB
echo "scale=0; ($R2 - $R1) * 86400 / ($T2 - $T1)" | bc
2. Rule out normal warmup. If uptime is under 1800 seconds, growth is cache warming. Wait and re-check. Cache hit rate should climb from near 0% toward baseline over 30-60 minutes.
3. Compare BIND internal memory with OS-reported RSS. If InUse from the statistics channel is stable or declining while VmRSS stays high, the gap is allocator fragmentation. If both climb in lockstep, the growth is real consumption.
4. Check whether the cache is at capacity and evicting. If DeleteLRU is incrementing, the cache has hit max-cache-size and is actively evicting. The cap is working. If TreeMemInUse exceeds the configured max-cache-size without DeleteLRU activity, the cap may not be effective for your BIND version.
5. Verify max-cache-size is configured. The default is 90% of physical memory for recursive views. On multi-purpose servers, this default is dangerous. In chroot deployments where BIND cannot detect physical memory, the default is effectively unlimited. Set an explicit value.
6. Check BIND version against known memory bugs.
- CVE-2026-3104: affects BIND 9.20.0 through 9.20.20 and 9.21.0 through 9.21.19. A crafted domain query causes unbounded RSS growth with no recovery.
namedalso crashes with an assertion failure on shutdown or reload. Fixed in 9.20.21 and 9.21.20. If your resolver handles untrusted query traffic and is in an affected range, treat this as the primary suspect. - jemalloc dirty-page purging glitch: if a thread’s first allocator call is
free()rather thanmalloc(), jemalloc’s dirty-page purging ticker is never initialized, causing RSS to creep upward over days. Fixed in BIND 9.16.29+ and 9.18.3+ via a dummy allocation at thread start.
7. Identify non-cache memory consumers. Authoritative zone data, RPZ datasets, and the Address Database (ADB) all consume memory outside the max-cache-size budget. On a server with thousands of zones or large RPZ feeds, these can dwarf the cache.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
VmRSS (/proc/$pid/status) | Physical memory consumed. The gauge before OOM. | Monotonic growth after warmup plateau. |
TreeMemInUse / HeapMemInUse | BIND’s internal cache memory accounting per view. | Growing without CacheNodes increase indicates fragmentation or leak. |
CacheNodes | Current cache entry count. Correlates cache size with memory use. | Flat or declining while TreeMemInUse grows means fragmentation. |
DeleteLRU | LRU eviction count. Confirms cache is at capacity and cap is working. | Zero while TreeMemInUse exceeds max-cache-size means cap is ineffective. |
max-cache-size | Caps cache memory. Default is 90% of RAM if unset. | Not explicitly configured on shared hosts. |
| System available memory | Headroom before OOM kill. | Declining trend with no corresponding cache growth. |
NRestarts (systemd) | Restart count. OOM-kill loop indicator. | Incrementing with OOM events in dmesg. |
Fixes
Cache at capacity
If DeleteLRU is incrementing rapidly and cache hit ratio is declining, the cache is undersized for the working set. Options:
- Raise
max-cache-size. This directly increases the memory ceiling, so verify the host has headroom first. Apply withrndc reconfigafter updatingnamed.conf. - Add resolver capacity behind a load balancer to spread the query load and increase aggregate cache.
- Accept the eviction rate if cache hit ratio is still above baseline.
See BIND cache eviction storms for the full pressure spiral pattern.
max-cache-size not configured or ineffective
Set an explicit value in named.conf:
options {
max-cache-size 2g;
};
For recursive views, this caps the cache. For authoritative-only views where recursion no, the setting is irrelevant because there is no resolver cache. Apply changes with rndc reconfig.
Be aware that on certain BIND versions in the 9.19 development branch, max-cache-size was inadvertently turned into a no-op for cache memory limiting because the internal water-mark function was removed from the cache sizing code path. If you observe TreeMemInUse growing past your configured limit with no DeleteLRU activity, test on a patched version.
Allocator fragmentation (stable high RSS)
If RSS is high but stable, and BIND’s internal InUse is much lower than OS-reported VmRSS, this is fragmentation. Do not restart named to “fix” it. The restart gives you a temporary lower RSS but loses the warm cache, triggering a cache warming storm and elevated upstream load.
For the jemalloc dirty-page glitch on older BIND versions, setting MALLOC_CONF=dirty_decay_ms:0 forces immediate purging of dirty pages at a performance cost. This is a workaround. Upgrade to a version with the dummy-allocation fix instead.
Genuine memory leak
If RSS grows monotonically without stabilizing and you have ruled out cache growth, fragmentation, and non-cache consumers:
- Check your BIND version against CVE-2026-3104 (affects 9.20.0 through 9.20.20). Upgrade to 9.20.21 or later if in range.
- On BIND 9.20.6+, use
rndc memprofto toggle runtime memory profiling and inspect allocator behavior. - If running BIND 9.16.x with
-M internal, switch to the default external allocator. The internal allocator was removed in 9.18. - As a last resort, schedule rolling restarts during low-traffic windows to reset RSS. Track the growth rate to estimate how frequently restarts are needed.
Non-cache memory (RPZ, zones, ADB)
Zone data and RPZ datasets are not bounded by max-cache-size. If these are the source of growth:
- Monitor RPZ feed sizes independently. A threat intelligence feed that doubles in size doubles its memory footprint.
- For authoritative servers with many zones, account for zone data separately in memory planning.
- The ADB tracks reachability and RTT to upstream authoritative servers. On a resolver resolving many unique upstream targets, ADB memory can be significant.
Prevention
Set max-cache-size explicitly on every recursive resolver. The default of 90% of physical RAM is appropriate only on dedicated DNS hosts. On shared infrastructure, use a fixed value that leaves room for the OS and other processes. Target 70% of available memory as the ceiling for total named RSS.
Track RSS as a trend, not a threshold. A single RSS value is meaningless. Track the daily growth rate after warmup. If it is nonzero and sustained, compute runway: (memory_limit - current_RSS) / growth_rate_per_day. Alert when runway drops below 14 days.
Keep BIND patched. Memory leaks in BIND are typically version-specific bugs. CVE-2026-3104 and the jemalloc glitch are both fixed in recent releases. Running an Extended Support Version (currently 9.18.x) with current patches is the simplest defense.
Separate recursive and authoritative roles. Mixed-role servers make memory diagnosis harder because cache growth and zone data growth are indistinguishable in aggregate RSS. Cache memory is bounded by max-cache-size; zone data is not.
Avoid polling the catch-all /json statistics endpoint too frequently. On hosts with many CPUs, a single fetch of the full JSON statistics can serialize tens of thousands of task objects and trigger a transient memory spike. Use granular endpoints (/json/v1/server, /json/v1/mem) instead.
How Netdata helps
Netdata’s BIND collector gathers per-view TreeMemInUse, HeapMemInUse, CacheNodes, DeleteLRU, and DeleteTTL at per-second resolution, correlated on the same timeline as process RSS (VmRSS), available system memory, and cgroup limits. This makes the fragmentation gap visible as the spread between internal memory accounting and OS-reported RSS.
The OOM kill detector surfaces kernel log events alongside the RSS trend that preceded them. After a restart, the cold-cache signature (zero CacheHits, elevated outbound query rate) appears on the same chart as the RSS reset.
ML-based anomaly detection on the RSS growth rate can flag a changing growth pattern before it crosses a fixed threshold, providing earlier runway warning than a static alert.
Related guides
- BIND DNSSEC validation failing: ‘broken trust chain’, ValFail, and SERVFAIL for signed domains
- BIND cache eviction storms: DeleteLRU, an undersized max-cache-size, and the pressure spiral
- BIND cache hit ratio dropping: the leading edge of recursive pain
- BIND clients-per-query and max-clients-per-query: duplicate recursion for popular names
- BIND cold cache after restart: the warming storm and elevated upstream load
- BIND DNSSEC failing from clock drift: NTP, RRSIG inception/expiry windows, and SERVFAIL
- BIND dnssec-validation disabled: the security regression that ‘fixes’ SERVFAIL
- BIND dynamic update failures: UpdateFail, denied updates, and TSIG drift
- BIND forwarding loops: recursion that never terminates and burns recursive slots
- How BIND actually works in production: a mental model for operators
- BIND inline signing silently failed: missing keys and a zone served unsigned
- BIND journal (.jnl) corruption: dynamic-update and IXFR failures that block zone load






