How I Freed 26 GiB of RAM by Auditing My Homelab Kubernetes Cluster
A deep audit of my two-node Talos Linux cluster uncovered a Longhorn instance-manager holding 26 GiB of RAM due to Prometheus snapshot chain bloat. Here’s how I found it and fixed it.
My homelab Kubernetes cluster has been running on two nodes for a few months now: a Talos Linux VM (16 GiB) and an Intel NUC (32 GiB). Last week, Prometheus fired two alerts: CPUThrottlingHigh on a Grafana sidecar and NodeMemoryHighUtilization on the NUC. The NUC was sitting at 95% memory usage. On a 32 GiB machine running a homelab, that felt wrong.
What followed was a deep audit that uncovered a chain of issues: OOMKilling pods, network policies blocking sidecars, missing resource limits, and (the big one) a Longhorn instance-manager process holding 26 GiB of RAM due to Prometheus snapshot chain bloat.
The Starting Point
Here’s what kubectl top nodes showed:
NAME CPU(cores) MEMORY(bytes) MEMORY(%)
nuc-worker-01 1428m 29937Mi 95%
talos-1ue-4vm 646m 9845Mi 65%95% memory on a 32 GiB node. Something was very wrong. kubectl top pods immediately pointed the finger:
longhorn-system/instance-manager-... 232m 26254MiA single Longhorn instance-manager pod was consuming 25.6 GiB. But I didn’t start there. I found this after peeling back several layers.
Layer 1: The Obvious Fires
Miniflux OOMKilled (226 Restarts)
Miniflux, my RSS reader, had been OOMKilled 226 times. The memory limit was set to 128Mi, too tight for a Go process managing hundreds of RSS feeds. Fix was straightforward:
# apps/miniflux/deployment.yaml
resources:
requests:
cpu: 50m
memory: 128Mi # was 64Mi
limits:
cpu: 300m
memory: 256Mi # was 128MiLoki Sidecar Crash Loop (30 Restarts)
The loki-sc-rules sidecar (a kiwigrid/k8s-sidecar container that watches ConfigMaps for alerting rules) was crashing in a loop. Logs showed:
ConnectTimeoutError: Connection to 10.96.0.1 timed out10.96.0.1 is the Kubernetes API server ClusterIP. The sidecar needs API access to list and watch ConfigMaps, but my CiliumNetworkPolicy only allowed egress to DNS and Alertmanager. The fix was one stanza:
# infrastructure/loki/network-policies.yaml (CiliumNetworkPolicy)
egress:
# Sidecar watches ConfigMaps via K8s API
- toEntities:
- kube-apiserver
# ... existing DNS and Alertmanager rulesAfter applying this and restarting the Loki pod: zero restarts, sidecar synced immediately.
Grafana Sidecar CPU Throttling
The Grafana dashboard sidecar (grafana-sc-dashboard) was hitting its 200m CPU limit during dashboard discovery across all namespaces. Bumped to 500m. Alert cleared.
Layer 2: Missing Resource Limits
I ran an audit of every container in the cluster and found several sidecars and config-reloaders running without any resource limits:
- Loki’s
loki-sc-rulessidecar - Alloy’s
config-reloader - Prometheus and Alertmanager
config-reloadersidecars
Each of these was using 10–70 MiB. Not dangerous individually, but containers without limits are a ticking time bomb under memory pressure. The tricky part was finding the correct Helm value paths. Each chart has its own opinion about where sidecar resources go:
# Loki chart: top-level sidecar key
sidecar:
resources:
limits: { cpu: 100m, memory: 128Mi }
# Alloy chart: top-level configReloader key (NOT under alloy.)
configReloader:
resources:
limits: { cpu: 100m, memory: 64Mi }
# kube-prometheus-stack: under prometheusOperator
prometheusOperator:
prometheusConfigReloader:
resources:
limits: { cpu: 100m, memory: 64Mi }Lesson: always check helm show values for the actual schema. Don’t guess at nesting.
Layer 3: The 26 GiB Elephant
After fixing the obvious issues, the NUC was still at 95%. Time to dig into Longhorn.
The instance-manager is Longhorn’s supervisor process. It runs as a single pod per node and manages all the replica and engine processes for every volume on that node. I had 17 replicas running on nuc-worker-01. The child processes were using 25–90 MiB each (normal). But the parent Go process (PID 7) had ballooned:
$ cat /proc/7/smaps_rollup
Rss: 25522508 kB
Pss_Anon: 25513972 kB
Private_Dirty: 25513972 kB25.5 GiB of anonymous dirty memory in a single Go process. It had been running for 12 days without a restart.
Root Cause: Prometheus Snapshot Chain Bloat
I checked volume actual sizes versus spec sizes:
Volume Spec Actual Ratio
pvc-95b89bc0 (Prometheus) 10.0G 35.4G 3.5x !!!
pvc-efcabd3d (Loki) 5.0G 1.7G 0.3x
pvc-c2f4be48 (Grafana) 2.0G 1.4G 0.7x
... all others under 1.5G ...Prometheus was the only outlier. The reason: TSDB compaction. Prometheus periodically rewrites its time-series blocks, which means every Longhorn snapshot captures massive diffs. My snapshot config was retaining 7 daily + 4 weekly snapshots. The weekly snapshots alone were 7–10 GiB each:
weekly-s-ae7ab0de Mar 01 9.6 GiB
weekly-s-8332fe7d Feb 22 7.2 GiB
daily-sn-05bc8881 Mar 06 6.9 GiB10 snapshots totaling ~35 GiB of snapshot chain on a volume spec’d for 10 GiB. The instance-manager had to track all of this in memory. Over 12 days, the Go runtime accumulated allocations and never returned them to the OS (MADV_FREE behavior in Go’s GC).
The Fix
Four actions, applied in sequence:
1. Exclude ephemeral volumes from recurring snapshots. Prometheus, Loki, Alertmanager, Grafana, ntfy, and autokuma volumes don’t need Longhorn snapshots: their data is either ephemeral or rebuilt from scrapes/configs.
# Remove from the default snapshot group
kubectl label volume.longhorn.io/pvc-95b89bc0... \
-n longhorn-system \
recurring-job-group.longhorn.io/default-2. Purge all Prometheus snapshots.
kubectl get snapshots.longhorn.io -n longhorn-system \
-l longhornvolume=pvc-95b89bc0... -o name | \
xargs -I{} kubectl delete {} -n longhorn-system --wait=false3. Reduce snapshot retention for the remaining volumes (5 daily / 2 weekly, down from 7/4).
4. Restart the instance-manager to force a fresh Go process.
kubectl delete pod -n longhorn-system instance-manager-96c62ec7... --grace-period=30This causes a brief (~30 second) disruption to all volumes on the node while Longhorn rebuilds the engine processes. Every pod on nuc-worker-01 momentarily lost disk access. They all recovered automatically.
The Result
BEFORE:
nuc-worker-01 30038Mi 95%
instance-manager: 26254Mi
AFTER:
nuc-worker-01 4770Mi 15%
instance-manager: 497MiFrom 95% to 15% memory usage. 26 GiB freed. The cluster now has 27 GiB of headroom on the NUC.
Lessons
Don’t snapshot Prometheus volumes with Longhorn. TSDB compaction creates enormous diffs between snapshots. If you need Prometheus backup, use Thanos sidecar or periodic TSDB snapshots to object storage, not block-level snapshots.
Every container needs resource limits. Even “tiny” sidecars. Not because they’ll use a lot of memory today, but because under pressure, a container without limits can consume everything. Audit with:
kubectl get pods -A -o json | python3 -c "
import json, sys
for pod in json.load(sys.stdin)[items]:
for c in pod[spec].get(containers, []):
if not c.get(resources, {}).get(limits, {}):
print(f\"{pod[metadata][namespace]}/{c[name]}\")"Network policies are the silent killer of sidecars. Default-deny egress is the right posture, but every sidecar that talks to the Kubernetes API needs an explicit toEntities: [kube-apiserver] egress rule. The symptoms (timeout, crash loop) don’t obviously point to network policy. They look like API server issues.
Go processes don’t return memory. The Go runtime uses MADV_FREE, which marks pages as available but doesn’t force the kernel to reclaim them. A long-running Go process that had a high-water mark will hold that RSS forever. The only fix is a restart. For Longhorn, this means periodic instance-manager restarts are worth considering if you see memory creep.
Check actualSize, not spec.size. Longhorn’s spec.size is the provisioned capacity. The status.actualSize includes snapshot chains and can far exceed the spec. Monitor this: it’s the real disk (and memory) cost.
Full Change Summary
| Change | Before | After |
|---|---|---|
| Grafana sidecar CPU limit | 200m | 500m |
| Miniflux memory limit | 128Mi | 256Mi |
| Loki egress to API server | Blocked | Allowed |
| Traefik memory limit | 128Mi | 256Mi |
| Sidecar resource limits | Missing | Set on all |
| Longhorn snapshot retention | 7d/4w | 5d/2w |
| Prometheus snapshots | 10 (35 GiB) | Excluded |
| Non-critical volume replicas | 2 | 1 |
| nuc-worker-01 memory | 95% | 15% |
All changes deployed via FluxCD GitOps: four commits to the home-ops repo, reconciled automatically. The whole audit and fix cycle took about an hour.