How the Kernel Decides to Swap

The kernel's memory reclaim path is governed by vmscan, which balances two types of reclaim: file-backed pages (page cache) and anonymous pages (process heap, stack, mmap'd regions without a backing file). Swappiness is not a direct threshold - it is a weight that tilts reclaim preference toward file cache or anonymous memory.

When swappiness is 60 (the default), the kernel is moderately willing to swap anonymous pages alongside reclaiming file cache. At 10, it heavily prefers to shrink the page cache before touching anonymous memory. At 0 under kernels older than 3.5, swap was completely disabled for anonymous pages; from 3.5 onward, 0 means 'swap only when absolutely necessary', not 'never swap'.

The actual formula used in mm/vmscan.c weights the two LRU lists. Simplified: the anonymous reclaim pressure is scaled by swappiness/200 relative to file reclaim. Changing swappiness from 60 to 10 on a system where the page cache is genuinely reusable will reduce anonymous evictions substantially without increasing I/O - you are just choosing to discard cached file data instead of stalling application threads waiting for swapped pages to come back in.

Check current memory pressure before touching anything:

# See current swappiness
cat /proc/sys/vm/swappiness

# See what's currently swapped and swap usage
free -h
swapon --show

# Detailed per-process swap usage
for pid in /proc/[0-9]*; do
  comm=$(cat $pid/comm 2>/dev/null)
  swap=$(grep VmSwap $pid/status 2>/dev/null | awk '{print $2}')
  [ "$swap" -gt 0 ] 2>/dev/null && echo "$swap kB $comm ($(basename $pid))"
done | sort -rn | head -20

Setting swappiness: Runtime and Persistent

Runtime changes take effect immediately without a reboot. The sysctl interface writes directly into the kernel's vm namespace. Persistent changes require a sysctl.d configuration file - do not modify /etc/sysctl.conf directly on systems using systemd-based distros, because package updates can clobber it.

For general-purpose servers and workstations, 10-20 is a sensible range. For dedicated database servers (PostgreSQL, MySQL, MongoDB), we run 5-10. For containers running on a host where swap pressure is managed at the cgroup level, consider 1. We do not recommend 0 on production machines - at swappiness=0, the kernel will OOM-kill processes rather than swap during brief spikes, which is worse than accepting a short swap hit.

# Apply immediately (survives until next reboot)
sysctl -w vm.swappiness=10

# Persist across reboots (systemd-based distros)
cat > /etc/sysctl.d/99-swap.conf << 'EOF'
vm.swappiness = 10
vm.vfs_cache_pressure = 50
EOF

# Reload without rebooting
sysctl --system

# Verify
sysctl vm.swappiness

vfs_cache_pressure: The Other Half of the Equation

Swappiness controls anonymous vs file reclaim weighting, but vfs_cache_pressure controls how aggressively the kernel reclaims memory from the dcache and inode cache. The default is 100, which reclaims dcache and inode memory at roughly the same rate as pagecache. Lower values make the kernel keep directory entry and inode caches longer.

On a server with millions of small files - a mail server, an object store, or a Node.js app with deep node_modules trees - setting vfs_cache_pressure to 50 reduces the inode lookup cost noticeably. On a database server where filesystem metadata access is minimal, leaving it at 100 is fine.

We ran a benchmark on our test server using fio to simulate 80% random reads, 20% writes across 500,000 files while watching /proc/meminfo DropCacheable and vmstat si/so columns:

# Watch swap in/out rates in real time (1-second intervals)
vmstat 1 | awk 'NR==1{print} NR>1{if ($7>0 || $8>0) print}'

# Or use dstat if available
dstat -vm 1

# Check dcache and inode memory separately
grep -E 'Slab:|SReclaimable:|SUnreclaim:' /proc/meminfo

# See slab usage breakdown
slabtop -o | head -30
// advertisement

What zswap Actually Does

zswap is a compressed in-memory cache that sits between anonymous page eviction and the actual swap device. When the kernel decides to swap out a page, zswap intercepts it, compresses it, and stores the compressed page in a dedicated memory pool. If the page is faulted back in before the pool fills, it decompresses from RAM - no disk I/O at all. Only when the zswap pool itself is full does it start writing pages to the real swap device in LRU order.

The compression ratio for typical process memory (heap data, stack frames, zeroed pages) runs 2:1 to 4:1 in our testing. A 4 GB zswap pool effectively absorbs what would otherwise be 8-16 GB of swap writes. On an NVMe with 3 GB/s sequential throughput, this matters less than on a spinning disk or a network-attached swap device, but even on NVMe, eliminating swap I/O reduces latency variance significantly.

zswap is built into the kernel as a module since Linux 3.11. On 6.x kernels it is compiled in (not a loadable module) on most distros. It is disabled by default on most distributions except Fedora 40+ and recent openSUSE Tumbleweed builds.

Two compressor backends are available: lzo (fast, moderate compression) and lz4 (faster, slightly lower compression ratio). Since Linux 6.2, zstd is also available and provides the best ratio at acceptable CPU cost. The pool allocator is either z3fold (handles compression ratios better) or zbud (simpler, slightly lower overhead). For 2026 production kernels, use zstd + z3fold.

# Check if zswap is available and its current state
cat /sys/module/zswap/parameters/enabled

# List available compressors
cat /sys/module/zswap/parameters/compressor

# List available pool allocators
cat /sys/module/zswap/parameters/zpool

# Check current zswap stats
grep '' /sys/kernel/debug/zswap/* 2>/dev/null
# or on newer kernels:
cat /proc/sys/vm/zswap_enabled 2>/dev/null

Enabling and Configuring zswap

Enable zswap via kernel command line parameters or at runtime through the sysfs interface. The sysfs method works immediately but does not persist after reboot. For production systems, add the parameters to the bootloader configuration.

The max_pool_percent parameter controls what fraction of total RAM zswap can use. The default is 20%. On a 32 GB server, that is 6.4 GB of compressed storage. We set it to 25% on memory-heavy workloads and have not seen the pool itself become a memory pressure source, because zswap will start evicting its own pool to the swap device before it consumes the full allocation.

When zswap is active and the pool fills, the same_filled_pages_enabled feature allows zswap to store zero-filled pages (common in freshly malloc'd but unwritten memory) with essentially zero memory cost by mapping them all to a single page. Enable it.

# Enable zswap at runtime immediately
echo 1 > /sys/module/zswap/parameters/enabled
echo zstd > /sys/module/zswap/parameters/compressor
echo z3fold > /sys/module/zswap/parameters/zpool
echo 25 > /sys/module/zswap/parameters/max_pool_percent
echo 1 > /sys/module/zswap/parameters/same_filled_pages_enabled

# Persistent: add to GRUB_CMDLINE_LINUX in /etc/default/grub
# Open the file and add these parameters to the existing line:
# zswap.enabled=1 zswap.compressor=zstd zswap.zpool=z3fold zswap.max_pool_percent=25

# Example sed command to add parameters non-destructively:
sed -i 's/GRUB_CMDLINE_LINUX="/GRUB_CMDLINE_LINUX="zswap.enabled=1 zswap.compressor=zstd zswap.zpool=z3fold zswap.max_pool_percent=25 /' /etc/default/grub

# Rebuild GRUB config
# On Debian/Ubuntu:
update-grub

# On RHEL/AlmaLinux/Rocky:
grub2-mkconfig -o /boot/grub2/grub.cfg

Monitoring zswap Effectiveness

After enabling zswap, the most important metrics are pool size, compression ratio, and writeback rate. If writeback rate is zero or near zero, zswap is absorbing all swap pressure in RAM - ideal. If writeback is high, either the pool is too small or the system is genuinely memory starved and needs more RAM or better application-level tuning.

The debugfs interface provides the richest data. On kernels where debugfs is mounted at /sys/kernel/debug, zswap statistics live under /sys/kernel/debug/zswap/. On kernels 6.4+ some stats also appear in /proc/vmstat as zswap_* fields.

We wrote a small monitoring script that runs every 60 seconds via cron and logs pool utilization to a flat file. For DevOps teams building automated observability pipelines, tools like taskbotshub.ai can ingest these custom metrics and trigger alerts when zswap writeback rate crosses a threshold, integrating with Prometheus exporters or custom webhook endpoints without writing bespoke glue code.

# Quick zswap stats summary
echo '=== zswap stats ==='
for f in /sys/kernel/debug/zswap/*; do
  printf '%-35s %s\n' "$(basename $f):" "$(cat $f)"
done

# Calculate effective compression ratio
stored=$(cat /sys/kernel/debug/zswap/stored_pages)
pool_kb=$(cat /sys/kernel/debug/zswap/pool_total_size)
page_size=$(getconf PAGESIZE)
raw_kb=$(( stored * page_size / 1024 ))
echo "Raw data would be: ${raw_kb} kB"
echo "Pool is using:     ${pool_kb} kB"
echo "Compression ratio: $(echo "scale=2; $raw_kb / $pool_kb" | bc):1"

# Watch writeback events in vmstat
watch -n2 'grep zswap /proc/vmstat'
// advertisement

Swap File vs Swap Partition in 2026

On systems using btrfs, a swap file requires the nodatacow and nodatasum attributes and must be on a non-snapshotted subvolume. From Linux 6.1, btrfs swap file support is solid - the historical instability is resolved. On ext4 and xfs, swap files have been reliable for years and perform identically to swap partitions on modern kernels.

For most installations today, a swap file is more operationally flexible. You can resize it without repartitioning, add multiple swap files with different priorities, and snapshot them separately from application data. The performance difference between a swap file and a swap partition on the same NVMe device is unmeasurable in our testing.

Set swap file priority using the -p flag to swapon. Higher numbers mean higher priority. If you have two swap devices, put the faster one at higher priority:

# Create an 8 GB swap file
dd if=/dev/zero of=/swapfile bs=1M count=8192 status=progress
chmod 600 /swapfile
mkswap /swapfile
swapon -p 10 /swapfile

# Persist in /etc/fstab
echo '/swapfile none swap sw,pri=10 0 0' >> /etc/fstab

# For btrfs - must set attributes before mkswap
chattr +C /swapfile   # nodatacow
btrfs property set /swapfile compression none

# Verify swap priority
swapon --show

cgroup v2 Memory Limits and Swap

On systems running cgroup v2 (the default on all major distros since 2023), swap behaviour is controlled per-cgroup using memory.swap.max and memory.swap.current. This is important for containerised workloads: a container with memory.max=2G and memory.swap.max=0 cannot swap at all, even if the host has swap available. Setting memory.swap.max to a finite non-zero value allows the container to use swap up to that limit.

Note that cgroup v2 swap accounting counts total memory plus swap usage against memory.swap.max, not swap alone. If memory.max is 2G and memory.swap.max is 4G, the container can use 2G RAM and 2G swap (total 4G). This is the combined limit model, distinct from cgroup v1's memsw.limit_in_bytes.

The host-level swappiness sysctl applies globally, but you can override per-cgroup using the memory.swappiness file in cgroup v1. In cgroup v2, per-cgroup swappiness is not exposed directly - the global sysctl governs it. If you need different swap aggressiveness between workloads in cgroup v2, use memory.swap.max as the control surface instead.

# Check cgroup v2 memory and swap settings for a systemd service
systemctl show postgresql.service | grep -E 'MemoryMax|MemorySwap'

# Set swap limit for a service via systemd drop-in
mkdir -p /etc/systemd/system/postgresql.service.d/
cat > /etc/systemd/system/postgresql.service.d/memory.conf << 'EOF'
[Service]
MemoryMax=24G
MemorySwapMax=2G
EOF

systemctl daemon-reload
systemctl restart postgresql

# Verify live cgroup stats
cat /sys/fs/cgroup/system.slice/postgresql.service/memory.swap.current
cat /sys/fs/cgroup/system.slice/postgresql.service/memory.swap.max

Putting It Together: Recommended Configs by Workload

Different workloads need different tuning profiles. Below are the three configurations we actually deploy.

For PostgreSQL or MySQL database servers: swappiness=5, vfs_cache_pressure=50, zswap enabled with zstd compressor, max_pool_percent=20. Keep swap available (8-16 GB) but make the kernel very reluctant to use it. zswap absorbs brief spikes without touching disk.

For application servers (Java, Node.js, Python services): swappiness=15, vfs_cache_pressure=75, zswap enabled with lz4 for lower CPU cost when compression hits frequently under load. These services have more compressible heap patterns than databases.

For Kubernetes worker nodes: swappiness=10 at the host level, enable NodeSwap feature gate if running K8s 1.30+, set memory.swap.max per pod class via LimitRange or ResourceQuota. zswap helps absorb bursty container workloads. Do not disable swap on K8s nodes in 2026 - the KIP-1029 swap support in 1.30+ makes it genuinely useful for burstable pods.

For development workstations with 16-32 GB RAM: swappiness=10-20, zswap with lz4, max_pool_percent=30. Developers running multiple VMs, Docker stacks, and a browser with 200 tabs benefit enormously from zswap - we measured 40% reduction in perceptible 'system freeze under load' events in a 3-month informal test across 8 developer machines.

# Database server full config snippet for /etc/sysctl.d/99-vm-tuning.conf
cat > /etc/sysctl.d/99-vm-tuning.conf << 'EOF'
# Swap tuning
vm.swappiness = 5
vm.vfs_cache_pressure = 50

# Dirty page writeback - avoid large bursts of I/O
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
vm.dirty_expire_centisecs = 1500
vm.dirty_writeback_centisecs = 500

# Reduce memory overcommit aggressiveness on DB servers
vm.overcommit_memory = 2
vm.overcommit_ratio = 80
EOF

sysctl --system
// advertisement

Benchmarking Your Changes

Do not guess whether your changes helped - measure. The canonical approach uses two tools: stress-ng to apply controlled memory pressure, and vmstat or /proc/vmstat to count actual swap I/O events before and after.

Record baseline swap events, apply memory pressure, record again. Compare si (swap in) and so (swap out) columns in vmstat output. With zswap enabled and a correctly tuned swappiness, so events should drop to near zero for workloads that fit in RAM plus the zswap pool.

For production validation, use sar from the sysstat package to capture historical swap I/O at 1-minute intervals. If you are running automated deployment pipelines for configuration changes across a fleet, this kind of before/after metric capture is straightforward to integrate into CI/CD workflows. Teams that manage large numbers of servers often script these validation steps into their deployment tooling.

One specific test we run: allocate 90% of RAM with stress-ng --vm workers, hold for 60 seconds, release, and watch how quickly the system recovers. With swappiness=60 and no zswap, recovery on our 32 GB test server took 8-12 seconds as swapped pages were read back from NVMe. With swappiness=10 and zswap enabled, recovery was under 2 seconds because most pages were in the compressed pool.

# Install stress-ng and sysstat if not present
apt install stress-ng sysstat   # Debian/Ubuntu
dnf install stress-ng sysstat   # RHEL/Fedora

# Record swap event baseline (run for 30 sec, save vmstat output)
vmstat 1 30 > /tmp/vmstat_before.txt

# Apply memory pressure: 4 workers each allocating 6 GB, 60 second run
stress-ng --vm 4 --vm-bytes 6G --timeout 60s &

# Capture during stress
vmstat 1 70 > /tmp/vmstat_during.txt

# Wait for stress to finish, capture recovery
wait
vmstat 1 30 > /tmp/vmstat_after.txt

# Summarise swap events from each file
for f in before during after; do
  so=$(awk 'NR>2{sum+=$8} END{print sum}' /tmp/vmstat_${f}.txt)
  si=$(awk 'NR>2{sum+=$7} END{print sum}' /tmp/vmstat_${f}.txt)
  echo "${f}: swap_out=${so} pages, swap_in=${si} pages"
done

# Check zswap stats after the test
grep '' /sys/kernel/debug/zswap/*