On a 2 GB or 4 GB VPS, memory pressure does not announce itself politely. The kernel simply reclaims, swaps, throttles, and eventually kills a process. The settings that decide which process dies and how early the system starts thrashing live in a handful of sysctl keys and one per-process integer. This guide covers sizing a swapfile on small instances, reading the OOM killer’s decision with oom_score_adj, and tuning vm.swappiness, overcommit and dirty writeback so the box degrades in a predictable order instead of collapsing.
Start by measuring actual memory headroom
Before changing anything, record what the workload really uses at steady state. Run free -m and vmstat 1 30 under full load, then check /proc/meminfo for MemAvailable, SwapCached, and Committed_AS. MemAvailable is the number that matters; free memory is mostly page cache and will look alarmingly low on a healthy server.
Compare Committed_AS against physical RAM plus swap. If commits regularly exceed that total, overcommit is the real problem and no swapfile will save you. Also watch the si and so columns in vmstat: sustained values above roughly 500 KB/s mean the working set no longer fits and you are paying disk latency for every page fault.
Swapping on SSD VPS: size, swappiness and swapfile setup
A practical rule for small instances is swap equal to 50–100 percent of RAM up to 4 GB, then 2 GB fixed above that. A 2 GB/2 GB setup absorbs a runaway build or a PHP worker spike without invoking the killer, which is exactly the grace window you want. Create it with fallocate -l 2G /swapfile, chmod 600, mkswap, swapon, and a matching fstab entry, then verify with swapon --show. If your provider offers flavours with a fixed swap partition instead, our VPS plan overview marks which tiers include one.
Set vm.swappiness=10 on servers where resident latency matters, and leave it near 60 only for batch workloads that benefit from keeping anonymous pages out of RAM. Modern kernels treat swappiness as a relative IO-cost hint rather than a straight ratio, so 10 still allows swap under pressure. Confirm behaviour after a change with cat /sys/fs/cgroup/system.slice/memory.swap.current under cgroup v2 to see how much swap a specific service actually consumed.
Reading the OOM killer decision
When memory is truly exhausted, the kernel scores every process in the cgroup and kills the highest score. Check /proc/<pid>/oom_score and /proc/<pid>/oom_score_adj, or list them all with for p in /proc/[0-9]*; do echo "$(cat $p/oom_score) $p"; done | sort -n | tail -20. The score is proportional to resident memory; adj shifts it from -1000 (never kill) to 1000 (kill first).
Protect the pieces that must survive: systemctl edit sshd with OOMScoreAdjust=-800, and do the same for your supervisor or systemd-journald if logs vanish during incidents. Push the disposable workers the other way, for example OOMScoreAdjust=500 on a queue consumer that can be restarted. Never set -1000 on a large process; it effectively makes the kernel choose a different victim, often your database.
Overcommit policy: heuristic vs always overcommit
The default vm.overcommit_memory=0 uses a heuristic that refuses obviously impossible allocations. Mode 1 accepts everything, which is what Redis and some JVM deployments request in their documentation. Mode 2 is strict and caps allocation at CommitLimit, visible in /proc/meminfo, computed from RAM plus swap times vm.overcommit_ratio.
For a shared VPS, mode 0 is usually right: it prevents one fork bomb from booking all address space while still letting optimistic allocators work. If you do switch to mode 2, set the ratio deliberately (50 is a sane start) and test with stress-ng --vm 2 --vm-bytes 80% --timeout 60s. Mode 1 on a small box just moves the failure later and makes the OOM killer the only backstop.
Dirty page writeback and the stall nobody expects
A container that writes logs fast can hit writeback throttling long before it runs out of memory. vm.dirty_ratio defaults to 20 percent of RAM; on a 4 GB VPS that is 800 MB of dirty pages before writers block. On slow virtual disks that stall lasts seconds. Lower vm.dirty_ratio to 10 and vm.dirty_background_ratio to 5, or use absolute values such as vm.dirty_bytes=268435456 and vm.dirty_background_bytes=67108864.
Measure the effect with grep -E 'Dirty|Writeback' /proc/meminfo while running your write workload, and watch iostat -x 1 for rising aqu-sz and w_await. If write latency spikes correlate with high Dirty values, the fix is either more background flushing or a faster volume — not more memory.
Per-service memory limits instead of global guesswork
Global sysctls are a blunt instrument. Under cgroup v2, cap each service with MemoryMax= and MemoryHigh= in a drop-in, then read memory.events to count throttling and OOM kills. A unit with MemoryHigh=512M and MemoryMax=640M reclaims eagerly at the high mark and only invokes the killer at the hard limit, which keeps neighbours untouched.
Correlate kills with application errors by checking journalctl -k | grep -i 'killed process'. The line names the victim, its total-vm and the cgroup, which usually identifies the leaking worker immediately. If kills happen at the hard limit while MemoryHigh throttling is rare, raise the budget; if throttling dominates and latency suffers, the workload needs more RAM or fewer concurrent workers.
Proving the tuning worked
Build a repeatable check: run your realistic peak workload for ten minutes, capture vmstat 1, /proc/pressure/memory and the cgroup’s memory.events before and after. The target is some avg10 memory pressure under 5 at peak, zero OOM kills, and swap-in traffic confined to cold pages. If pressure stays high with no swap activity, your working set is simply larger than the instance.
When numbers stay flat after tuning, the honest conclusion is that the plan is undersized. Sizing up one tier is cheaper than a week of incident calls, and our VPS plan breakdown lists what each memory tier realistically supports for LEMP, Docker and small JVM services. Include disk class in that decision too. Automated alerting on memory.events gives you the last piece: a page before the killer fires, not after.


Leave a Reply
You must be logged in to post a comment.