InnoDB Buffer Pool vs Swap on a Small VPS: The Memory Arithmetic

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.

ConsumerMeasured RSS (MB)Notes
Kernel, slab, page tables90–120Grows with connection count and inode cache
sshd + systemd + cron + journald25–35journald RSS grows with SystemMaxUse
nginx (2 workers)18–28Plus ~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–95Baseline overhead is ~25 MB before data
mysqld non-buffer overhead180–260Per-connection buffers, dictionary cache, temp tables
Headroom for page cache and spikes60–90Never 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:

ConfigBuffer poolSwapTPSp99 query (ms)si/so (pages/s)
A700 MB2 GB, swappiness 60410118900–2,400
B400 MB2 GB, swappiness 10890410
C200 MB1 GB, swappiness 1760520
D200 MBzram 512 MB + 512 MB file, swappiness 10081547zram 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 1 shows si/so above 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/vmstat pgscan_direct is 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