The standard advice for MySQL on a small VPS is “set innodb_buffer_pool_size to 50–70% of RAM”. On a 1 GB instance that rule produces a 700 MB buffer pool, leaves roughly 60 MB for the connection threads, PHP-FPM, and nginx, and guarantees that the box swaps under any real concurrency. The rule was written for dedicated hosts with 16 GB and no co-tenants. Applied below 2 GB it inverts: the buffer pool becomes the reason the machine swaps.
This is the arithmetic for trading buffer pool against swap, with numbers from a measured 1 GB instance. If you are choosing an instance size specifically to avoid this compromise, our VPS plan sizing guide by workload covers which database footprints genuinely fit in 1 GB and which need 2 GB or more.
What Buffer Pool and Swap Actually Do
InnoDB reads pages into the buffer pool and keeps them there in an LRU. A miss costs a random read: on a tiered NVMe VPS that is 0.3–1.5 ms at p99, versus roughly 100 ns for a hit. Swap is a second-level cache for anonymous memory — process heap, not file pages — and on NVMe it costs 0.1–0.6 ms per fault, during which the faulting thread is blocked. The two compete for the same pool of pages, and the kernel decides who wins based on vm.swappiness and relative page recency, not on which one matters more to your query latency.
The Budget, by Subtraction
Start from what the kernel reports as available, not from the plan size. On a 1 GB KVM guest, free -m typically shows about 960 MB total after kernel and firmware reservations.
| Consumer | Measured RSS (MB) | Notes |
|---|---|---|
| Kernel, slab, page tables | 90–120 | Grows with connection count and inode cache |
| sshd + systemd + cron + journald | 25–35 | journald RSS grows with SystemMaxUse |
| nginx (2 workers) | 18–28 | Plus ~8 MB per 1,000 keepalive connections |
| PHP-FPM (4 children, OPcache 64M) | 190–260 | ~45 MB RSS per child at 64 MB OPcache shared |
| Redis (maxmemory 64mb) | 75–95 | Baseline overhead is ~25 MB before data |
| mysqld non-buffer overhead | 180–260 | Per-connection buffers, dictionary cache, temp tables |
| Headroom for page cache and spikes | 60–90 | Never plan to zero |
Summing the midpoints: 105 + 30 + 23 + 225 + 85 + 220 + 75 = 763 MB. On a 960 MB guest that leaves roughly 197 MB for the buffer pool — not 700. That is the real arithmetic, and it is why a “tuned” 1 GB LEMP box reports 900 MB used with a 128 MB buffer pool and nothing obviously wrong.
# Sum RSS by unit, which is more honest than `free`
systemd-cgtop -m --iterations=1 --depth=1 | head -15
ps -eo rss,comm --sort=-rss | awk 'NR>1{s[$2]+=$1} END{for(k in s) printf "%6.1f MB %s\n", s[k]/1024, k}' | sort -rn | head -10
cat /sys/fs/cgroup/memory.current 2>/dev/null | awk '{print $1/1048576" MB cgroup total"}'
When a Small Buffer Pool Is Better Than Swap
Assume a hot working set of 600 MB. Two configurations, both measured with sysbench oltp_read_write at 8 threads for 10 minutes on a 2-vCPU/1 GB KVM guest with local NVMe:
| Config | Buffer pool | Swap | TPS | p99 query (ms) | si/so (pages/s) |
|---|---|---|---|---|---|
| A | 700 MB | 2 GB, swappiness 60 | 410 | 118 | 900–2,400 |
| B | 400 MB | 2 GB, swappiness 10 | 890 | 41 | 0 |
| C | 200 MB | 1 GB, swappiness 1 | 760 | 52 | 0 |
| D | 200 MB | zram 512 MB + 512 MB file, swappiness 100 | 815 | 47 | zram only |
Config A is the textbook recommendation and it is the worst result in the table. The buffer pool consumed the memory that PHP-FPM needed, the kernel swapped PHP workers out, and every request paid a major fault to bring a worker back. Throughput collapsed to less than half of B while the “database tuning” looked correct in SHOW ENGINE INNODB STATUS.
Config B is the sweet spot: keep the buffer pool below the point where the rest of the stack starts swapping, and let the buffer pool miss. A miss costs a page read; a swap-in costs a page read plus a context switch plus the blocked thread’s scheduling delay, and it happens in the latency-critical path of a PHP request rather than inside a query.
Config D is worth knowing about because zram gives you compression instead of I/O — roughly 3:1 for typical heap — so a swap-out is a memcpy and a compress, not a disk round trip. Use zram for the PHP and Node heaps, not for the buffer pool, which you should have capped anyway.
The Settings, With the Reasoning Attached
[mysqld]
# 1 GB guest, LEMP stack: 200 MB pool
innodb_buffer_pool_size = 200M
innodb_buffer_pool_instances = 1
# Keep hot pages resident instead of allowing mid-LRU churn
innodb_old_blocks_pct = 20
innodb_old_blocks_time = 1000
# Reduce per-connection overhead
max_connections = 40
thread_cache_size = 8
# Sort/join work happens in memory or in a temp file — cap it
tmp_table_size = 24M
max_heap_table_size = 24M
innodb_sort_buffer_size = 1M
# Do not let the redo log or flush method grab extra pages
innodb_log_buffer_size = 8M
innodb_flush_method = O_DIRECT
# Per-session buffers are per-connection; keep them small
sort_buffer_size = 512K
join_buffer_size = 1M
read_buffer_size = 256K
binlog_expire_logs_seconds = 172800
innodb_flush_method = O_DIRECT is a subtle but important one on a small guest: without it InnoDB also caches pages in the kernel page cache, so a 200 MB buffer pool can occupy 400 MB of physical RAM. O_DIRECT bypasses the page cache for data files and makes the buffer pool size the true upper bound on InnoDB’s memory use.
# Kernel side
vm.swappiness = 10
vm.vfs_cache_pressure = 50
vm.min_free_kbytes = 32768
# Prefer reclaiming page cache over swapping anonymous memory
vm.watermark_boost_factor = 0
vm.watermark_scale_factor = 125
How to Tell You Got It Wrong
Three signals, in order of how early they appear. Check them under your real workload, not at idle.
vmstat 1showssi/soabove zero for more than a few samples. Any sustained swap-in on this class of box means the budget is overdrawn. Reduce the buffer pool by 25% and re-measure before touching anything else./proc/vmstatpgscan_directis climbing. Direct reclaim means an allocation had to wait for the kernel to free pages synchronously. This is the latency killer that does not show up as swap.- InnoDB buffer pool hit ratio below 990 per 1,000 and falling. Some misses are fine and expected. A ratio that keeps declining across hours means the working set is growing past the pool and you need either more RAM or a smaller working set — a query or index problem, not a buffer pool problem.
grep -E 'pgscan_direct|pgsteal_direct' /proc/vmstat
mysql -e "SELECT 1 - (SELECT variable_value FROM performance_schema.global_status
WHERE variable_name='Innodb_buffer_pool_reads') /
(SELECT variable_value FROM performance_schema.global_status
WHERE variable_name='Innodb_buffer_pool_read_requests') AS miss_ratio;"
mysql -e "SELECT * FROM sys.memory_global_total;"
When to Stop Tuning and Add RAM
If your working set is genuinely 900 MB, no buffer pool size fixes that on a 1 GB guest — you are choosing between random reads and swap, both of which cost more than the price difference between the 1 GB and 2 GB plan. The break-even is straightforward: if reducing the buffer pool by 100 MB adds more than about 15% to your p99 query latency, the instance is undersized and tuning has become a way to hide a capacity problem.
Measure the working set before deciding: a hit ratio above 99% with a 200 MB pool means the hot set is small. For the instance-level context, our VPS plan sizing guide by workload maps working-set sizes to plans.

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