Set Node max-old-space-size on a 2 GB VPS
Cap the V8 old space on a 2 GB Ubuntu VPS so Node cannot starve the kernel, nginx, and the page cache before the Linux OOM killer fires first.
Netbay Infrastructure Team
Netbay Engineering
On this page
A 2 GB VPS is enough for a Node API, nginx, and a small database. It is not enough for V8 to pretend the machine is a laptop with headroom. The old-space heap is the long-lived object graph. If you let it grow until the kernel OOM killer fires, you lose nginx in the same shot. If you cap it, Node throws a heap out-of-memory error, the unit restarts, and the rest of the box stays up. That is the whole trick: fail inside the process, not inside the kernel.
This post is for Ubuntu 24.04 with Node 22, 2 GB of RAM, High-Speed SSD, and a typical split of one Node service plus nginx. Numbers are starting points. Measure rss and heapUsed on your app and then edit them.
What 2 GB is actually spent on
The kernel, sshd, systemd, journald, and nginx idle want a few hundred megabytes. Page cache wants whatever is left; it is the reason a second request for the same file is fast. If Node rss sits at 1.6 GB, the cache is gone and disk reads show up in iowait. Swap on SSD will keep the process technically alive while turning every GC into a latency cliff. Treat swap as a last-resort crash net, not as heap.
V8 old space is not rss. rss includes the stack, mapped files, and native addons. --max-old-space-size only caps the JavaScript heap old generation, in megabytes. A process can still rss well above that number. You need both the V8 flag and a cgroup MemoryMax so native leaks cannot eat the host.
On a 2 GB instance a sane first cap is 768 MB of old space and MemoryMax=1G on the unit. That leaves room for the OS and for nginx workers. If the app is a tiny JSON API, 512 MB is plenty. If it is a SSR renderer with big templates, 768 MB is the number you justify with a metric, not a hope.
[Service]
Type=simple
User=nodeapp
Group=nodeapp
WorkingDirectory=/srv/app
EnvironmentFile=/etc/nodeapp/app.env
Environment=NODE_OPTIONS=--max-old-space-size=768
ExecStart=/usr/bin/node /srv/app/server.js
Restart=on-failure
MemoryMax=1G
MemoryHigh=768M
MemorySwapMax=0
OOMScoreAdjust=200MemoryHigh is a soft throttle; MemoryMax is the hard ceiling where the cgroup OOM killer runs. OOMScoreAdjust=200 makes the Node process more attractive to the killer than sshd if something still escapes. MemorySwapMax=0 refuses to hide a leak in swap. NODE_OPTIONS is the supported way to pass V8 flags without rewriting ExecStart; do not also add the flag on the command line or you get conflicting values.
See heap vs rss before you guess
process.memoryUsage() reports rss, heapTotal, heapUsed, and external. Log it on an interval in staging, or expose it on a private loopback route. heapUsed approaching 768 MB while rss is 900 MB is a healthy cap. rss of 1.4 GB with heapUsed of 400 MB is native memory or buffers, and --max-old-space-size will not save you. Then you look at Buffers and addons, not at V8 flags.
const http = require('http');
function line() {
const m = process.memoryUsage();
return 'rss_mb=' + Math.round(m.rss / 1048576) +
' heap_used_mb=' + Math.round(m.heapUsed / 1048576) +
' heap_total_mb=' + Math.round(m.heapTotal / 1048576) +
' external_mb=' + Math.round(m.external / 1048576);
}
setInterval(function () { console.log(line()); }, 30000).unref();
http.createServer(function (req, res) {
if (req.url === '/metrics') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(line() + '\n');
return;
}
res.end('ok\n');
}).listen(3000, '127.0.0.1');Bind that metrics route to loopback only, or better, do not put it on the public server at all and read journald. A public memory dump is not a secret, but it is a reconnaissance gift.
When V8 hits the cap you get FATAL ERROR: Reached heap limit Allocation failed. systemd sees a non-zero exit and restarts. That is success compared with a host that lost ssh. If restarts loop, the leak is still there; the cap only converted a host outage into an app outage. Profile with heap snapshots on a staging box the same size, not on a 16 GB laptop that never reproduces the pressure.
Do not copy laptop defaults
Laptops often have 16 GB and no nginx. Developers ship without NODE_OPTIONS, then the 2 GB VPS takes the V8 default, which on 64-bit Node can be well over a gigabyte. The first traffic spike GC stalls, the load average climbs, and someone adds more RAM instead of a cap. More RAM is valid if the working set is honestly large. It is not valid if the working set is a leak.
Xeon Platinum cores will not save you from GC. Extra vCPU helps a bit with parallel marking; it does not create memory. Size the instance for rss plus 512 MB, then lock the heap so the process cannot grow into the remainder.
The takeaway: on 2 GB, set --max-old-space-size to a number you can defend, put MemoryMax on the unit, and let Node crash instead of taking sshd with it. Then fix the leak or buy more RAM on purpose.
Cap the heap on a Netbay 2 GB Ubuntu VPS and watch rss before the first traffic spike — netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance