·10 min read·Updated Sep 6, 2026

Low-RAM VPS Tuning: Swap, zram, Memory Limits and the OOM Killer

How to keep a 512MB-2GB VPS stable: swap vs zram, swappiness, systemd memory limits, earlyoom, and tuning MariaDB, PHP-FPM and Node to stop OOM kills.

A small VPS does not usually die from CPU exhaustion. It dies when the kernel's Out-Of-Memory killer picks the wrong process — usually your database — and everything downstream breaks. If you run a 512 MB, 1 GB or 2 GB instance, memory is your scarcest resource and the default configuration of most software assumes far more of it than you have.

This guide covers what actually keeps a low-RAM Linux VPS stable: how to measure real memory pressure, when swap helps and when it hurts, how zram compares to a swap file, how to use cgroup limits so one runaway service can't take down the box, and how to tune the usual memory hogs. It also covers a privacy detail most tuning guides skip: swap writes process memory to disk in plaintext unless you plan for it.

The direct answer

For most small VPS instances:

  1. Add a swap file (1–2× RAM, capped around 2 GB) or enable zram — swap is not optional on a 1 GB server running a database.
  2. Lower vm.swappiness to roughly 10–20 so the kernel prefers reclaiming page cache over swapping active anonymous memory.
  3. Put explicit memory limits on your services with systemd MemoryMax / MemoryHigh, so a leak in one app doesn't kill the SSH daemon or the database.
  4. Tune the big three consumers: database buffer pool, PHP-FPM / worker process counts, and JVM/Node heap sizes.
  5. Install a userspace OOM daemon (earlyoom or systemd-oomd) so kills happen predictably instead of after minutes of thrashing.

Everything below is the reasoning and the exact commands.

Measure before you tune

free -h is the starting point, but the "available" column matters more than "free":

free -h
              total   used   free   shared  buff/cache  available
Mem:          962Mi  512Mi   61Mi    18Mi      388Mi       310Mi
Swap:         2.0Gi  126Mi  1.9Gi

Low free with healthy available is normal — the kernel uses spare RAM for page cache. What you care about is whether the system is reclaiming under pressure.

Check pressure stall information, which is far more honest than load average:

cat /proc/pressure/memory
some avg10=0.00 avg60=0.12 avg300=0.31 total=41283
full avg10=0.00 avg60=0.00 avg300=0.00 total=2914

Any sustained full value above zero means all tasks were stalled waiting on memory — that is real thrashing. some in the low single digits is tolerable.

Find who is actually using memory, sorted by RSS:

ps -eo pid,rss,comm --sort=-rss | head -15

Or per-service, which is more useful on a systemd host:

systemd-cgtop -m --order=memory

And check whether you have already been hit by an OOM kill:

journalctl -k --grep="Out of memory|oom_kill" --since "7 days ago"

Per-cgroup counters tell you if a service is being throttled or killed against its own limit:

cat /sys/fs/cgroup/system.slice/mariadb.service/memory.events

If high or max counters are climbing, that service is hitting its ceiling. This is the same class of diagnostic work as figuring out whether the node itself is the problem — if memory looks fine but everything is slow, check whether your VPS node is oversold before tuning further.

Swap file vs zram

These solve different problems and can be combined.

A swap file gives you real overflow capacity on disk. It lets rarely-touched pages (idle daemons, forked workers that never run again) leave RAM entirely. On NVMe-backed storage the latency penalty is acceptable for cold pages. The cost is disk I/O and, on a shared node, I/O contention.

zram creates a compressed block device in RAM and swaps to it. Typical compression on anonymous memory is roughly 2–3:1 with zstd or lzo-rle, so 512 MB of zram may hold well over a gigabyte of pages while consuming only the compressed footprint. It is fast, involves no disk writes, and never persists secrets to storage. The cost is CPU cycles for compression and the fact that it does not increase total capacity the way disk swap does — you are trading RAM for RAM.

Rule of thumb: zram first on very small instances (512 MB–1 GB), plus a modest disk swap file as a safety net. Give zram a lower swap priority number than… actually, higher priority — Linux uses the highest priority swap device first, so zram should have the higher priority and disk swap the lower one.

Creating a swap file

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon --priority 10 /swapfile
echo '/swapfile none swap sw,pri=10 0 0' >> /etc/fstab

If fallocate fails on your filesystem (some setups with certain btrfs configurations), use dd if=/dev/zero of=/swapfile bs=1M count=2048.

Enabling zram

On Debian/Ubuntu with systemd, systemd-zram-generator is the cleanest option:

apt install systemd-zram-generator

/etc/systemd/zram-generator.conf:

[zram0]
zram-size = min(ram / 2, 1024)
compression-algorithm = zstd
swap-priority = 100

Then systemctl daemon-reload && systemctl start [email protected]. Verify with zramctl and swapon --show.

Swappiness and cache pressure

# /etc/sysctl.d/99-memory.conf
vm.swappiness = 15
vm.vfs_cache_pressure = 60
vm.min_free_kbytes = 32768

Apply with sysctl --system.

swappiness=15 biases reclaim toward page cache instead of pushing your database's working set to disk. If you use zram as the primary swap, a higher swappiness (60–100) is often better, because swapping to compressed RAM is cheap. Set it according to which device the kernel will use.

min_free_kbytes keeps a reserve so the kernel can satisfy atomic allocations under pressure rather than stalling. Don't set it absurdly high on a small box — 2–4% of RAM is a reasonable ceiling.

The swap privacy caveat

Swap can contain anything that was in process memory: decrypted database rows, session tokens, private keys held by an application, plaintext of files you were editing. On a disk-backed swap file on unencrypted storage, that data persists across reboots and lives on whatever storage backend the provider uses.

Options, in order of practicality:

  • Use zram only for workloads handling sensitive material. Compressed RAM disappears at power-off.
  • Put the swap file on an encrypted volume. This is where full-disk encryption matters, though you should be clear-eyed about what LUKS actually protects on a remote server — a running VPS has its keys in RAM.
  • Avoid hibernate-style features entirely (not usually relevant on a VPS, but worth stating).

Also remember that swap is not the only place memory contents leak to disk: core dumps do the same thing. Disable them if you don't need them (ulimit -c 0, kernel.core_pattern, and Storage=none in /etc/systemd/coredump.conf).

Use cgroups so the right thing dies

Left alone, the kernel OOM killer scores processes by memory footprint and kills the biggest one. On a web server, that is your database — the single process you most want to survive. Fix this with explicit limits rather than by fighting oom_score_adj.

Create a drop-in per service, e.g. systemctl edit php8.2-fpm.service:

[Service]
MemoryHigh=250M
MemoryMax=320M
MemorySwapMax=128M

MemoryHigh is a soft ceiling: the kernel aggressively reclaims and throttles the cgroup above it, which usually slows the leak enough for you to notice. MemoryMax is hard — allocations beyond it trigger a kill inside that cgroup only. Set High roughly 20% below Max so you get back-pressure before the axe falls.

Protect the things that must survive:

# systemctl edit mariadb.service
[Service]
MemoryMin=200M

MemoryMin reserves memory that will not be reclaimed from that cgroup. Do the same for ssh.service if you have ever been locked out of a thrashing box — losing SSH during an incident turns a 10-minute fix into a rescue-console session.

For Docker workloads, the equivalent is --memory=256m --memory-swap=384m per container, or mem_limit in Compose. Containers without limits are the single most common cause of OOM on small VPS instances.

Kill early, kill predictably

The kernel OOM killer intervenes only at the very last moment. Before that, the machine may thrash for minutes: SSH times out, nginx returns 502s, and monitoring reports the host as down while it is technically alive.

A userspace killer acts earlier. earlyoom is minimal and dependency-free:

apt install earlyoom

/etc/default/earlyoom:

EARLYOOM_ARGS="-m 5 -s 20 --avoid '(^|/)(sshd|mariadbd|systemd)$' --prefer '(^|/)(chrome|node|python3)$'"

That triggers when available memory drops below 5% and free swap below 20%, protecting sshd and the database while preferring known-fat processes. systemd-oomd is the alternative and is PSI-based rather than threshold-based; use one or the other, not both.

Tuning the usual suspects

MariaDB / MySQL. Defaults are sized for real servers. On a 1 GB VPS:

[mysqld]
innodb_buffer_pool_size = 128M
innodb_log_file_size = 64M
max_connections = 30
performance_schema = OFF
table_open_cache = 200
tmp_table_size = 16M
max_heap_table_size = 16M

performance_schema = OFF alone typically frees a meaningful chunk of RSS. Remember that per-connection buffers multiply by max_connections — an inflated connection limit is a hidden memory bomb.

PostgreSQL. shared_buffers = 128MB, work_mem = 4MB, max_connections = 25, and put a pooler in front if your app opens connections carelessly.

PHP-FPM. The killer setting is pm.max_children. Measure your average worker RSS (ps -ylC php-fpm8.2 --sort:rss), then set max_children so children × RSS fits your budget. With 40 MB workers and a 250 MB budget, that is 6 — not the default 5–50 dynamic range.

Node.js. Cap the heap: node --max-old-space-size=192 app.js. Without it, V8 sizes the heap from total system memory and will happily grow past what you have.

Java. Use -XX:MaxRAMPercentage=50 rather than a fixed -Xmx if the container size may change, and account for non-heap overhead (metaspace, threads, direct buffers) — real RSS is often 1.3–1.5× the heap.

journald. Logs consume both disk and, via caching, memory pressure. Cap them:

# /etc/systemd/journald.conf
SystemMaxUse=100M
RuntimeMaxUse=32M

There are privacy reasons to go further here too; see the guide on VPS log minimization and retention.

Common mistakes

Disabling swap entirely. Popular advice on fast NVMe boxes, and wrong for small instances. Without swap, the kernel cannot evict cold anonymous pages at all, so it reclaims page cache instead — which means more disk reads and worse performance, right up to a hard OOM kill.

Setting swappiness to 0. This does not "disable swapping"; it makes the kernel avoid it until it is nearly out of options, producing exactly the late, violent OOM behaviour you were trying to avoid.

Giving a service MemoryMax without testing it. A hard limit set too low turns a slow leak into instant, repeated restarts. Watch memory.events for a few days at MemoryHigh only, then set Max above the observed peak.

Counting only RSS. Shared library pages are counted once per process in RSS, so summing RSS over-reports usage. systemd-cgtop, smem -tk, or cgroup memory.current give a truer figure.

Assuming more RAM is always the answer. Sometimes it is, and vertical scaling is legitimately the cheapest fix. But an unbounded connection pool or a 900 MB PHP-FPM fleet will exhaust 4 GB just as reliably as 1 GB.

FAQ

How much swap should a VPS have? For 512 MB–2 GB of RAM, 1–2× RAM is a sane range, capped around 2 GB. More than that on a small instance mostly buys you longer thrashing, not more capacity.

Does swap wear out the disk or count against I/O quotas? Swap writes are real writes. On providers that meter or throttle I/O, heavy swapping degrades everything else on the instance. This is the strongest argument for zram as the first swap tier.

Should I use both zram and a swap file? Yes, on very small instances. Give zram the higher priority so hot pages compress into RAM, and let the disk file absorb genuinely cold pages.

Will MemoryMax make my app slower? Above MemoryHigh, yes — deliberately. The cgroup gets throttled while the kernel reclaims. That is the point: degraded is better than dead, and it gives you a window to react.

Why did the OOM killer target the wrong process? Because the kernel scores by footprint, not importance. Fix it with per-service cgroup limits and MemoryMin protection, plus an --avoid list in earlyoom, instead of relying on oom_score_adj alone.

Practical takeaway

A stable small VPS is one where memory limits are explicit rather than emergent. Add swap — preferably zram plus a modest file — lower swappiness, then put MemoryHigh/MemoryMax on every non-critical service and MemoryMin on the ones that must survive. Cap your database buffers, worker counts and language runtime heaps to real measured numbers instead of defaults. Finally, install earlyoom so failures happen in seconds instead of after ten minutes of unreachable thrashing.

If you conclude you need a larger instance, plan the move properly rather than resizing under pressure — the minimal-downtime VPS migration checklist covers the sequencing. IronBalkans runs full-root KVM instances in Romania with dedicated (not shared-kernel) memory, so the tuning above behaves the way the kernel documentation says it should — and accounts are no-KYC with Monero, Bitcoin or Litecoin billing if that matters for your threat model.

Written by IronBalkans. Last reviewed Sep 6, 2026.