ObservabilityLVI · Linux ObservabilityLinuxObs
Memory Observability
What you'll learn
- Distinguish MemTotal, MemFree, MemAvailable, and why the difference matters
- Read OOM kills from kernel logs and from node_vmstat_oom_kill
- Use PSI metrics to distinguish memory pressure from CPU pressure
- Diagnose the four most common memory failure shapes on production hosts
- Set per-host-class memory thresholds in production alerting rules
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
The application on host app-prod-12 is slow. The on-call engineer
opens Grafana. The memory panel reports “55% used.” Nothing is
red. The user complaint is real. The dashboard is asking the wrong
question: it is reporting used memory, not available memory.
A host can be at 95% used and still perfectly healthy because the
kernel is reclaiming clean pages for disk cache; the same host can
be at 55% used and thrashing because the dirty set is pinned and
swap is full.
Memory observability is the discipline of measuring the available memory (the kernel’s own estimate of how much a new workload could allocate without swapping), the pressure on the memory subsystem (how often something is waiting for memory), and the errors (OOM kills, swap exhaustion, allocation failures). The metrics that answer these questions are the production baseline.
What it is
Memory observability on Linux is the practice of exposing four classes of measurement from the kernel:
- Capacity -
MemTotal, the physical RAM. - Allocated -
MemFree,Buffers,Cached,SReclaimable,AnonPages. - Available -
MemAvailable, the kernel’s estimate of free memory plus memory that can be reclaimed without swapping. - Pressure - PSI
memorylines, which report the fraction of time at least one task is waiting for memory. - Errors - OOM kills, page-steal, swap-in/swap-out rate, allocation failures.
The naive dashboard reports “used” as MemTotal - MemFree. The
production-graded dashboard reports “available” as MemAvailable.
The difference is the most common single error in memory
observability.
Why a sysadmin cares
Memory exhaustion is the most operationally expensive host-level failure mode. The recovery is slow: the kernel starts swapping, the disk fills, the page cache thrashes, the application freezes and the user gives up. The OOM killer fires long after the saturation has begun, and the postmortem has to reconstruct the seconds before the kill from logs that may also be stuck in the same memory pressure.
The production pattern is: a host has been at 95% used for six months. The team looked at the panel and said “this is fine, the kernel is just using memory for cache.” Two days before the incident, a deployment increased the application’s resident set size beyond the cache-reclaimable zone. The host went from 95% used to 95% used and 95% MemAvailable exhausted, in one release. The OOM killer fired. The on-call engineer learned the difference between used and available at 03:00.
The lesson learned: capacity is not the same as availability. The metric you alert on is the metric that has to mean what you think it means.
How it works
The Linux kernel publishes its memory state through /proc/meminfo
and /proc/vmstat. The relevant fields and how node_exporter
maps them:
/proc/meminfo field node_exporter metric
--------------------- ----------------------------------------
MemTotal node_memory_MemTotal_bytes
MemFree node_memory_MemFree_bytes
MemAvailable node_memory_MemAvailable_bytes
Buffers node_memory_Buffers_bytes
Cached node_memory_Cached_bytes
SwapTotal node_memory_SwapTotal_bytes
SwapFree node_memory_SwapFree_bytes
AnonPages node_memory_AnonPages_bytes
Dirty node_memory_Dirty_bytes
Slab node_memory_Slab_bytes
/proc/vmstat field node_exporter metric
--------------------- ----------------------------------------
pswpin node_vmstat_pswpin
pswpout node_vmstat_pswpout
pgmajfault node_vmstat_pgmajfault
oom_kill node_vmstat_oom_kill
/proc/pressure/memory node_exporter metric
--------------------- ----------------------------------------
some avg10=0.00 node_pressure_memory_waiting_seconds_total
full avg10=0.00 node_pressure_memory_stalled_seconds_total
MemAvailable is the kernel’s own estimate of memory that can be
made available to a new workload without swapping. It is
calculated as MemFree + reclaimable cache + reclaimable slab. The
exact formula is in the kernel source; the value is conservative.
PSI (/proc/pressure/memory) is the kernel’s stall instrumentation.
Some line reports the fraction of time at least one task is
waiting for memory; Full line reports the fraction of time the
entire system is waiting. PSI is the only metric that directly
answers “is something in the system memory-starved right now?”
Memory Pressure (USE: saturation)
=================================
mem.total = 64 GiB
mem.avail = 4.2 GiB - the kernel can reclaim 4.2 GiB
without swapping. Below this,
new allocations are at risk.
psi/some avg10 = 0.4 - 40% of the last 10s, at least one
task was waiting for memory.
This is the on-host signal.
vmstat.pswpin = 12/s - the host is swapping in pages from
swap. This is the saturation metric.
vmstat.oom_kill = 0 - the killer has not fired yet
(this counter is cumulative).
Under the hood
How to configure it
node_exporter collects memory metrics by default. The flags that
matter are the filesystem excludes (to avoid scraping tmpfs and
overlay as memory consumers) and the processes collector (which
captures process-level RSS):
# /etc/systemd/system/node_exporter.service.d/override.conf
[Service]
ExecStart=
ExecStart=/opt/node_exporter/node_exporter \
--web.listen-address=0.0.0.0:9100 \
--collector.systemd \
--collector.processes \
--collector.filesystem.mount-points-exclude=^/(dev|proc|sys|run|var/lib/docker/.+)($|/) \
--collector.filesystem.fs-types-exclude=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|nsfs|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|selinuxfs|squashfs|sysfs|tracefs)$ \
--collector.vmstat.fields=^(oom_kill|pgmajfault|pswpin|pswpout|nr_free_pages|nr_inactive_anon|nr_active_anon|nr_inactive_file|nr_active_file|nr_dirty|nr_writeback)$
The collected vmstat.fields are the production baseline. The
ones above cover OOM kills, page faults, swap I/O, and the
per-zone page counts. The full list is longer; the bare minimum
is oom_kill, pgmajfault, pswpin, pswpout.
Reload the unit:
# SEVERITY: SERVICE-IMPACT
sudo systemctl daemon-reload
sudo systemctl restart node_exporter
For OOM kills, the kernel ring buffer is the source of truth.
node_exporter does not collect it; the production path is
journalctl -k plus a log shipper (Loki, etc.) for retention.
To make OOM kills generate a syslog line, set the kernel printk level on RHEL 9 (the default is sufficient) and ensure the systemd journal is forwarded:
# SEVERITY: READ-ONLY
journalctl -k --since '-1h' | grep -i 'out of memory'
Expected output (illustrative):
Jan 14 02:51:13 app-prod-12 kernel: Out of memory: Killed process 18421 (java)
total-vm:268435456kB, anon-rss:18350080kB, file-rss:5242880kB
How to validate it
The first check is that the metrics are present:
# SEVERITY: READ-ONLY
curl -s http://localhost:9100/metrics | grep -E 'node_memory_MemAvailable_bytes|node_vmstat_oom_kill|node_pressure_memory_waiting_seconds_total'
Expected output (illustrative):
node_memory_MemAvailable_bytes 4.2e+09
node_vmstat_oom_kill 0
node_pressure_memory_waiting_seconds_total 0.0008
The second check is the dashboard query. The production-grade panel is:
100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)
This is the used fraction by the MemAvailable definition. The
common variant, 100 * (1 - node_memory_MemFree_bytes / node_memory_MemTotal_bytes), is the dashboard error
this lesson warns against.
The third check is the alert rule. The SLO-class rule for memory pressure is:
# /etc/prometheus/rules/memory.rules.yml
groups:
- name: memory.pressure
interval: 30s
rules:
- alert: HostMemoryPressureHigh
expr: |
(
node_pressure_memory_waiting_seconds_total
/
node_pressure_memory_waiting_seconds_total + 0.0001
) > 0.20
for: 10m
labels:
severity: ticket
team: platform
annotations:
summary: 'Memory pressure on {{ $labels.instance }} above 20% for 10m'
description: 'PSI some avg10 is {{ $value | humanizePercentage }}. Host is stalling on memory. Inspect MemAvailable and OOM count.'
runbook_url: 'https://runbooks.example.com/host/memory-pressure'
The fourth check is the kernel log for any recent OOM kills:
# SEVERITY: READ-ONLY
journalctl -k --since '-24h' | grep -c 'Out of memory'
A non-zero count is a hard incident. The number should match
node_vmstat_oom_kill. If they diverge, the metric is being
overcounted or the journal is being rotated before the metric
captures it.
How it can fail
Six failure modes appear repeatedly in production.
- Used-vs-available confusion. The dashboard reports
MemTotal - MemFreeas “used.” A host can be at 75% used by that calculation and at 12% used byMemAvailable. Symptom: the team calls the host “fine” while the kernel is reclaiming cache and the application is slowing down. The fix is to addMemAvailableto the panel and to subtract it fromMemTotal. - The OOM killer fires, but the metric was not captured.
The metric
node_vmstat_oom_killis present but the journal line is not. Symptom: the metric says zero kills,journalctlsays one. The fix is to verify the journal is forwarded to the log platform before relying on the metric. - PSI is missing. The kernel is older than 4.20 or the
sysctl
kernel.psi_enabled=0is set. Symptom: thenode_pressure_*series are absent. The fix is to enable PSI or substitutenode_load5andnode_vmstat_pswpinas saturation proxies. - Swap is enabled when it should not be. A database host
has swap on a fast SSD; the kernel swaps instead of OOM killing.
Symptom: latency spikes during period memory peaks, no OOM
kill. The fix is to disable swap (
swapoff -aand remove from/etc/fstab) for hosts whose memory budget is sized correctly. - Memory cgroup limits are wrong. A container host runs
containers with
memory.limit_in_bytesbelow the application’s working set. Symptom: the cgroup kills processes, but the host reports plenty of free memory. The fix is to size the cgroup to the application’s working set + headroom. - The kernel overcommit is set aggressively.
vm.overcommit_memory=1allows all allocations; the system never reports pressure until the page allocator itself fails. Symptom: a sudden OOM kill on a host that was at 30% used by the panel. The fix is to setvm.overcommit_memory=0(default) andvm.overcommit_ratio=to a value the host can back with swap.
How to troubleshoot it
The diagnostic order when a host is slow and memory is the suspect:
- Inspect
MemAvailableandMemTotal. Is the host at 10% available or 1% available? - Inspect
node_pressure_memory_waiting_seconds_total. Is the PSI “some” avg10 above 5%? - Inspect
node_vmstat_pswpinandnode_vmstat_pswpout. Is the host swapping? - Inspect
node_vmstat_oom_kill. Has the OOM killer fired? - Inspect
journalctl -k --since '-1h' | grep -i 'out of memory'for the most recent kill. Correlate with the application logs. - Inspect the per-process RSS via
top -o %MEMorps -eo pid,rss,comm --sort -rss | head -20. - Inspect the cgroup if the host is a container host:
cat /sys/fs/cgroup/memory/memory.usage_in_bytesandmemory.limit_in_bytes.
Each step confirms or rules out a layer. Step 1 is the capacity question. Step 2 is the pressure question. Step 3 is the saturation question. Step 4 is the error question. The distinction is the USE method applied to memory.
Security implications
/proc/meminfo and /proc/vmstat are world-readable. The
metrics node_exporter exposes do not contain process names,
process owners, or memory contents. The PSI values are aggregate
fractions. The PII surface is low.
The cgroup path is more sensitive. Per-container memory usage is a useful metric for the operator but can leak information about which tenants are running and how much they consume. The production baseline is to expose aggregate metrics to the scraper and to require authentication for the per-cgroup breakdown.
The kernel ring buffer is not rate-limited. A misconfigured log shipper can DoS the host by writing OOM lines to a slow disk. The fix is to ship the kernel ring via the journal and to use the journal’s rate-limiting.
Performance implications
The memory metrics on node_exporter are counters and gauges
on a finite, bounded set of fields. The cardinality is small
(tens of series per host). The cost is reading /proc/meminfo
and /proc/vmstat on every scrape, which is a few hundred
microseconds. The scrape interval is not a problem.
The PSI collector reads /proc/pressure/memory on every
scrape. The cost is a few hundred microseconds; the kernel
updates the value every two seconds. The collector is cheap.
The per-process collector (top -o %MEM or ps) is expensive
on hosts with many processes. The node_exporter processes
collector walks /proc/<pid>/ and emits aggregate counts,
not per-process memory. Per-process memory on a high-process
host is collected by a dedicated exporter or by an internal
metric on the application.
Production guidance
- Alert on
MemAvailableand PSI, not onMemFree. The two metrics are not the same. - SLO the panel on memory pressure, not on memory usage. The user feels the pressure, not the usage.
- Disable swap on hosts whose memory is sized correctly. Swap is a safety net for sizing errors, not a working-set solution.
- Set explicit
vm.overcommit_memoryper host class. Heuristic is fine for general-purpose hosts;2for hosts running untrusted workloads;1for database hosts whose memory is sized to the working set. - Forward the kernel ring buffer to the log platform. The
node_vmstat_oom_killcounter is enough for alerting; the log line is what the postmortem needs.
Verification
You should now be able to answer:
- What is the difference between
MemTotal - MemFreeandMemAvailable, and why does the dashboard error this? - How does the kernel signal that something is waiting for
memory, and how does
node_exporterexpose that? - What is the canonical source for OOM kills, and how does the
metric
node_vmstat_oom_killdiffer from the journal line? - Why is swap on a database host usually a misconfiguration?
- What is the diagnostic order when a host is slow and memory is the suspect?
Quiz
Knowledge check · 8 questions
Q1. Which metric captures the kernel estimate of memory a new workload could allocate without swapping?
Q2. Which node_exporter metric signals that at least one task is currently waiting for memory?
Q3. A host can report 95% used by the MemTotal minus MemFree formulation and still be healthy by MemAvailable
Q4. A database host with swap on a fast SSD starts swapping during slow queries. The right production fix is:
Q5. Name the two sources the operator should check to confirm an OOM kill happened on a host.
Q6. Which of these are valid saturation signals for memory on a Linux host?
Q7. What is the canonical response when a host has fired OOM kills twice in seven days?
Q8. A container host has a cgroup OOM kill. The host dashboards show plenty of free memory. The most likely cause is:
Passing score: 75%. Answers are checked in this browser.