A process that dies with nothing but Killed on the line — or a container that exits with code 137 — was almost always stopped by the kernel’s out-of-memory (OOM) killer, not by a bug in your program. The machine ran out of memory, and the kernel killed the biggest process to stay alive. Confirm it in the kernel log, then fix it with swap, a memory cap, or more RAM.
sudo dmesg -T | grep -i -E 'out of memory|killed process'
# or from the journal (current boot; add -b -1 for the previous one):
sudo journalctl -k | grep -i -E 'out of memory|oom'137 is the giveaway: a process killed by a signal exits with 128 + signal number, and the OOM killer uses SIGKILL (9), so 128 + 9 = 137. That is the same exit code a manual kill -9 produces, so 137 means “killed by SIGKILL” — usually, but not always, the OOM killer.
How do I know it was the OOM killer?
Look for the kernel’s own line. When it steps in, it logs which process it chose:
sudo dmesg -T | grep -i 'killed process'
# Out of memory: Killed process 2314 (mysqld) total-vm:... anon-rss:...The name in parentheses is the process it killed — which is the victim, not necessarily the cause: the OOM killer targets a large process to reclaim the most memory, and that is often not the one that leaked. For the other two places the signal shows up:
# a systemd service killed by OOM
systemctl show -p Result myapp # prints Result=oom-kill
sudo journalctl -u myapp | grep -i oom
# a Docker container
docker inspect -f '{{.State.OOMKilled}}' myctr # true if the OOM killer hit it
docker inspect -f '{{.State.ExitCode}}' myctr # 137What is using the memory?
Before you add swap or raise a limit, find out where the memory went. If the kill has already happened, these show you the shape of it so you can keep it from recurring:
free -h # total, used, available, swap
ps -eo pid,comm,rss --sort=-rss | head # top processes by resident memory
sudo systemd-cgtop -m # memory by service / cgroupRead the available column of free -h, not free: the kernel uses spare RAM for cache, which it hands back under pressure, so a low “free” number is normal and not the problem. When available approaches zero and swap is full too, the next allocation triggers the killer.
How do I add swap?
Swap gives the kernel somewhere to push cold pages instead of killing a process. It is not a substitute for enough RAM — a box that swaps heavily is slow — but a couple of gigabytes absorbs spikes and turns a hard kill into a slowdown. A 2 GB swap file:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # survive rebootSwap needs free disk, so this fails on a full volume — if fallocate or dd reports no space, clear room first (No space left on device). On a VPS you can also tune how eagerly the kernel swaps with vm.swappiness (a lower value, say 10, keeps more in RAM; set it in /etc/sysctl.d/).
Why does npm run build or pip install get killed?
The most common version of this on a small VPS: a build tool, not a running service. npm run build, vite build, a webpack run, pip install compiling a wheel, or composer on a big dependency tree all spike memory and get killed on a 1–2 GB plan while the app itself would run fine.
- Node: cap the heap below the box’s RAM so V8 collects garbage instead of growing until the kernel kills it —
NODE_OPTIONS=--max-old-space-size=1536 npm run buildon a 2 GB server. - Fewer parallel jobs: parallelism multiplies peak memory. Use
make -j2rather than-j$(nproc), and tell the bundler to use fewer workers. - pip:
pip install --no-cache-dirlowers peak memory when a large package is downloaded, and a prebuilt wheel skips the compile step that usually does the damage. - Build elsewhere. The cleanest fix for a production box is not to build on it at all: build in CI or on a larger machine and ship the artefact. It also keeps a compiler off the live server.
Why do MySQL or PHP-FPM get OOM-killed?
Long-running services are usually killed because they are configured to assume more RAM than the plan has. Two defaults account for most of it:
- MySQL / MariaDB:
innodb_buffer_pool_sizeis the big one. Set it to a figure that leaves room for the OS and everything else — on a 2 GB box, a few hundred megabytes, not the “70% of RAM” that tuning guides assume on a dedicated database server. - PHP-FPM:
pm.max_childrentimes the memory each worker uses is the ceiling. Too many children and a traffic spike exhausts RAM. Size it to (available RAM ÷ average worker size), with headroom.
How do I limit one service instead of the whole box?
Rather than let any process take the whole machine down, cap the one that misbehaves. Under systemd, MemoryMax= puts a hard ceiling on a unit — when it is exceeded, only that service’s processes are killed, and the rest of the box keeps running:
sudo systemctl edit myapp
# in the editor, add (systemd does not allow comments after a value,
# so keep notes on their own lines):
[Service]
# start reclaiming before the hard cap
MemoryHigh=448M
MemoryMax=512M
# systemctl edit reloads systemd on save; restart to apply
sudo systemctl restart myappdocker run --memory=512m --memory-swap=512m myimageYou can also protect the processes you never want killed. The OOM killer scores each process; nudging a critical one down makes it the last thing chosen:
# as a drop-in for the service (sudo systemctl edit mariadb, for example)
[Service]
OOMScoreAdjust=-800
# or live, for a running PID
echo -800 | sudo tee /proc/$(pgrep -o sshd)/oom_score_adjProtecting a process only moves the target — the kernel still has to kill something when memory runs out, so this buys you a reachable SSH session to fix the real cause, not a cure.
Can exit code 137 happen without the OOM killer?
Yes. 137 only means “killed by SIGKILL”. The OOM killer is the usual sender, but not the only one:
- A slow container shutdown.
docker stopsends SIGTERM, waits (10 seconds by default) and then SIGKILLs anything still running — which exits 137 withOOMKilled=false. Handle SIGTERM in your app, or raise the grace period withdocker stop -t 30(stop_grace_periodin Compose). - A manual or orchestration kill.
kill -9, a Kubernetes liveness-probe failure, or a CI step that timed out and was force-stopped.
The one reliable test is the flag: docker inspect -f '{{.State.OOMKilled}}' ctr is true only for a real memory kill, and dmesg has no OOM line when it was something else.
Does it matter whether the VPS is KVM or a container?
It changes who does the killing. On a container-based VPS (OpenVZ, LXC) your processes share the host’s kernel, the limit is a container memory limit, and free may show figures that do not match your plan. On a KVM virtual machine your own kernel manages the RAM your plan assigns, so free and the OOM log line describe your machine and the fixes above apply as written. The difference is spelled out in KVM vs OpenVZ.
If the box also restarts rather than just losing a process, that is a different trail — the OOM killer does not reboot a machine on its own (unless vm.panic_on_oom is set), so see VPS keeps rebooting. And when a kernel thread like kswapd0 is pinning the CPU, that is memory pressure showing up as load: VPS high CPU usage covers it.
When the honest answer is simply “not enough RAM”, move up a size rather than fight the OOM killer on a box that is too small for the job. On our Standard KVM line, for example, memory grows with each tier, from 4 GB at the entry size to 64 GB at the top.
Frequently asked
What does exit code 137 mean?
A process killed by a signal exits with 128 plus the signal number, and 137 = 128 + 9, so it was killed by SIGKILL. On a server that is almost always the kernel's out-of-memory killer. Confirm it with 'sudo dmesg -T | grep -i "killed process"' or, for a container, 'docker inspect -f {{.State.OOMKilled}} <ctr>'. The same code appears after a manual 'kill -9' or a forced container stop, so check for the OOM log line before assuming memory.
Does adding swap fix out-of-memory kills?
It helps, but it is not a cure. Swap gives the kernel somewhere to put cold pages instead of killing a process, which absorbs short spikes and turns a hard kill into a slowdown. A box that swaps constantly is slow, so if you swap all the time the real fix is less memory use (buffer pool, worker counts, build heap) or more RAM.
How do I stop the OOM killer from killing sshd or MySQL?
Lower the process's OOM score so it is chosen last: a systemd drop-in with OOMScoreAdjust=-800, or 'echo -800 > /proc/<pid>/oom_score_adj' for a running process. Better, cap the process that actually misbehaves with systemd's MemoryMax= or a container's --memory, so only it is killed instead of your database. The kernel still has to kill something when memory runs out — this chooses what.
Why was my npm build killed?
A build (npm, vite, webpack, pip compiling a wheel) spikes memory far above what the running app needs, and on a 1–2 GB VPS that spike hits the OOM killer. Cap Node's heap below the box's RAM with NODE_OPTIONS=--max-old-space-size=1536, reduce parallel jobs, use 'pip install --no-cache-dir', or build in CI and ship the artefact instead of building on the server.