How Much VPS Do You Actually Need?
The two failure modes when picking a VPS are equally common: over-buying an 8 GB box for an app that uses 400 MB, or cramming a real workload onto a 512 MB box and getting mysterious OOM kills at 2 a.m. Both come from guessing instead of measuring. Here’s a framework to size it properly.
Memory is almost always the constraint
On modern VPSes, you’ll run out of RAM long before CPU for most web workloads. So size for memory first. Add up what your processes actually use:
| Component | Typical RSS |
|---|---|
| Node web process | 60–150 MB each |
| Python (FastAPI/Django) worker | 80–200 MB each |
| JVM app | 300 MB – 1 GB+ |
| Postgres (small) | 100–300 MB |
| Redis (small) | 20–100 MB |
| Reverse proxy (Caddy/nginx) | 10–30 MB |
| The OS itself | 150–300 MB |
| Your process manager | ~4 MB (Oxmgr) / ~80 MB (PM2) |
A concrete example — a Node API with 4 instances, a worker, Postgres, and Caddy:
4 × 120 MB (web) = 480 MB
1 × 150 MB (worker) = 150 MB
Postgres = 250 MB
Caddy = 20 MB
OS = 250 MB
process manager = 4 MB
--------------------------------
Total ≈ 1.15 GB Add ~50% headroom for spikes, page cache, and growth → a 2 GB VPS. Not 8. Not 512 MB.
Then check CPU
CPU sizing is about concurrency and per-request work:
- IO-bound apps (most CRUD APIs — they wait on the database) need surprisingly little CPU. One core handles a lot; you add instances/clustering to use more cores mostly for parallelism and resilience, not because you’re CPU-starved.
- CPU-bound work (image processing, SSR-heavy pages, WebSocket game state, ML) scales with cores. Here you want
instances ≈ coresand you’ll feel the difference.
Rule of thumb: start with 2 vCPU for a small IO-bound app, and only go higher when you’ve measured CPU as the bottleneck.
Measure, don’t guess
The whole point is to replace estimates with numbers from your actual app. Run it under load (even ab/k6 against staging) and watch:
# Per-process memory, live, straight from the process manager
oxmgr ls --json | jq -r '.[] | "(.name)\t(.memory_bytes/1e6|floor)MB\t(.cpu_percent)%"'
# System-wide
free -h # is memory near full? is swap being touched?
vmstat 1 # 'r' column high = CPU-bound; 'si/so' nonzero = swapping (bad) Two signals tell you you’re undersized: swap activity (si/so in vmstat — memory is the problem) and a consistently high run queue (r in vmstat above your core count — CPU is the problem). If neither is happening, you have headroom.
The total-footprint jq recipe gives you the exact memory your app stack uses, which is the single most useful number for sizing.
A sizing cheat sheet
| Workload | Start with |
|---|---|
| Side project, one small app | 1 GB / 1 vCPU |
| Small SaaS: app + DB + worker | 2 GB / 2 vCPU |
| Busy app, few thousand users | 4 GB / 2–4 vCPU |
| CPU-heavy (media, SSR, WS) | 8 GB / 4+ vCPU |
| JVM app + DB | 4 GB minimum (heap eats RAM) |
These are starting points to then measure from, not final answers.
Scale up before you scale out
When you do outgrow the box, the cheapest, simplest move is almost always a bigger single box, not a second one. Vertical scaling has no distributed-systems tax — no shared session store, no load balancer, no cross-node database. A single 8 GB VPS runs an enormous amount of app. Only split to multiple servers (or reconsider Kubernetes) when one machine genuinely can’t hold the load or you need failover.
Keep it from creeping
Two habits keep a right-sized box right-sized:
- Cap memory per process with resource limits, so a leak recycles a process instead of eating the whole box.
- Watch the footprint with a periodic
oxmgr ls --jsoncheck, so growth is a graph you see coming, not a 2 a.m. page.
Size for memory, verify CPU, measure under load, add headroom, and prefer a bigger box to more boxes. Do that and you’ll neither overpay for idle RAM nor get surprise-killed for lack of it.