{"id":1499,"date":"2026-10-24T22:32:48","date_gmt":"2026-10-24T22:32:48","guid":{"rendered":"https:\/\/virtualserversvps.com\/blog\/?p=1499"},"modified":"2026-10-24T22:32:48","modified_gmt":"2026-10-24T22:32:48","slug":"measuring-page-cache-hit-ratio-vmtouch-cachestat","status":"publish","type":"post","link":"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/","title":{"rendered":"Measuring Page-Cache Hit Ratio on a VPS with vmtouch and cachestat"},"content":{"rendered":"<p class=\"wp-block-paragraph\"><code>free -h<\/code> showed 412 MB of 3.8 GB &#8220;available&#8221; on a web node also holding 610 MB of page cache for static assets \u2014 and could not say whether that cache was helping. Two tools answer it: <code>vmtouch<\/code> for which files are resident, <code>cachestat<\/code> for what fraction of reads the page cache serves. Together they turn a memory graph into a hit ratio with a target.<\/p>\n\n<p class=\"wp-block-paragraph\">This walkthrough uses Debian 12, kernel 6.1, and a 3.8 GB <a href=\"https:\/\/virtualserversvps.com\/\">VPS<\/a> serving nginx from a 2.1 GB asset tree. Everything below is a command you can run on a live box without restarting anything.<\/p>\n\n<h2 class=\"wp-block-heading\">Why \/proc\/meminfo cannot give you a hit ratio<\/h2>\n\n<p class=\"wp-block-paragraph\"><code>MemAvailable<\/code>, <code>Cached<\/code> and <code>Buffers<\/code> are instantaneous snapshots, not rates. <code>Cached<\/code> rising tells you the cache is growing, not whether it is serving reads. The kernel does expose aggregate counters in <code>\/proc\/vmstat<\/code> \u2014 <code>pgpgin<\/code>, <code>pgpgout<\/code>, <code>pgmajfault<\/code> \u2014 but they are cumulative since boot and mix file-backed and anonymous pages, which makes rate computation awkward and misleading.<\/p>\n\n<pre class=\"wp-block-code\"><code>grep -E \"pgpgin|pgmajfault|nr_file_pages\" \/proc\/vmstat\nfree -m\n# MemAvailable is an estimate; it cannot say which files are cached<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">A page-cache hit costs roughly 100 ns of copy; a miss costs an 80 \u00b5s NVMe read. At 20,000 file reads per second a 10% shift in hit ratio means 2,000 extra device reads per second \u2014 idle disk versus saturated.<\/p>\n\n<h2 class=\"wp-block-heading\">cachestat: hit ratio as a rate<\/h2>\n\n<p class=\"wp-block-paragraph\"><code>cachestat<\/code> ships with bcc-tools and reads the kernel&#8217;s per-CPU cachestat counters in rate form. It prints hits\/misses\/dirty per one-second window; the ratio is hits\/(hits+misses).<\/p>\n\n<pre class=\"wp-block-code\"><code># apt install bcc-tools  (or bpfcc-tools on Debian)\ncachestat 1\n\n# example output\n#     HITS   MISSES  DIRTIES  HITRATIO   BUFFERS_MB  CACHED_MB\n#   142030     3120     1801    97.8%          192      3184\n#   138911     4102     2043    97.1%          192      3211<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">A cold cache after <code>echo 3 &gt; \/proc\/sys\/vm\/drop_caches<\/code> started at 21.4% and settled at 97.6\u201398.2%. The interesting figure is the 34 seconds to reach 95% \u2014 how long a reloaded worker serves slower responses. Never drop caches on production to &#8220;test performance&#8221;: it is a self-inflicted latency incident with a measurable duration.<\/p>\n\n<p class=\"wp-block-paragraph\">Run it under real traffic: on an idle box the ratio is dominated by systemd and cron reads. A useful run is 60 seconds at normal request rate, compared between configurations rather than trusted absolutely.<\/p>\n\n<h2 class=\"wp-block-heading\">vmtouch: which files are resident, and how much RAM they need<\/h2>\n\n<p class=\"wp-block-paragraph\"><code>vmtouch<\/code> queries the page cache directly via <code>mincore(2)<\/code> per page. On a directory it reports how much of the tree is resident and, with <code>-v<\/code>, which individual files are fully or partly cached.<\/p>\n\n<pre class=\"wp-block-code\"><code>apt install vmtouch\nvmtouch \/var\/www\/static          # summary: resident vs total\nvmtouch -v \/var\/www\/static | head -20   # per-file residency\nvmtouch -l \/var\/www\/static       # lock into RAM (needs CAP_IPC_LOCK)<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">After an hour of traffic <code>vmtouch<\/code> reported 1,842 of 2,151 MB resident (85.6%), the missing 309 MB concentrated in 41 rarely-requested files \u2014 exactly what LRU should evict. That is more actionable than any global <code>Cached<\/code> figure.<\/p>\n\n<p class=\"wp-block-paragraph\"><code>vmtouch -l<\/code> pins page cache the kernel cannot reclaim. On this 3.8 GB box, locking 2.1 GB left 1.3 GB for the application and produced an OOM kill within four hours. Use <code>-t<\/code> to preload without pinning, or keep the lock under 25% of RAM.<\/p>\n\n<h2 class=\"wp-block-heading\">Computing a hit ratio you can track over time<\/h2>\n\n<p class=\"wp-block-paragraph\"><code>cachestat<\/code> gives one-second samples; to track a ratio over days, run it under a wrapper that emits a line per interval to your metrics agent, or sample <code>\/proc\/vmstat<\/code> <code>pgpgin<\/code> deltas against application read counts.<\/p>\n\n<pre class=\"wp-block-code\"><code># 60-second mean hit ratio, parsed from cachestat\ncachestat 1 60 | awk 'NR&gt;2 {gsub(\/%\/,\"\",$4); h+=$4; n++} END {printf \"mean hit ratio: %.2f%% over %d samples\\n\", h\/n, n}'\n\n# per-process file read hit proxy via BPF (requires root)\nvfsstat 5 1<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\"><code>vfsstat 5 1<\/code> from bcc-tools breaks reads\/writes down by operation; a rising <code>Read<\/code> rate with a flat <code>Cached<\/code> growth is the signature of a working set that has outgrown RAM. Pair it with <code>nstat -az | grep -i drop<\/code> if the source is a network mount rather than local disk.<\/p>\n\n<h2 class=\"wp-block-heading\">Reading the numbers: what a good ratio looks like<\/h2>\n\n<p class=\"wp-block-paragraph\">There is no universal target, but the shape of the distribution is informative. Below are the same box under three traffic levels, with the working set fixed at 2.1 GB and RAM at 3.8 GB.<\/p>\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Traffic<\/th><th>cachestat hit ratio<\/th><th>vmtouch resident<\/th><th>Device read IOPS<\/th><th>p99 latency<\/th><\/tr><\/thead><tbody><tr><td>20 req\/s<\/td><td>98.1%<\/td><td>2,148 \/ 2,151 MB<\/td><td>38<\/td><td>9.4 ms<\/td><\/tr><tr><td>200 req\/s<\/td><td>97.4%<\/td><td>2,149 \/ 2,151 MB<\/td><td>410<\/td><td>11.2 ms<\/td><\/tr><tr><td>800 req\/s<\/td><td>94.8%<\/td><td>2,151 \/ 2,151 MB<\/td><td>1,720<\/td><td>19.8 ms<\/td><\/tr><tr><td>800 req\/s + 900 MB DB cache<\/td><td>71.3%<\/td><td>1,540 \/ 2,151 MB<\/td><td>9,300<\/td><td>63.0 ms<\/td><\/tr><\/tbody><\/table><\/figure>\n\n<p class=\"wp-block-paragraph\">The last row is the failure mode worth memorising: adding a database to the same 3.8 GB box pushed the asset working set out of cache, the resident fraction fell from 100% to 71.6%, and device read IOPS rose 5.4\u00d7. Nothing in <code>free -h<\/code> flagged it \u2014 <code>available<\/code> looked the same, because the pages had been reused rather than freed. Only the hit ratio and <code>vmtouch<\/code> residency showed the cause.<\/p>\n\n<p class=\"wp-block-paragraph\">A ratio in the mid-90s with residency near 100% means more RAM buys nothing. A ratio below 90% with residency under 85% means the working set does not fit: shrink it or buy memory. In between, <code>vmtouch -v<\/code> shows whether a few large files crowd out many small ones.<\/p>\n\n<h2 class=\"wp-block-heading\">Practical fixes when the ratio is low<\/h2>\n\n<ul class=\"wp-block-list\"><li><b>Shrink the working set.<\/b> Recompressing JPEGs and PNGs to WebP\/AVIF on this tree cut it from 2.1 GB to 940 MB, moving it comfortably inside RAM and taking the hit ratio to 99.4% under the 800 req\/s load.<\/li><li><b>Separate caches by workload.<\/b> A database buffer pool and a file cache compete for the same pages; on a box that must host both, cap one with cgroup <code>memory.high<\/code> so it cannot evict the other&#8217;s working set.<\/li><li><b>Use <code>noatime<\/code>.<\/b> On this box <code>atime<\/code> updates were generating metadata writes that forced pages out prematurely; mounting with <code>noatime<\/code> raised the steady-state ratio by 1.3 points.<\/li><li><b>Preload after deploys.<\/b> <code>vmtouch -t \/var\/www\/static<\/code> reads the tree into cache sequentially in about 2.4 seconds at 900 MB\/s, avoiding the 34-second re-warm window.<\/li><\/ul>\n\n<h2 class=\"wp-block-heading\">Putting it in a check<\/h2>\n\n<p class=\"wp-block-paragraph\">A five-minute check catches the regression before users do: emit the ratio and alert on a sustained drop below baseline minus five points.<\/p>\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/sh\n# \/usr\/local\/bin\/cache-ratio.sh\ncachestat 1 5 | awk 'NR&gt;2 {gsub(\/%\/,\"\",$4); s+=$4; n++} END {printf \"%.2f\\n\", s\/n}'\n\n# alert if below 90\nratio=$(\/usr\/local\/bin\/cache-ratio.sh)\nawk -v r=\"$ratio\" 'BEGIN {exit !(r &lt; 90)}&#039; &amp;&amp; \\\n  logger -t cachecheck &quot;page-cache hit ratio ${ratio}% below threshold&quot;<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">Keep the threshold in monitoring, not the script; the shell version makes the measurement reproducible on any host without a full agent. For how much RAM should go to cache versus application heap, see <a href=\"https:\/\/virtualserversvps.com\/\">virtualserversvps.com<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>free -h showed 412 MB of 3.8 GB &#8220;available&#8221; on a web node also holding 610 MB of page cache for static assets \u2014 and could not say whether that&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":1,"footnotes":""},"categories":[3],"tags":[],"class_list":["post-1499","post","type-post","status-publish","format-standard","hentry","category-performance-optimization"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v26.1 (Yoast SEO v26.1) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Measuring Page-Cache Hit Ratio on a VPS with vmtouch and cachestat - Virtual Servers VPS Blog<\/title>\n<meta name=\"description\" content=\"free -h showed 412 MB of 3.8 GB &quot;available&quot; on a web node also holding 610 MB of page cache for static assets \u2014 and could not say whether that cache was\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Measuring Page-Cache Hit Ratio on a VPS with vmtouch and cachestat\" \/>\n<meta property=\"og:description\" content=\"Measuring Page-Cache Hit Ratio on a VPS with vmtouch and cachestat\" \/>\n<meta property=\"og:url\" content=\"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/\" \/>\n<meta property=\"og:site_name\" content=\"Virtual Servers VPS Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-24T22:32:48+00:00\" \/>\n<meta name=\"author\" content=\"Virtual-Servers-Vps-Editor\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Virtual-Servers-Vps-Editor\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"5 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/\",\"name\":\"Measuring Page-Cache Hit Ratio on a VPS with vmtouch and cachestat - Virtual Servers VPS Blog\",\"isPartOf\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\"},\"datePublished\":\"2026-10-24T22:32:48+00:00\",\"author\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\"},\"description\":\"free -h showed 412 MB of 3.8 GB \\\"available\\\" on a web node also holding 610 MB of page cache for static assets \u2014 and could not say whether that cache was\",\"breadcrumb\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/virtualserversvps.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Measuring Page-Cache Hit Ratio on a VPS with vmtouch and cachestat\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/\",\"name\":\"Virtual Servers VPS Blog\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\",\"name\":\"Virtual-Servers-Vps-Editor\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"caption\":\"Virtual-Servers-Vps-Editor\"},\"sameAs\":[\"https:\/\/virtualserversvps.com\/blog\"],\"url\":\"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Measuring Page-Cache Hit Ratio on a VPS with vmtouch and cachestat - Virtual Servers VPS Blog","description":"free -h showed 412 MB of 3.8 GB \"available\" on a web node also holding 610 MB of page cache for static assets \u2014 and could not say whether that cache was","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/","og_locale":"en_US","og_type":"article","og_title":"Measuring Page-Cache Hit Ratio on a VPS with vmtouch and cachestat","og_description":"Measuring Page-Cache Hit Ratio on a VPS with vmtouch and cachestat","og_url":"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/","og_site_name":"Virtual Servers VPS Blog","article_published_time":"2026-10-24T22:32:48+00:00","author":"Virtual-Servers-Vps-Editor","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Virtual-Servers-Vps-Editor","Est. reading time":"5 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/","url":"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/","name":"Measuring Page-Cache Hit Ratio on a VPS with vmtouch and cachestat - Virtual Servers VPS Blog","isPartOf":{"@id":"https:\/\/virtualserversvps.com\/blog\/#website"},"datePublished":"2026-10-24T22:32:48+00:00","author":{"@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0"},"description":"free -h showed 412 MB of 3.8 GB \"available\" on a web node also holding 610 MB of page cache for static assets \u2014 and could not say whether that cache was","breadcrumb":{"@id":"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/virtualserversvps.com\/blog\/measuring-page-cache-hit-ratio-vmtouch-cachestat\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/virtualserversvps.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Measuring Page-Cache Hit Ratio on a VPS with vmtouch and cachestat"}]},{"@type":"WebSite","@id":"https:\/\/virtualserversvps.com\/blog\/#website","url":"https:\/\/virtualserversvps.com\/blog\/","name":"Virtual Servers VPS Blog","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0","name":"Virtual-Servers-Vps-Editor","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","caption":"Virtual-Servers-Vps-Editor"},"sameAs":["https:\/\/virtualserversvps.com\/blog"],"url":"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/"}]}},"_links":{"self":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1499","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/comments?post=1499"}],"version-history":[{"count":2,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1499\/revisions"}],"predecessor-version":[{"id":1505,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1499\/revisions\/1505"}],"wp:attachment":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/media?parent=1499"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/categories?post=1499"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/tags?post=1499"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}