How a 128MB InnoDB Buffer Pool Crippled a WooCommerce Store on a 16GB VPS

· 10 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.

I was brought in after the third failed 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.

The First Thing That Looked Wrong

I SSH'd in and ran free -m to see how the RAM was being used:

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 to see most of that consumed by buffer/cache. Something wasn't using the memory it had available.

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 value that 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. When a query needs to read a row, MariaDB first checks whether that data page is already in the buffer pool. If it is — a "buffer pool hit" — the read happens at memory speed. If not — a "buffer pool miss" — MariaDB reads the page from disk, which is orders of magnitude slower.

On a well-tuned server, the buffer pool hit rate should be above 99%. That means fewer than 1 in 100 reads need to touch the disk. On this server:

SHOW GLOBAL STATUS WHERE Variable_name IN (
  'Innodb_buffer_pool_read_requests',
  'Innodb_buffer_pool_reads',
  'Innodb_buffer_pool_pages_total',
  'Innodb_buffer_pool_pages_data',
  'Innodb_buffer_pool_pages_free'
);
+--------------------------------------+------------+
| Variable_name                        | Value      |
+--------------------------------------+------------+
| Innodb_buffer_pool_pages_data        | 8071       |
| Innodb_buffer_pool_pages_free        | 0          |
| Innodb_buffer_pool_pages_total       | 8191       |
| Innodb_buffer_pool_read_requests     | 48293741   |
| Innodb_buffer_pool_reads             | 18472956   |
+--------------------------------------+------------+

Zero free pages — the buffer pool was completely full. And the hit rate:

Hit rate = 1 - (reads / read_requests)
         = 1 - (18472956 / 48293741)
         = 1 - 0.382
         = 61.8%

A 61.8% hit rate means 38% of all data page reads were going to disk. On every single page load, MariaDB was making thousands of disk reads that should have been served from memory.

Why This Happens

The database itself was 3.8GB:

mysql -e "SELECT table_schema AS 'Database',
  ROUND(SUM(data_length + index_length) / 1024 / 1024, 0) AS 'Size (MB)'
  FROM information_schema.tables
  WHERE table_schema NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
  GROUP BY table_schema;"
+-------------------+-----------+
| Database          | Size (MB) |
+-------------------+-----------+
| woocommerce_live  | 3814      |
+-------------------+-----------+

A 3.8GB database trying to fit into a 128MB buffer pool. MariaDB was constantly evicting cached pages to make room for new ones — a process called LRU (Least Recently Used) eviction. Every page load touched different tables and indexes, so the buffer pool was churning constantly. Nothing stayed cached long enough to be useful.

The reason this wasn't caught earlier is that 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 hosting provider saw "database is responding" and concluded the problem was elsewhere.

The Fix

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 database in memory with substantial headroom for growth:

# /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

A few notes on the additional settings:

  • innodb_log_file_size = 1G: Larger redo logs mean MariaDB can batch more writes before flushing to disk. The default 48MB causes excessive checkpoint flushes on active WooCommerce stores.
  • innodb_flush_method = O_DIRECT: Bypasses the OS page cache for InnoDB data files. Without this, data gets double-buffered — once in the InnoDB buffer pool and once in the OS cache. O_DIRECT eliminates that waste.
  • innodb_io_capacity = 2000: Tells MariaDB how many IOPS the underlying storage can handle. The default of 200 is appropriate for spinning disks. This server had NVMe SSDs.

Then restart MariaDB:

sudo systemctl restart mariadb

On MariaDB 10.11+, you can resize the buffer pool dynamically without a restart:

SET GLOBAL innodb_buffer_pool_size = 10737418240;

But the other settings require a restart, so I did it all at once.

The Result

I waited 30 minutes for the buffer pool to warm up — MariaDB populates it as queries come in, not all at once — then checked the stats again:

SHOW GLOBAL STATUS WHERE Variable_name IN (
  'Innodb_buffer_pool_read_requests',
  'Innodb_buffer_pool_reads',
  'Innodb_buffer_pool_pages_total',
  'Innodb_buffer_pool_pages_data',
  'Innodb_buffer_pool_pages_free'
);
+--------------------------------------+------------+
| Variable_name                        | Value      |
+--------------------------------------+------------+
| Innodb_buffer_pool_pages_data        | 248916     |
| Innodb_buffer_pool_pages_free        | 406284     |
| Innodb_buffer_pool_pages_total       | 655360     |
| Innodb_buffer_pool_read_requests     | 2847392    |
| Innodb_buffer_pool_reads             | 4213       |
+--------------------------------------+------------+

Hit rate: 99.85%. From 61.8% to 99.85%. The entire working set of the database was now in memory. Free pages showed the buffer pool wasn't even half full — plenty of room for growth.

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.

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}'

If the hit rate is below 99% and your server has available RAM, the buffer pool is undersized. 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 server shares the machine with PHP-FPM, Nginx, and Redis. On a dedicated database server, allocate up to 80% of RAM to the buffer pool.

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. The server has the resources, but MariaDB doesn't know it's allowed to use them.

Ongoing Monitoring

I add a buffer pool hit rate check to every server's monitoring stack. A simple cron job that alerts if the rate drops below 99%:

# /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]")}'

For a more complete picture, I track these metrics in Grafana via Telegraf's MySQL input plugin:

  • Innodb_buffer_pool_read_requests (logical reads — should be high)
  • Innodb_buffer_pool_reads (physical disk reads — should be near zero)
  • Innodb_buffer_pool_pages_free (a healthy pool will have some free pages, but zero alone isn't an emergency — it just means InnoDB has filled the pool with cached data)
  • Innodb_buffer_pool_wait_free (cumulative count of waits since startup — track the rate of change, not the absolute value)

The key metric is the hit rate over time. A sustained drop below 99% paired with a rising Innodb_buffer_pool_wait_free rate means the buffer pool can't keep up with the workload. Monitor deltas between samples rather than raw values — a cumulative counter that hasn't moved in hours is healthy, regardless of its absolute number.

The Bigger Lesson

This client spent several hundred dollars upgrading their VPS three times. Each upgrade gave them more RAM that MariaDB never used. The actual fix was a one-line configuration change that took effect in seconds.

If you're managing a WordPress or WooCommerce site on a VPS and you've never touched MariaDB's configuration, your buffer pool is almost certainly at 128MB. Check it. It's the single highest-impact database tuning change you can make, and it costs nothing — you're already paying for the RAM.

This is one of the first things I check during server onboarding for my maintenance clients. A properly sized buffer pool isn't optional — it's the difference between a database that runs from memory and one that grinds through every query from disk.

If your WooCommerce store feels slow despite adequate hardware, get in touch — this is exactly the kind of issue I find and fix during a server audit.

Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans Get in Touch

Get in Touch to Discuss Your Needs