Linux Memory Accounting for Small VPS Hosts: MemAvailable vs Free, and Every Bit of Overhead You Forgot

Every “is my VPS out of memory?” decision rests on two numbers from /proc/meminfo, and most operators read the wrong one. MemFree tells you how much RAM is completely unused — which on a healthy Linux host is always small, because the kernel spends spare memory on page cache. MemAvailable is the kernel’s own estimate of how much can be reclaimed under pressure without swapping. This article builds a repeatable budget: what the kernel costs, what your services cost in RSS and PSS, and how to prove the accounting adds up.

Why MemFree Is the Wrong Number

grep -E 'MemTotal|MemFree|MemAvailable|Cached|Buffers|SReclaimable|Shmem|Slab' /proc/meminfo

# MemAvailable is already computed by the kernel at mm/vmscan.c level.
# Reproduce it intentionally to understand the formula:
awk '/MemFree/{f=$2} /Cached/{c=$2} /SReclaimable/{s=$2}
     /Shmem/{sh=$2} END{printf "approx available: %.0f MB\n", (f + c + s - sh)/1024}' /proc/meminfo

On a 1 GB VPS, MemFree of 60 MB with Cached of 500 MB is a well-utilised host, not a struggling one. Alert on MemAvailable / MemTotal < 15% sustained, combined with swap-in activity from vmstat:

# the only line that matters for memory pressure
vmstat 5 12   # columns: si (swap in) so (swap out)

# per-cgroup pressure stall information — best early-warning signal
cat /sys/fs/cgroup/system.slice/memory.pressure
# some avg10=0.00 avg60=0.05 avg300=0.10 total=182340
# full avg10=0.00 avg60=0.02 avg300=0.05 total=91211

Building the Overhead Budget

RSS double-counts shared pages. PSS divides each shared page by the number of mappers, which is the only honest way to attribute memory to services. Use PSS for budgeting:

# per-process PSS, memory in KB, largest first
sudo smem -tk --sort=pss | head -20

# or without smem, from /proc directly
for p in $(ls /proc | grep -E '^[0-9]+$'); do
  pss=$(awk '/^Pss:/{s+=$2} END{print s+0}' /proc/$p/smaps_rollup 2>/dev/null)
  [ -n "$pss" ] && [ "$pss" -gt 0 ] && \
    printf "%8d KB  %s\n" "$pss" "$(tr -d '\0' /dev/null)"
done | sort -rn | head -20

Now account for the parts that never show up under a process name:

ItemTypical cost on a 1 GB VPSWhere to see it
Kernel slab80–200 MBSlab in /proc/meminfo, slabtop
Page tables20–60 MBPageTables
Kernel stack + task structs~16 KB × threadsKernelStack, thread count
tmpfs / /dev/shmOften unboundedShmem, df -h /dev/shm
Socket / conntrack buffers5–40 MB at high connection countstcp_mem, nf_conntrack
Hugepages2 MB per reserved pageHugePages_Total
Fragmentation reserve~5% unusable in practiceindirect
# find the slab hogs
sudo slabtop -o -s c | head -15

# page table cost scales with mapped memory, not with process count alone
grep -E 'PageTables|KernelStack|Shmem|Slab|SUnreclaim' /proc/meminfo

# thread count is a memory cost multiplier
ps -eLf | wc -l

# tmpfs can silently eat gigabytes
df -h | grep -E 'tmpfs|shm'

Practical Budget on a 1 GB VPS

Starting from 1024 MB of MemTotal, a realistic allocation that avoids OOM without wasting RAM:

MemTotal           1024 MB
kernel + slab       160 MB   (fixed tax)
page tables          30 MB
nginx (PSS)          45 MB
php-fpm 4 workers   180 MB   (4 x 45 MB PSS, shared opcache counted once)
MariaDB            280 MB   (innodb_buffer_pool 128M + per-thread overhead)
redis               25 MB
systemd/journald    40 MB
sshd + cron + misc  35 MB
-----------------------------
allocated          795 MB
headroom           229 MB   (page cache + burst, target >=15%)

If your accounting shows less than 15% headroom, either reduce a line item or accept swap as a safety net rather than a strategy. A methodical reduction of the largest consumers — buffer pools and worker counts — is covered in depth in the memory-focused tuning guides on virtualserversvps.com. For workloads that genuinely need more than 1 GB, re-examine whether a larger plan is cheaper than engineering around a hard ceiling: the plan comparison on virtualserversvps.com lays out the break-even point.

Verify the Accounting Adds Up

# 1. sum PSS across all processes
total_pss=$(for p in /proc/[0-9]*; do
  awk '/^Pss:/{s+=$2} END{print s+0}' "$p/smaps_rollup" 2>/dev/null; done \
  | paste -sd+ | bc)
echo "total PSS: $((total_pss/1024)) MB"

# 2. kernel-side consumption
awk '/MemTotal/{t=$2} /MemFree/{f=$2} /Cached/{c=$2} /Slab/{s=$2}
     END{printf "used-non-cache: %.0f MB\n", (t-f-c)/1024}' /proc/meminfo

# 3. the two should reconcile within ~5%; a large gap means kernel
#    allocations you are not accounting for (usually slab fragmentation)
sudo slabtop -o -s c | head -5

Guardrails

  • Alert on MemAvailable, never on MemFree.
  • Budget services by PSS from smaps_rollup, and treat the thread count as a multiplier.
  • Audit Shmem and tmpfs monthly; they grow silently.
  • Set explicit cgroup memory.high on each service so one component cannot trigger a host-wide OOM kill.
  • Validate with a reconciliation script and store the output — a budget you never check is a guess.

Memory planning on a small VPS is arithmetic, not intuition. Once MemAvailable and PSS-based accounting are both in place, “why did my server get OOM-killed” becomes a solved query rather than a mystery.

Leave a Reply