InnoDB buffer pool sizing for WooCommerce: too small starves your server, too big gets MariaDB killed
· 16 min read
A client running a WooCommerce store — around 2,000 products, 250 orders per day — had been battling slow page loads for months. Their hosting provider's advice had been to upgrade the VPS. They'd gone from 4GB to 8GB to 16GB of RAM over six months. Each upgrade made no noticeable difference. Pages still took 5-6 seconds to load. The admin dashboard was worse — 8-10 seconds to open the WooCommerce orders screen.
The root cause was an InnoDB buffer pool that was far too small. I've also been called in for the exact opposite: a different client's store where the buffer pool was far too big for its server, and the Linux OOM killer terminated MariaDB mid-write, corrupting the database. Both incidents are in this post, because they're two ends of the same mistake — nobody sized the buffer pool to the database working set within the server's total memory budget. Too small wastes the RAM you're paying for. Too big gets your database killed.
Incident One: 16GB of RAM, 128MB of Buffer Pool
I was brought in after the third failed VPS upgrade. The server was a 16GB VPS running CloudPanel, Nginx, PHP 8.2-FPM, MariaDB 10.11, and Redis. On paper, this should handle a store this size without breaking a sweat.
I SSH'd in and ran free -m:
total used free shared buff/cache available
Mem: 16384 2847 11203 148 2334 13089
available: 13089
Eleven gigabytes of RAM sitting completely idle. On a properly tuned database server, you'd expect most of that consumed by buffer/cache. PHP-FPM was using about 1.2GB across its worker pool. Redis was allocated 512MB. Nginx was negligible. That left MariaDB — which should have been the biggest consumer of RAM on this server.
mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
+-------------------------+-----------+
| Variable_name | Value |
+-------------------------+-----------+
| innodb_buffer_pool_size | 134217728 |
+-------------------------+-----------+
134,217,728 bytes. That's 128MB — the default MariaDB ships with, designed for a shared hosting environment or a development machine, not a production WooCommerce server with 16GB of RAM.
What the Buffer Pool Actually Does
The InnoDB buffer pool is where MariaDB caches table data and index pages in memory. If a query's page is already in the pool — a "buffer pool hit" — the read happens at memory speed. If not, MariaDB reads it from disk, which is orders of magnitude slower. On a well-tuned server, the hit rate should be above 99%. On this server:
SHOW GLOBAL STATUS WHERE Variable_name IN (
'Innodb_buffer_pool_read_requests',
'Innodb_buffer_pool_reads',
'Innodb_buffer_pool_pages_free'
);
+--------------------------------------+------------+
| Variable_name | Value |
+--------------------------------------+------------+
| Innodb_buffer_pool_pages_free | 0 |
| Innodb_buffer_pool_read_requests | 48293741 |
| Innodb_buffer_pool_reads | 18472956 |
+--------------------------------------+------------+
Zero free pages, and the hit rate:
Hit rate = 1 - (reads / read_requests)
= 1 - (18472956 / 48293741)
= 61.8%
A 61.8% hit rate means 38% of all data page reads were going to disk. The database itself was 3.8GB, trying to fit into a 128MB buffer pool. MariaDB was constantly evicting cached pages via LRU eviction to make room for new ones; every page load touched different tables and indexes, so nothing stayed cached long enough to be useful.
Nobody caught this earlier because MariaDB doesn't complain about a small buffer pool. There's no warning in the error log. The server doesn't crash. It just quietly reads from disk instead of memory, and everything is slow.
The Fix on the 16GB Server
On a 16GB VPS running WordPress with PHP-FPM and Redis, I budget the RAM roughly like this:
- PHP-FPM: ~2GB (depends on worker count and per-process memory)
- Redis: 512MB
- OS and Nginx: ~1GB
- InnoDB buffer pool: 10-12GB
For this server, I set the buffer pool to 10GB — enough to hold the entire 3.8GB database in memory with substantial headroom:
# /etc/mysql/mariadb.conf.d/99-tuning.cnf
[mysqld]
innodb_buffer_pool_size = 10G
innodb_log_file_size = 1G
innodb_flush_method = O_DIRECT
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
The larger redo log (innodb_log_file_size = 1G) lets MariaDB batch more writes before flushing; the default 48MB causes excessive checkpoint flushes on active stores. O_DIRECT bypasses the OS page cache so data isn't buffered twice. innodb_io_capacity = 2000 matches the NVMe storage — the default of 200 assumes spinning disks.
After a restart and 30 minutes of warm-up, the hit rate went from 61.8% to 99.85%. Page load times dropped from 5-6 seconds to under 800ms. The WooCommerce orders screen went from 8-10 seconds to 1.2 seconds. The client had been paying for 16GB of RAM for months and MariaDB was only using 128MB of it.
So the answer is "make the buffer pool as big as possible", right? No. Here's what happens when you overshoot.
Incident Two: a 2GB Buffer Pool on a 4GB VPS
A different client's WooCommerce store went down at 3am on a Saturday: blank white page, "Error establishing a database connection." Nginx was running, PHP-FPM was running, the VPS itself was responsive. But MariaDB was dead, and systemctl start mariadb failed with a silent non-zero exit. The real story was in the journal:
sudo journalctl -u mariadb --since "3 hours ago" --no-pager
Two things jumped out. First, the OOM killer entry from 3:12am:
kernel: Out of memory: Killed process 14823 (mariadbd) total-vm:2847632kB, anon-rss:1689420kB, file-rss:0kB, shmem-rss:0kB, UID:27 pgtables:4612kB score:412
Then the failed restart, where MariaDB tried to come back up and immediately crashed:
mariadbd: InnoDB: Page [page id: space=4, page number=287] log sequence number 28441927168 is in the future! Current system log sequence number 28439012864.
mariadbd: InnoDB: Database page corruption on disk or a failed file read of tablespace shop/wp_options page [page id: space=4, page number=287]
The OOM killer had terminated MariaDB in the middle of writing to disk. InnoDB's write-ahead log and the data files were out of sync. The database was corrupted.
Why did the server run out of memory? This was a 4GB VPS running Nginx (roughly 50MB), PHP-FPM with pm.max_children = 25 at 60MB per worker (up to 1,500MB), MariaDB with innodb_buffer_pool_size = 2G, and Redis (256MB). That adds up to 3,854MB on a 4,096MB VPS — 242MB left for the operating system and everything else, with zero swap configured.
It worked on a normal day because PHP-FPM rarely hit all 25 workers and MariaDB rarely filled the whole buffer pool. Then a Friday-evening promotional email drove a traffic spike, PHP-FPM scaled to 22 concurrent workers, the kernel ran out of memory, and the OOM killer picked the biggest process — MariaDB, at 1.6GB resident. SIGKILL cannot be caught. No graceful shutdown, no final flush, no checkpoint.
And here's the part that makes it the mirror image of incident one: the entire database was 380MB on disk. A 2GB buffer pool for a 380MB dataset was wasting 1.6GB of RAM on empty cache space — RAM the server didn't have to spare.
Recovering from InnoDB Corruption
When crash recovery itself fails, the tool is innodb_force_recovery. Added under [mysqld]:
innodb_force_recovery = 1
MariaDB came up. Level 1 tells InnoDB to keep running even if it detects corrupt pages, rather than crashing — the gentlest mode, and enough to read data and run mysqldump. Writes are technically permitted at levels 1-3 since MariaDB 10.2.7, but I treat the database as read-only during recovery regardless — the goal is to get the data out, not to run the application against a damaged tablespace.
mysqlcheck --check --extended flagged three corrupt tables: wp_options, wp_wc_orders (the HPOS order storage), and wp_actionscheduler_actions — all high-write-frequency tables, the most likely to have open transactions when the kill landed. The remaining 60+ tables checked clean.
The recovery sequence:
# 1. Dump everything while force recovery holds the door open
mysqldump -u root -p --single-transaction --routines --triggers shop > /root/shop_recovery_$(date +%Y%m%d_%H%M%S).sql
# 2. Verify the dump completed (a truncated dump lacks this footer)
tail -5 /root/shop_recovery_20260518_034500.sql # must end with "-- Dump completed"
# 3. Record row counts in critical tables for comparison after restore
mysql -u root -p -e "SELECT COUNT(*) FROM shop.wp_options;"
mysql -u root -p -e "SELECT COUNT(*) FROM shop.wp_wc_orders;"
--single-transaction matters because force recovery mode does not support LOCK TABLES. Pre-restore counts: wp_options 4,847 rows, wp_wc_orders 31,206.
Then I removed the force recovery line, stopped MariaDB, and moved — not deleted — the corrupted tablespace files (ibdata1, the ib_logfiles, and the database directory) into a backup directory. Never destroy evidence of a crash until you're certain the recovery succeeded. MariaDB initialised fresh system tablespace files on start, I recreated the database with utf8mb4_unicode_ci, and restored the dump — about 4 minutes for a 1.2GB file.
Post-restore verification: wp_options came back with 4,831 rows — 16 lost from the corrupt pages, most likely transients WordPress would regenerate on the next page load. wp_wc_orders: all 31,206 orders intact — the corruption there had hit an index page rather than a data page. Total downtime: 47 minutes from the OOM kill to the site serving pages again.
For reference, because you'll need this at 3am and won't want to read documentation:
| Level | What it does | Safe to dump? |
|---|---|---|
| 1 | Ignores corrupt pages, continues running | Yes |
| 2 | Prevents the purge thread from running | Yes |
| 3 | Skips transaction rollback after crash recovery | Yes |
| 4 | Prevents insert buffer merge operations | Mostly — some data may be stale |
| 5 | Skips undo log processing on startup | Risky — data integrity not guaranteed |
| 6 | Skips redo log roll-forward on startup | Last resort — expect data loss |
Always start at 1. Only increase if MariaDB will not start at the current level. At 4 or above, compare row counts and verify critical data manually before trusting the restore.
Right-Sizing Both Servers' Memory
On the 4GB server, the permanent fix was the opposite of the 16GB one: I reduced the buffer pool from 2GB to 512MB — enough to hold the entire 380MB dataset in memory with headroom — and cut PHP-FPM from 25 workers to 15. Total committed memory came down to about 2,230MB, leaving 1.8GB of genuine breathing room instead of a 242MB knife-edge.
Two more defences, which I now apply to every VPS I manage:
Swap as a parachute. A VPS with zero swap goes straight from "memory pressure" to "kill something". I added a 2GB swapfile with vm.swappiness = 10 so the kernel only touches it under genuine pressure. Swap is not a performance tool — it's a crash-prevention tool. A server that swaps under load will be slow. A server that runs out of memory without swap will have its database killed and potentially corrupted. Slow is better than corrupt.
Move MariaDB to the back of the OOM queue. Via systemctl edit mariadb:
[Service]
OOMScoreAdjust=-500
This doesn't make MariaDB unkillable — that would be dangerous if it ever genuinely leaked memory. It just means the kernel kills PHP-FPM workers (stateless, instantly recoverable) before touching the database (which is neither).
How to Check Your Own Server
Here's the quick diagnostic I run on every server onboarding:
#!/bin/bash
# buffer-pool-check.sh — run as root or mysql user
echo "=== Buffer Pool Configuration ==="
mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';" | awk 'NR==2{printf "Buffer pool: %.0f MB\n", $2/1024/1024}'
echo ""
echo "=== Database Size ==="
mysql -e "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 0) AS 'Total DB Size (MB)' FROM information_schema.tables WHERE table_schema NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys');"
echo ""
echo "=== Buffer Pool Hit Rate ==="
mysql -e "SHOW GLOBAL STATUS WHERE Variable_name IN ('Innodb_buffer_pool_read_requests', 'Innodb_buffer_pool_reads');" | awk '
/read_requests/ {requests=$2}
/pool_reads/ {reads=$2}
END {
if (requests > 0) {
rate = (1 - reads/requests) * 100
printf "Hit rate: %.2f%%\n", rate
if (rate < 99) print "WARNING: Hit rate below 99% — buffer pool likely undersized"
}
}
'
echo ""
echo "=== Server RAM ==="
free -m | awk '/^Mem:/{printf "Total: %d MB | Used: %d MB | Available: %d MB\n", $2, $3, $7}'
The rule that both incidents point to: the buffer pool should comfortably hold your database's working set, and the total of MariaDB + PHP-FPM + Redis + OS must fit inside physical RAM with real headroom. As a baseline:
| Server RAM | Recommended Buffer Pool | Assumes |
|---|---|---|
| 2GB | 512MB | Small blog, shared with PHP-FPM |
| 4GB | 1-2GB | Single WooCommerce site |
| 8GB | 4-5GB | Active WooCommerce store |
| 16GB | 10-12GB | Large store or multiple sites |
| 32GB | 20-24GB | High-traffic WooCommerce or multisite |
These assume the database shares the machine with PHP-FPM, Nginx, and Redis — and they're a ceiling, not a target. If your entire database is 380MB, a 512MB pool on a 4GB VPS beats a 2GB one. On a dedicated database server, allocate up to 80% of RAM.
Why Control Panels Don't Fix This
CloudPanel, cPanel, Plesk, RunCloud — none of them tune MariaDB's buffer pool based on available RAM. They install MariaDB with its default configuration and leave it. The defaults are conservative because they're designed for the lowest common denominator: a shared server running dozens of sites where each database gets a sliver of memory.
On a VPS dedicated to one or two WordPress sites, the defaults are actively harmful in one direction — and a well-meaning "more cache is better" manual tweak, like the 2GB pool on the 4GB VPS, is harmful in the other. The server either has resources MariaDB doesn't know it can use, or commitments it can't honour.
Monitoring Both Failure Modes
For the undersized direction, I track the hit rate. A cron check every 15 minutes:
# /etc/cron.d/buffer-pool-monitor
*/15 * * * * root mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';" | awk '/read_requests/{r=$2} /pool_reads\t/{d=$2} END{if(r>0 && (1-d/r)<0.99) system("echo \"Buffer pool hit rate below 99%\" | mail -s \"MariaDB Alert\" [email protected]")}'
In Grafana via Telegraf's MySQL input, I watch Innodb_buffer_pool_read_requests (should be high), Innodb_buffer_pool_reads (physical disk reads — should be near zero), and the rate of change of Innodb_buffer_pool_wait_free. A sustained hit rate below 99% paired with a rising wait-free rate means the pool can't keep up. Monitor deltas between samples, not raw values — a cumulative counter that hasn't moved in hours is healthy regardless of its absolute number.
For the oversized direction, I monitor memory pressure before the OOM killer gets involved:
#!/bin/bash
FREE_MB=$(free -m | awk '/^Mem:/ {print $7}')
SWAP_USED=$(free -m | awk '/^Swap:/ {print $3}')
if [ "$FREE_MB" -lt 200 ]; then
echo "Low memory alert: ${FREE_MB}MB available" | mail -s "Memory Alert - $(hostname)" [email protected]
fi
if [ "$SWAP_USED" -gt 100 ]; then
echo "Swap usage alert: ${SWAP_USED}MB in use" | mail -s "Swap Alert - $(hostname)" [email protected]
fi
Any swap usage above 100MB is an early warning that the memory budget needs revisiting. I also compare MariaDB's uptime against the server's — if the database's is lower, it has restarted, and unplanned restarts need investigation.
The Bigger Lesson
The first client spent several hundred dollars upgrading their VPS three times, and every upgrade handed MariaDB more RAM it never used — the actual fix was a one-line configuration change. The second client's server was configured as if RAM were infinite, and the bill came due as a corrupted database at 3am.
Neither failure announces itself in advance. An undersized pool just makes everything quietly slow; an oversized one works fine until the traffic spike that pushes the kernel over the edge. Sizing the buffer pool to the actual working set — and checking that the whole server's memory budget still adds up — is the single highest-impact database tuning change you can make, and it costs nothing.
This is one of the first things I check during server onboarding for my maintenance clients, alongside connection limits — see the WooCommerce store where MariaDB ran out of connections for a different kind of database pressure.
If your WooCommerce store feels slow despite adequate hardware — or your VPS is running without swap, memory monitoring, or OOM protection — get in touch. This is exactly the kind of issue I find and fix during a server audit.
