Productivity

VPS 2GB RAM Nginx Postgres Node App Swap Memory Tuning Guide

VPS 2GB RAM Nginx Postgres Node App Swap Memory Tuning Guide

A 2GB RAM VPS costs roughly $6–12/month on DigitalOcean, Hetzner, or Vultr in 2026. Thousands of solo developers and small teams run production workloads on exactly this spec. Most of them hit the same wall: the app runs fine for a week, then starts swapping to death at 2 AM on a Tuesday.

The fix isn’t more RAM. It’s configuration.

This is the checklist that covers what actually kills these deployments — and what keeps them stable.

Key Takeaways

  • A 2GB VPS running Nginx + Postgres + Node.js burns through the first 500MB of baseline overhead before serving a single user request, leaving almost no headroom before swap becomes critical.
  • Misconfigured swap — wrong size, wrong swappiness, wrong filesystem — causes more production outages on constrained VPS setups than application bugs, according to ServerSpan’s Linux memory management analysis.
  • PostgreSQL’s shared_buffers defaults to 128MB and should be capped at 256MB on a 2GB instance. The default is fine. The problem is blindly following “set it to 25% of RAM” advice written for dedicated servers.
  • Node.js heap defaults to ~1.5GB on V8. On a 2GB machine, that single default will crater your entire stack under moderate load.
  • Proper swap tuning — 2GB swapfile, swappiness=10, vm.vfs_cache_pressure=50 — can extend a 2GB VPS’s effective stability window by 3–5x under burst traffic.

The Reality of 2GB RAM in 2026: What Eats Your Memory First

Before any tuning, the math has to be honest.

Fresh Ubuntu 24.04 LTS boot: ~220MB used. Nginx with a basic config: ~15–30MB. PostgreSQL at idle with default config: ~25–50MB. A single Node.js process with a medium Express app: ~80–150MB at startup, climbing toward 300–500MB under real load. That’s 500–850MB consumed before a single user request completes.

Add the Linux kernel’s file cache, systemd overhead, and any monitoring agents, and you’re sitting at 900MB–1.1GB baseline. On a 2GB machine, that leaves under 1GB of working room.

The MassiveGRID Ubuntu VPS memory management guide documents this pattern directly: constrained VPS instances hit swap not because of traffic spikes but because of poor baseline configuration. The process isn’t dramatic. It’s slow memory pressure accumulating until the OOM killer fires.

That’s why this checklist starts with measurement, not tweaks. Run free -h and htop before touching a single config file. Know your actual baseline. Then cut from there.


Swap Configuration: The Numbers That Matter

Swap on a VPS isn’t a fallback — it’s a pressure relief valve. Sized wrong, it destroys performance. Sized right, it buys time for graceful degradation instead of hard crashes.

The recommended setup for a 2GB instance:

# Create a 2GB swapfile
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# Make it permanent
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Then the kernel parameters. These go in /etc/sysctl.conf:

vm.swappiness = 10
vm.vfs_cache_pressure = 50

swappiness=10 tells the kernel to strongly prefer keeping processes in RAM and only push to swap under real pressure. The Linux default is 60, which is tuned for desktop workloads. On a production VPS, 60 means Postgres starts getting swapped out during normal Node.js activity — the worst possible outcome. According to ServerSpan’s definitive guide on Linux swap vs. RAM, values between 10–15 are the production consensus for constrained cloud instances.

vfs_cache_pressure=50 reduces how aggressively the kernel reclaims inode/dentry cache. At the default of 100, the kernel trades filesystem cache for swap space too readily. At 50, it holds that cache longer — critical for Postgres, which relies heavily on OS page cache for performance.

One more thing worth flagging: never put your swapfile on a tmpfs or a network mount. SSD-backed block storage only. Swap on a network filesystem will make your latency numbers embarrassing.


Per-Service Memory Caps: The Actual Tuning Work

PostgreSQL

The “25% of RAM = shared_buffers” rule comes from Postgres documentation aimed at dedicated database servers. On a 2GB VPS running three services, follow this instead:

shared_buffers = 256MB
effective_cache_size = 512MB
work_mem = 4MB
maintenance_work_mem = 64MB
max_connections = 20

max_connections = 20 is the one most people skip. Each idle Postgres connection uses ~5–10MB. At the default of 100 connections, you’ve allocated up to 1GB just in connection overhead. Use PgBouncer in transaction-mode pooling if the app needs more than 20 concurrent database connections.

This approach can fail when connection pooling is configured incorrectly — PgBouncer set to session mode instead of transaction mode won’t meaningfully reduce the active connection count under concurrent load. Transaction mode is non-negotiable here.

Node.js

V8’s default max heap is approximately 1.5GB on 64-bit systems. On a 2GB machine, that’s a near-guarantee of OOM events. Cap it explicitly:

node --max-old-space-size=512 server.js

512MB is a reasonable ceiling for a single Node process on this hardware. If the app consistently needs more than that, a 2GB VPS isn’t the right host without horizontal scaling alongside it.

Nginx

Nginx’s memory footprint is lean by default, but worker processes add up. Set:

worker_processes 1;
worker_connections 512;

One worker process handles thousands of concurrent connections via async I/O. Two worker processes on a single-core VPS just doubles memory usage for no throughput gain.


Comparison: Swap Configurations on a 2GB VPS

ConfigurationswappinessSwap SizeStability Under BurstPostgres PerformanceRecommended For
Linux Default60NonePoor — OOM at moderate loadDegrades quicklyDesktop/dev machines
Minimal Swap10512MBMarginal — helps brieflyAcceptable idleLow-traffic hobby projects
Balanced Production102GBGood — absorbs traffic burstsStable with tuned configSolo SaaS, small APIs
Aggressive Swap14GBHigh swap use — latency spikesPoor under loadNot recommended

The “Balanced Production” row is where a properly tuned checklist lands. Aggressive swap with swappiness near 1 sounds appealing but creates a different problem: when the system does need to swap, it does so in large, sudden chunks instead of gradually — producing latency spikes rather than preventing them.

The Reddit r/selfhosted thread on 4GB VPS memory numbers shows a similar pattern at the 4GB tier. Even with more headroom, users who skipped swappiness tuning still hit instability under moderate self-hosted workloads. The configuration discipline matters more than the raw RAM number.


Production Stability: What to Monitor and When to Act

The hard part isn’t the initial setup. It’s detecting slow memory leaks before the OOM killer fires at 3 AM.

Scenario 1: Memory grows 5% per day for two weeks. This is a Node.js heap leak. Add --max-old-space-size plus a process manager like PM2 with --max-memory-restart 450M. PM2 restarts the process automatically when it approaches the cap, rather than letting it crash the entire server.

pm2 start server.js --max-memory-restart 450M

Scenario 2: Postgres response times climb after midnight. Usually autovacuum running without memory limits. Add autovacuum_work_mem = 32MB to postgresql.conf to cap its appetite.

Scenario 3: Server becomes unresponsive under traffic bursts. Swap is exhausted before the burst ends. Either the swapfile is too small (under 1GB) or swappiness is at the Linux default of 60, causing premature swapping that fills the swapfile before the burst peaks. Increase swapfile to 2GB and drop swappiness to 10.

Set up basic alerting with Grafana Cloud’s free tier, or even a simple cron job that emails when free -h shows available RAM below 200MB. Catching pressure early beats reacting to downtime.


What to Expect After You Apply This

The full checklist distills to five concrete changes:

  • Baseline audit: Know your real memory numbers before tuning anything
  • Swap: 2GB swapfile, swappiness=10, vfs_cache_pressure=50
  • Postgres: shared_buffers=256MB, max_connections=20, add PgBouncer if needed
  • Node.js: --max-old-space-size=512, PM2 with --max-memory-restart
  • Nginx: worker_processes 1, worker_connections 512

Over the next 6–12 months, two things will shift this calculus. ARM-based VPS tiers — Hetzner CAX11, Oracle Ampere — are pushing 4GB RAM into the $5–7/month range, which may make the 2GB constraint less common for new deployments. And Bun’s continued memory efficiency improvements (Bun 1.x processes consistently run at 40–60% of Node.js heap usage for equivalent workloads) will make the Node heap ceiling less painful to hit.

But the swap and Postgres tuning? That stays relevant regardless of runtime.

Run free -h right now and compare your baseline to the numbers above. The gap is usually larger than expected.

References

  1. Linux Swap vs. RAM: The Definitive Guide to Memory Management on VPS - ServerSpan
  2. r/selfhosted on Reddit: Public self-hosted stack on a 4 GB VPS: current memory numbers and what I’m
  3. Ubuntu VPS Swap File Setup and Memory Management | MassiveGRID Blog

Photo by Vagaro on Unsplash