{
  "slug": "kswapd0",
  "url": "/process/kswapd0/",
  "name": "kswapd0",
  "kind": "kernel-thread",
  "package": null,
  "binary": "kernel thread (PPID 2, no executable)",
  "config": [],
  "summary": "Kernel thread that reclaims memory in the background when a zone's free pages fall below the low watermark; one per NUMA node, kswapd0 is node 0.",
  "symptoms": [
    "kswapd0 pinned at 50–100% CPU while 'available' memory in free -h is close to zero",
    "si/so columns in vmstat are non-zero: pages are being swapped in and out (swap thrash)",
    "high iowait, everything feels sluggish, the real hog is a process with a huge RSS",
    "/proc/pressure/memory shows 'some' avg10 well above 0 — tasks are stalling on memory"
  ],
  "safe_to_kill": "no",
  "safe_to_kill_note": "It is a kernel thread: kill sends a signal to nothing, and even if you could stop it, allocations would fall into direct reclaim and stall every process instead. kswapd0 eating CPU is the symptom of memory pressure, never its cause. Find what holds the memory, give the box swap or RAM, or move the watermarks so reclaim starts earlier.",
  "diagnose": [
    {
      "cmd": "free -h",
      "why": "is 'available' near zero, and is swap in use at all? No swap plus full RAM means kswapd can only evict page cache, over and over"
    },
    {
      "cmd": "vmstat 1 5",
      "why": "si/so = pages swapped in/out per second, wa = iowait. Non-zero si/so while kswapd0 is busy is swap thrash, not a kernel bug"
    },
    {
      "cmd": "cat /proc/pressure/memory",
      "why": "PSI: 'some' = at least one task stalled on memory, 'full' = all non-idle tasks stalled; avg10/avg60/avg300 are percentages of recent wall time, total is µs. Needs kernel ≥ 4.20 built with CONFIG_PSI — 'No such file' means the kernel lacks it (some cloud and microVM kernels do)"
    },
    {
      "cmd": "grep -E '^(pgscan_kswapd|pgsteal_kswapd|pgscan_direct|pgsteal_direct|allocstall_normal|allocstall_movable|kswapd_low_wmark_hit_quickly)' /proc/vmstat",
      "why": "background (kswapd) vs direct reclaim counters; run it twice a few seconds apart. Growing allocstall_* means processes are stalling in direct reclaim because kswapd cannot keep up"
    },
    {
      "cmd": "ps -eo pid,user,rss,pmem,comm --sort=-rss | head -15",
      "why": "who actually holds the memory (RSS in KiB). This list, not kswapd0, is what you act on"
    },
    {
      "cmd": "grep -E '^Node|^  pages free|^        (min|low|high) ' /proc/zoneinfo",
      "why": "per-zone watermarks in 4 KiB pages: kswapd wakes when free drops below 'low' and sleeps again above 'high'; below 'min' allocations do direct reclaim"
    },
    {
      "cmd": "sysctl vm.swappiness vm.watermark_scale_factor vm.min_free_kbytes",
      "why": "the three knobs that decide how early kswapd starts and whether it prefers dropping page cache or swapping anonymous pages"
    },
    {
      "cmd": "swapon --show",
      "why": "empty output = no swap device. On a small VPS that alone explains a permanently busy kswapd0"
    }
  ],
  "fixes": [
    {
      "title": "Fix the memory hog, not the messenger.",
      "cmd": "ps -eo pid,user,rss,pmem,args --sort=-rss | head -5",
      "note": "A database buffer pool sized for a bigger box, a JVM heap, a leaking worker: shrink it, restart it, or move it. kswapd0 goes quiet by itself when free memory climbs back over the high watermark."
    },
    {
      "title": "Give the kernel somewhere to put cold pages: add a swap file.",
      "cmd": "sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile",
      "note": "Persist with a line in /etc/fstab: /swapfile none swap sw 0 0. Swap is not a RAM substitute; it lets reclaim evict genuinely idle anonymous memory instead of thrashing the page cache. On a copy-on-write filesystem such as Btrfs the file must be created with chattr +C first; see the distribution's swap page."
    },
    {
      "title": "Wake kswapd earlier so it reclaims in the background instead of stalling allocations.",
      "cmd": "sudo sysctl vm.watermark_scale_factor=200",
      "note": "Units are fractions of 10,000 of memory: default 10 (0.1%), maximum 3000. Raise it when allocstall_* counters grow or kswapd_low_wmark_hit_quickly is high. Persist in /etc/sysctl.d/90-reclaim.conf."
    },
    {
      "title": "Tell reclaim what to evict first with swappiness.",
      "cmd": "sudo sysctl vm.swappiness=10",
      "note": "Default 60, range 0–200. Lower keeps anonymous memory and drops page cache first (databases that manage their own cache); 100 treats both as equal cost; values above 100 are legitimate with zram/zswap, the kernel docs give 133 as an example for swap twice as fast as the filesystem."
    },
    {
      "title": "Let a userspace OOM daemon end the thrash before the kernel OOM killer does.",
      "cmd": "systemctl is-active systemd-oomd",
      "note": "systemd-oomd (Ubuntu 22.04+ ships it, enabled on Desktop, usually inactive on Server) and earlyoom watch PSI and kill the hog at a threshold you choose, instead of letting kswapd0 spin for ten minutes first."
    },
    {
      "title": "On kernels with Multi-Gen LRU, protect the working set.",
      "cmd": "cat /sys/kernel/mm/lru_gen/enabled",
      "note": "0x0007 means MGLRU is on, 0x0000 means compiled in but switched off (write y to enable), 'No such file' means the kernel lacks CONFIG_LRU_GEN (kernel ≥ 6.1; WSL2 lacks it, a 6.18 cloud guest shipped it off). min_ttl_ms keeps the last N ms of working set in memory and prefers an OOM kill over thrashing; 1000 is the documented starting point."
    }
  ],
  "harden": [],
  "impostors": [
    {
      "cmd": "ps -o pid,ppid,user,comm -C kswapd0",
      "why": "a real kswapd0 is owned by root with PPID 2 (kthreadd). Any other parent, or a copy under a normal user, is a process that merely calls itself kswapd0 — cryptominers have used exactly this name since 2019"
    },
    {
      "cmd": "readlink /proc/$(pgrep -x kswapd0 | head -1)/exe || echo 'no exe: genuine kernel thread'",
      "why": "kernel threads have no executable; a path under /tmp, /dev/shm or a home directory is malware wearing the name"
    }
  ],
  "listens": [],
  "distros": {
    "ubuntu": "22.04+ ships systemd-oomd (active on Desktop, inactive on Server unless enabled); PSI is on by default; MGLRU is enabled on the generic 6.x kernels but not on every cloud or WSL2 kernel — check /sys/kernel/mm/lru_gen/enabled.",
    "any": "Cloud images often ship with zero swap. A 1–2 GiB swap file plus vm.swappiness=10 is the usual first fix on a small VPS."
  },
  "applies_to": "Linux 2.6 and later. /proc/pressure needs kernel 4.20+ with CONFIG_PSI; Multi-Gen LRU needs 6.1+ with CONFIG_LRU_GEN. Per-cgroup pressure is in memory.pressure on cgroup v2.",
  "see_also": [
    "kcompactd",
    "systemd-oomd",
    "kworker"
  ],
  "sources": [
    {
      "title": "Kernel docs — Concepts overview: reclaim, watermarks, direct reclaim",
      "url": "https://docs.kernel.org/admin-guide/mm/concepts.html",
      "fetched": "2026-10-20T00:00:00.000Z"
    },
    {
      "title": "Kernel docs — sysctl/vm: swappiness, watermark_scale_factor, min_free_kbytes",
      "url": "https://docs.kernel.org/admin-guide/sysctl/vm.html",
      "fetched": "2026-10-20T00:00:00.000Z"
    },
    {
      "title": "Kernel docs — PSI: pressure stall information",
      "url": "https://docs.kernel.org/accounting/psi.html",
      "fetched": "2026-10-20T00:00:00.000Z"
    },
    {
      "title": "Kernel docs — Multi-Gen LRU admin guide",
      "url": "https://docs.kernel.org/admin-guide/mm/multigen_lru.html",
      "fetched": "2026-10-20T00:00:00.000Z"
    }
  ],
  "tested_on": "Ubuntu 22.04 · kernel 6.6 (WSL2) and Ubuntu 26.04 · kernel 6.18 (Firecracker VM)",
  "updated": "2026-10-20T00:00:00.000Z",
  "changelog": [
    {
      "date": "2026-10-20T00:00:00.000Z",
      "note": "first version. Every command was run on both test kernels; both run the distro in a PID namespace that hides kernel threads, so the ps -C kswapd0 / readlink impostor checks ran against an empty result and still want a pass on a bare-metal or full-VM kernel. The 6.18 guest had no /proc/pressure (built without CONFIG_PSI) and MGLRU compiled in but off (0x0000) — both now noted in the card"
    }
  ]
}