MySQL9 min read

InnoDB Buffer Pool Sizing: A Field Guide for Production MySQL

The 75% rule is folklore, not a sizing method. This field guide covers working-set reality, online resizing, warmup dump and restore, and the two counters that actually tell you whether the buffer pool is big enough.

Every MySQL tuning guide repeats the same advice: set innodb_buffer_pool_size to 70 to 80 percent of RAM. I have sized buffer pools on machines from 4 GB to 768 GB, and the honest version is that the 75% number is a starting default for a dedicated database host, not a sizing method. It says nothing about your working set, your connection count, your per-session memory, or whether the box also runs a backup agent that periodically eats 20 GB of page cache.

This is a field guide to doing it properly on MySQL 8.0 and 8.4: what the buffer pool actually holds, how to measure whether it is too small, how to resize it online without an outage, and how to keep it warm across restarts.

What the buffer pool actually is

The buffer pool is InnoDB's cache of table and index pages, and in practice it is also where dirty pages wait to be flushed, where the change buffer lives, and where the adaptive hash index takes its memory. Unlike PostgreSQL, which leans heavily on the operating system page cache on top of a modest shared_buffers, InnoDB expects to own most of the memory itself and does its own read-ahead and eviction. I wrote up that architectural contrast in InnoDB buffer pool vs PostgreSQL shared_buffers; the short version is that undersizing the buffer pool hurts InnoDB much more than undersizing shared_buffers hurts Postgres, because InnoDB gets less help from the kernel.

The eviction policy is an LRU list split into a young and an old sublist. New pages land at the head of the old sublist and only get promoted if they are touched again after innodb_old_blocks_time milliseconds. That design exists so a single full table scan or a logical backup does not evict your hot working set. It mostly works, which is why "mysqldump ran and everything got slow" is rarer than people expect, and why when it does happen the cause is usually something else, like flushing pressure.

The 75% folklore vs the working set

The question that matters is not "what fraction of RAM," it is "does the hot working set fit." A 2 TB database with a 40 GB hot set runs beautifully in a 64 GB buffer pool. A 100 GB database where every row is touched daily will grind in that same pool. Percent-of-RAM tells you neither.

On a dedicated host, I start from total RAM and subtract the things that are not the buffer pool: the redo log and doublewrite activity need page cache unless you use O_DIRECT (you usually should), each connection can allocate sort, join, and read buffers on demand, the TempTable engine can take temptable_max_ram plus mmap space, and performance_schema takes a few hundred megabytes. On a 64 GB box serving 500 connections, 75% is often fine. On a 16 GB box with the same connection count, 75% can push you into the OOM killer's arms, and the kernel killing mysqld is a far worse day than a slightly smaller cache.

Shared hosts, containers, and Kubernetes pods deserve extra suspicion. Memory limits are enforced at the cgroup level, and MySQL 8.0 detects container limits imperfectly depending on version and configuration. Set the buffer pool explicitly; never let a heuristic decide inside a container.

Read the counters, not the ratio

The two status counters that matter are Innodb_buffer_pool_read_requests, which counts logical page reads, and Innodb_buffer_pool_reads, which counts the subset that missed the pool and had to hit storage. People collapse these into a hit ratio and then argue about whether 99.9% is good. I find the ratio actively misleading: on a busy system, 99.9% of a billion requests per hour is still a million disk reads per hour, which on network-attached storage can be your entire latency budget.

What I actually watch is the miss rate as an absolute number per second, trended over time:

SELECT variable_name, variable_value
FROM performance_schema.global_status
WHERE variable_name IN (
  'Innodb_buffer_pool_read_requests',
  'Innodb_buffer_pool_reads',
  'Innodb_buffer_pool_wait_free',
  'Innodb_buffer_pool_pages_free',
  'Innodb_buffer_pool_pages_dirty',
  'Innodb_buffer_pool_pages_total'
);

Sample these on an interval and look at deltas. A steady climb in Innodb_buffer_pool_reads per second while the workload is flat means the working set is outgrowing the pool. Innodb_buffer_pool_wait_free is the nastier signal: it counts the times a query had to wait because no clean page was available for eviction, which means you are not just cache-cold, you are flushing-bound. Any sustained nonzero rate there is worth an investigation.

Pages_free hovering near zero is normal and healthy; an LRU cache is supposed to be full. Do not alert on it.

Resizing online: chunks and instances

Since MySQL 5.7 the buffer pool is resizable at runtime, and on 8.0 this is routine. The mechanics have one rule worth understanding: the pool is allocated in chunks of innodb_buffer_pool_chunk_size (default 128 MB), and the total size must be a multiple of chunk size times innodb_buffer_pool_instances. If you request a size that does not divide evenly, MySQL silently rounds up, which surprises people doing capacity math.

-- check current geometry
SELECT @@innodb_buffer_pool_size / 1024 / 1024 / 1024 AS pool_gb,
       @@innodb_buffer_pool_chunk_size / 1024 / 1024 AS chunk_mb,
       @@innodb_buffer_pool_instances AS instances;

-- grow to 48 GiB online
SET GLOBAL innodb_buffer_pool_size = 48 * 1024 * 1024 * 1024;

-- watch progress
SHOW STATUS LIKE 'InnoDB_buffer_pool_resize_status';

Growing is cheap: InnoDB allocates new chunks and moves on. Shrinking is the risky direction, because InnoDB must relocate or evict every page living in the chunks being freed, and it takes locks on the pool while it does. I have watched a shrink on a hot system stall queries for tens of seconds. If you must shrink, do it off-peak, and expect the resize to pause while active transactions release the pages it wants.

One gotcha: innodb_buffer_pool_chunk_size itself can only be set at startup, and if your target pool size at boot is not a clean multiple, MySQL adjusts things for you in ways that can leave you with a chunk size you did not choose. Pick a chunk size that divides your intended sizes and leave it alone.

Warmup: dump and restore the page list

A cold buffer pool after a restart or failover is a real incident risk. A replica promoted with an empty pool can serve p99 latencies ten times normal until the working set pages back in, and on a big pool that can take an hour of organic traffic. InnoDB's answer is the buffer pool dump: it periodically writes the space and page IDs (not the data) of the most recently used pages to a file, and reloads those pages in bulk at startup.

On 8.0 this is on by default: innodb_buffer_pool_dump_at_shutdown and innodb_buffer_pool_load_at_startup are both enabled, and innodb_buffer_pool_dump_pct defaults to 25, meaning the top 25% most-recently-used pages per pool instance get recorded. For a latency-sensitive system I raise the percentage and, more importantly, dump on a schedule rather than only at shutdown, because a crash or an OOM kill never runs the shutdown dump. SET GLOBAL innodb_buffer_pool_dump_now = ON from a cron job is crude and effective. After a restart, SHOW STATUS LIKE 'Innodb_buffer_pool_load_status' tells you how far the reload has gotten, and it is worth gating your load balancer on that before sending full traffic to a rebooted node.

When a bigger pool will not save you

Buffer pool misses are the symptom people notice, but three failure modes masquerade as undersizing. First, a query regression: a dropped index or a bad plan that scans millions of pages will inflate read_requests and reads no matter how big the pool is; check your top statements before you check your memory, the same way you would triage with pg_stat_statements on Postgres (the MySQL side of that story is in MySQL slow query log vs pg_stat_statements). Second, flushing pressure: if dirty pages pile up because redo capacity or io_capacity is misconfigured, you get wait_free stalls that no amount of extra cache fixes. Third, connection memory: a pool sized to 80% of RAM plus 2,000 connections each doing 16 MB sorts is an OOM waiting for Black Friday.

My rule: prove the working set does not fit (rising reads per second at stable QPS, hot pages evicted between touches) before adding memory. Memory is the most expensive fix and the easiest to reach for.

Monitoring this with MonPG

MonPG today is a PostgreSQL monitoring platform, and I want to be straight about that: it does not monitor MySQL yet. We are actively building MySQL monitoring (coming soon), and buffer pool visibility is exactly the kind of signal it is being designed around: miss rate as an absolute trend rather than a vanity hit ratio, wait_free events surfaced as first-class stall evidence, and resize and warmup progress tracked across restarts so a cold failover is visible before customers feel it. If you run PostgreSQL alongside MySQL, the same evidence-first workflow already exists for Postgres and you can start there today. Either way, the sizing method in this guide needs nothing more than the counters above and somewhere durable to trend them.