Every indie hacker running on a $5 VPS has asked this question. The answer depends entirely on what kind of apps you're running — and most guides skip the actual math.
Here's the breakdown I wish I had when I started.
Your real available RAM is not 1GB
The OS and PM2 daemon eat RAM before your app runs a single line of code:
| Component | RAM Used |
|---|---|
| Ubuntu 22.04 (minimal) | ~180MB |
| PM2 daemon | ~30MB |
| Available for your apps | ~790MB |
So you're working with ~790MB, not 1024MB. Now let's see what fits.
App capacity by type
| App Type | Idle RAM | Under Load | Max Apps on 2-core 1GB |
|---|---|---|---|
| Simple bot / webhook | 60–90MB | ~120MB | 6–8 apps (fork mode) |
| Standard REST API (Express/Fastify) | 80–120MB | ~200MB | 2–3 apps |
| Next.js / Nuxt SSR | 350–500MB | 600MB+ | 1 app only |
| Heavy AI / Discord bot | 600MB+ | OOM risk | 1 app + swap |
The "under load" column is what actually matters — idle numbers are misleading because Node.js apps spike heavily during deploys, GC cycles, and traffic bursts.
Option A: One API, full cluster mode (2 workers)
If you have a single Node.js API and want to use both CPU cores:
// ecosystem.config.js
module.exports = {
apps: [{
name: 'my-api',
script: './dist/index.js',
instances: 2, // exactly 2 workers for a 2-core VPS
exec_mode: 'cluster',
max_memory_restart: '350M',
node_args: '--max-old-space-size=320',
env_production: { NODE_ENV: 'production', PORT: 3000 }
}]
};
2 workers × 350MB limit = 700MB max, leaving ~90MB headroom. The --max-old-space-size flag is critical — without it, V8 can claim up to 1.5GB and OOM-kill your server.
Option B: Multiple small apps, fork mode
If you're hosting 2–3 separate projects on the same VPS:
module.exports = {
apps: [
{
name: 'api',
script: './api/dist/index.js',
instances: 1,
exec_mode: 'fork',
max_memory_restart: '250M',
node_args: '--max-old-space-size=200',
},
{
name: 'worker',
script: './worker/dist/index.js',
instances: 1,
exec_mode: 'fork',
max_memory_restart: '200M',
node_args: '--max-old-space-size=160',
}
]
};
Two apps × ~200MB = 400MB used, ~390MB free for spikes. Fork mode is better here than cluster — each app runs independently, and a crash in one doesn't affect the other.
3 things I learned the hard way
1. Always create a swap file — it's not optional on 1GB
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
Deploys temporarily spike RAM usage by 30–50%. Without swap, a npm install on a busy server will OOM-kill your running app.
2. max_memory_restart is a safety net, not a target
If PM2 is regularly hitting the limit and restarting your app, that's a memory leak — fix the code, don't raise the limit. Use pm2 monit to watch RSS (resident set size) over time.
3. Node.js v20+ saves you ~20% RAM for free
Modern V8 engines use pointer compression by default on 64-bit systems, reducing heap object size significantly. If you're still on Node 16/18, upgrading alone might give you room for one more small app.
The quick decision guide
-
1 production API → cluster mode, 2 instances,
max_memory_restart: '350M' -
2 small projects → fork mode, 1 instance each,
max_memory_restart: '250M' -
3+ bots/webhooks → fork mode,
max_memory_restart: '120M', add swap - Next.js SSR → 1 app only, upgrade to 2GB RAM if possible
Full PM2 ecosystem configs for 1GB, 2GB, and 4GB VPS tiers at vpsfor.dev →
Top comments (0)