A 2 GB RAM VPS is the sweet spot where hobby projects meet real production traffic: cheap enough to run continuously, large enough to serve thousands of daily visitors. It is also where most “my server got slow” stories begin, because 2 GB of memory is a fixed budget that every process on the box has to share. This guide covers the memory arithmetic that decides what fits, the workloads that belong on this size, and the concrete Linux tuning that extracts measurably more throughput from the same hardware.
Start With the Hardware, Not the Price
Two plans advertised as “2 GB RAM” can be wildly different machines. A plan with NVMe storage and 2 vCPUs bursts far better than one on shared SATA-backed disks with 1 vCPU. Before tuning anything, check the CPU model, storage type, and whether the provider publishes a transparent spec and uptime commitment. If the storage is spinning or heavily oversubscribed, no amount of sysctl tuning will fix disk latency.
What Runs Comfortably — and What Does Not
- WordPress or another PHP site behind Nginx + PHP-FPM, tuned as below — a few thousand daily visitors is realistic.
- A Node.js, Python, or Go API — most JSON APIs idle below 150 MB and burst to a few hundred MB.
- A small database — MySQL, MariaDB, or PostgreSQL with a working set under roughly 1 GB, i.e. a few hundred thousand rows.
- Network services — WireGuard, a Caddy/Nginx reverse proxy, DNS, or a mail relay.
- Two to four lightweight containers — avoid running a JVM or Elasticsearch inside them.
What does not fit: Elasticsearch with default heap settings (it wants 4 GB+), heavy CI runners, video transcoding, modded game servers, and multiple concurrent production databases. The rule is simple — if the application documents a 4 GB minimum, do not fight it here, or you will spend evenings chasing OOM kills instead of shipping features.
The Memory Math: PHP-FPM
PHP-FPM is where 2 GB servers die first. Each worker holds one request in memory, so concurrent capacity equals your worker count. The formula is pm.max_children = usable RAM / average worker size. With 2 GB total, the OS and Nginx consume around 300 MB, leaving about 1.7 GB. At 256 MB per worker that is only 6 workers. A sane pool:
pm = dynamic
pm.max_children = 6
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 500
pm.max_requests = 500 recycles workers so slow leaks never accumulate. If you add Redis object caching, give Redis 64–128 MB and drop PHP’s per-worker limit to 128 MB — that roughly doubles concurrency without adding hardware. Verify that the pool actually matches your workload by watching pm.max_children reached in the FPM status page; if it is hit often you are underprovisioned, and if average worker memory stays far below the limit you can safely raise the count.
Swap, Swappiness, and the Kernel Budget
Size swap at 1–2 GB on fast NVMe, then set vm.swappiness=10 so the kernel swaps only under genuine pressure rather than eagerly evicting hot pages. Reduce the dirty-page writeback window to avoid latency spikes:
vm.swappiness = 10
vm.vfs_cache_pressure = 50
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
vfs_cache_pressure=50 biases the kernel toward retaining dentry and inode cache, which is exactly what you want on a box with lots of small files. Lowering the dirty ratios flushes written pages sooner, smoothing I/O and preventing a large write burst from stalling reads.
Protecting Critical Daemons from the OOM Killer
On a 2 GB box, one runaway process can trigger the out-of-memory killer and take down your database or web server along with it. systemd’s resource-control directives let you draw hard boundaries per service instead of relying on hope:
[Service]
MemoryMin=256M # guaranteed floor, protected from eviction
MemoryMax=512M # hard cap; the service is OOM-killed, not the box
MemoryHigh=400M # soft limit that throttles before the hard cap
MemoryMin protects the service’s working set from reclaim, MemoryHigh applies pressure so a leak slows rather than crashes, and MemoryMax contains the blast radius. Apply the same treatment to MySQL: compute an explicit InnoDB buffer pool (typically 256–512 MB here), never the defaults, and cap it with MemoryMax.
Verifying the Result
Do not trust config changes you have not measured. Watch free -m for available memory, vmstat 1 for si/so swap activity (both should stay near zero), and check dmesg -T | grep -i oom after load tests. A useful discipline is to change one knob at a time and re-measure, because several of these settings interact: aggressive swappiness plus a low worker count can look fine until traffic spikes, then collapse together.
Confirm that your tune actually raised throughput by running a quick load test — the whole point of tuning a constrained box is that every megabyte is accounted for. If a load test shows the box still swapping under normal traffic, the workload is simply too large for 2 GB and no sysctl will fix it. If it holds steady memory with headroom, you have room to grow the PHP-FPM pool or add a service. If you consistently hit the ceiling even after tuning, it is time for a plan with more RAM: compare the available configurations here.




Leave a Reply
You must be logged in to post a comment.