{"id":1260,"date":"2026-09-30T23:10:45","date_gmt":"2026-09-30T23:10:45","guid":{"rendered":"https:\/\/virtualserversvps.com\/blog\/?p=1260"},"modified":"2026-09-30T23:10:45","modified_gmt":"2026-09-30T23:10:45","slug":"cgroup-v2-io-latency-io-max-disk-qos-vps","status":"publish","type":"post","link":"https:\/\/virtualserversvps.com\/blog\/cgroup-v2-io-latency-io-max-disk-qos-vps\/","title":{"rendered":"Enforcing Disk I\/O QoS with cgroup v2 io.latency and io.max on Noisy Instances"},"content":{"rendered":"<p class=\"wp-block-paragraph\">CPU contention has a scheduler with fair shares and quotas. Disk contention historically had almost nothing: a single backup job saturating the block device could push your database&#8217;s read latency from 1 ms to 300 ms while every process on the box appeared to be under a cgroup limit. On cgroup v2, two controllers close that gap. <code>io.latency<\/code> guarantees a target latency for a workload; <code>io.max<\/code> imposes hard bandwidth and IOPS ceilings. Together they let you isolate disk access the same way you isolate CPU and memory.<\/p>\n\n<h2 class=\"wp-block-heading\">Check That cgroup v2 and the IO Controller Are Live<\/h2>\n\n<pre class=\"wp-block-code\"><code># Must print cgroup2fs\nstat -fc %T \/sys\/fs\/cgroup\/\n\n# Confirm the io controller is delegated\ncat \/sys\/fs\/cgroup\/cgroup.controllers\n# expect: cpuset cpu io memory pids ...\n\n# Enable controllers on the root for child use\n echo \"+io\" &gt; \/sys\/fs\/cgroup\/cgroup.subtree_control\n<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">If <code>\/sys\/fs\/cgroup<\/code> reports <code>tmpfs<\/code>, you are still on cgroup v1 and must boot with <code>systemd.unified_cgroup_hierarchy=1<\/code>. Most current distributions default to v2, but minimal VPS images occasionally ship hybrid mode. Verify the controller appears in <code>cgroup.controllers<\/code> before setting any limit &#8211; writes to <code>io.max<\/code> on a cgroup whose parent has not delegated the controller fail silently or with an <code>EACCES<\/code>, and the limit never applies.<\/p>\n\n<h2 class=\"wp-block-heading\">Identify the Device and Its Major:Minor<\/h2>\n\n<p class=\"wp-block-paragraph\">Every IO limit is per-device, addressed by the device&#8217;s major and minor number, not by name. On a VPS the root volume is usually a virtio block device (<code>vda<\/code>), but additional volumes may be <code>vdb<\/code>, an NVMe device, or an LVM mapping carrying the <code>dm-<\/code> major. Reading the wrong number is the most common reason a limit appears to have no effect.<\/p>\n\n<pre class=\"wp-block-code\"><code>lsblk -o NAME,MAJ:MIN,SIZE,TYPE,MOUNTPOINT\n# vda   252:0  80G disk\n# \u2514\u2500vda1 252:1  80G part \/\n\n# Cross-check against the kernel partition table\n cat \/proc\/partitions\n\n# Which device backs a given mount\n df -h \/var\/lib\/mysql\n lsblk -no MAJ:MIN $(findmnt -no SOURCE \/var\/lib\/mysql | xargs basename 2&gt;\/dev\/null) 2&gt;\/dev\/null || true\n<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">Note that limits can be set on either the whole disk (<code>252:0<\/code>) or a partition (<code>252:1<\/code>). Setting a limit on a partition throttles only I\/O issued to that partition; writes to the raw disk bypass it. For a database with its own dedicated volume, applying the limit to the full device is the safer choice because nothing can accidentally escape the cap.<\/p>\n\n<h2 class=\"wp-block-heading\">io.max: Hard Ceilings per Device<\/h2>\n\n<p class=\"wp-block-paragraph\"><code>io.max<\/code> accepts per-device limits on read\/write bytes per second and IOPS. The four keys are <code>rbps<\/code>, <code>wbps<\/code>, <code>riops<\/code>, and <code>wiops<\/code>; you may set any subset, and <code>max<\/code> clears a limit.<\/p>\n\n<pre class=\"wp-block-code\"><code># Create a slice for the backup job\nsystemd-run --slice=backup.slice --unit=nightly-backup --scope true\n\n# Cap the slice: 20 MiB\/s write, 2000 write IOPS on device 252:0\n echo '252:0 wbps=20971520 wiops=2000' &gt; \/sys\/fs\/cgroup\/backup.slice\/io.max\n\n# Read back the effective limits\ncat \/sys\/fs\/cgroup\/backup.slice\/io.max\n# 252:0 rbps=max wbps=20971520 riops=max wiops=2000\n\n# Remove one limit\n echo '252:0 wbps=max' &gt; \/sys\/fs\/cgroup\/backup.slice\/io.max\n<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">Setting <code>io.max<\/code> on a slice applies to every process inside it and to any child cgroups. Because the limit is inherited by children, a runaway worker spawned by the backup job cannot escape the ceiling. This is the correct tool for a bulk job whose only sin is consuming the disk &#8211; it needs bytes moved, not the fastest path. Divide the device&#8217;s measured throughput, not its theoretical peak, when choosing a number: on a volume that delivers 300 MB\/s sequentially, capping a backup at 20 MB\/s leaves generous headroom for interactive workloads.<\/p>\n\n<h2 class=\"wp-block-heading\">io.latency: Guaranteeing the Critical Path<\/h2>\n\n<p class=\"wp-block-paragraph\"><code>io.latency<\/code> works the other way round. Instead of capping the offender, you declare an acceptable latency for the workload that must not degrade, and the kernel throttles anything sharing that device until the target is met. It is a target-based controller: it observes the completion latency of each cgroup&#8217;s I\/O and applies back-pressure to the cgroups whose target is not the strictest.<\/p>\n\n<pre class=\"wp-block-code\"><code># Database slice: keep read latency under 10 ms\n echo '252:0 target=10000' &gt; \/sys\/fs\/cgroup\/db.slice\/io.latency\n\n# The backup slice tolerates more\n echo '252:0 target=100000' &gt; \/sys\/fs\/cgroup\/backup.slice\/io.latency\n\n# Inspect the configured targets\ncat \/sys\/fs\/cgroup\/db.slice\/io.latency\n# 252:0 target=10000\n<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">Values are in microseconds. The lowest target wins priority: the kernel measures observed latency for each cgroup and applies pressure to the others when the target is exceeded. Set a strict target for the database and a generous one for batch work, and the database keeps its 10 ms window even while a backup runs. Note that the strictest target on the device effectively acts as a global latency budget &#8211; if you set 10 ms for the database, the controller will throttle the backup regardless of the backup&#8217;s own target, which is exactly the isolation you want on a single-volume VPS.<\/p>\n\n<h2 class=\"wp-block-heading\">Wiring It Into systemd Services<\/h2>\n\n<ul class=\"wp-block-list\"><li><code>IOAccounting=yes<\/code> on the service and its slice, so <code>systemd-cgtop<\/code> shows per-service I\/O.<\/li><li><code>IOWeight=<\/code> for proportional sharing when you do not need a hard number.<\/li><li><code>IOMax=<\/code> for device-scoped caps in the unit file, expressed as <code>wbps=<\/code>, <code>riops=<\/code>, and so on.<\/li><li><code>IOSchedulingClass=idle<\/code> for jobs that should only use leftover disk time.<\/li><\/ul>\n\n<pre class=\"wp-block-code\"><code># \/etc\/systemd\/system\/nightly-backup.service\n[Service]\nType=oneshot\nExecStart=\/usr\/local\/bin\/backup.sh\nIOSchedulingClass=idle\nIOWeight=20\nIOMax=252:0wbps=20971520\nIOAccounting=yes\n\n# Verify what systemd actually applied\nsystemctl show nightly-backup.service -p IOWeight -p IOAccounting\nsystemd-cgtop --order=io\n<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">For stateless batch jobs, <code>IOSchedulingClass=idle<\/code> alone often suffices: the kernel gives the process only leftover disk time and interactive traffic keeps its responsiveness. Reserve <code>io.max<\/code> and <code>io.latency<\/code> for workloads that must be fast enough to meet a deadline rather than merely not interfere, and prefer <code>IOWeight<\/code> when you want proportional sharing instead of a cliff at a hard limit.<\/p>\n\n<h2 class=\"wp-block-heading\">Verify the Limits Are Working<\/h2>\n\n<pre class=\"wp-block-code\"><code># Per-cgroup read\/write bytes, IOPS, and discarded I\/O\ncat \/sys\/fs\/cgroup\/backup.slice\/io.stat\n# 252:0 rbytes=... wbytes=... rios=... wios=...\n\n# Stall information for the cgroup\ncat \/sys\/fs\/cgroup\/backup.slice\/io.pressure\n\n# Live per-device throughput and queue depth\niostat -x 1 | grep -E 'Device|vda'\n<\/code><\/pre>\n\n<p class=\"wp-block-paragraph\">If <code>io.stat<\/code> shows the backup slice writing far below its cap, check that the limit landed on the correct major:minor and that the process is genuinely inside that slice &#8211; a job launched by cron inherits the cron slice, not the one you built for it. Move it under systemd or use <code>systemd-run --slice=<\/code> from the cron job itself. A common trap is launching the daemon from an interactive shell: the process inherits the login session&#8217;s cgroup and your carefully constructed slice sits empty.<\/p>\n\n<h2 class=\"wp-block-heading\">Limits, Not Miracles<\/h2>\n\n<p class=\"wp-block-paragraph\">Disk QoS is a contention-management tool, not a capacity upgrade. On an instance whose storage tier is already saturated by host-level noisy neighbours, guest-side limits cannot manufacture IOPS that are not offered. Measure raw device latency with <code>fio<\/code> first, and if the ceiling is set by the provider, move the workload to a faster volume class. Our <a href=\"https:\/\/virtualserversvps.com\/managed-vs-unmanaged\">managed versus unmanaged service comparison<\/a> covers where that kind of host-side isolation is included, and the <a href=\"https:\/\/virtualserversvps.com\/\">VPS provisioning guides<\/a> discuss selecting storage by measured IOPS rather than headline type. Applied together, <code>io.latency<\/code> and <code>io.max<\/code> turn disk contention from an unpredictable, machine-wide problem into a configured policy: the critical path keeps its latency target, the batch jobs finish on budget, and neither one has to know the other exists.<\/p>","protected":false},"excerpt":{"rendered":"<p>Stop one runaway process from starving everything else on the disk. Use io.latency target-latency control and io.max bandwidth caps to enforce per-workload disk QoS on cgroup v2.<\/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-1260","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>Enforcing Disk I\/O QoS with cgroup v2 io.latency and io.max on Noisy Instances - Virtual Servers VPS Blog<\/title>\n<meta name=\"description\" content=\"Stop one runaway process from starving everything else on the disk. Use io.latency target-latency control and io.max bandwidth caps to enforce per-workload disk QoS on cgroup v2.\" \/>\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\/cgroup-v2-io-latency-io-max-disk-qos-vps\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Enforcing Disk I\/O QoS with cgroup v2 io.latency and io.max on Noisy Instances\" \/>\n<meta property=\"og:description\" content=\"Enforcing Disk I\/O QoS with cgroup v2 io.latency and io.max on Noisy Instances\" \/>\n<meta property=\"og:url\" content=\"https:\/\/virtualserversvps.com\/blog\/cgroup-v2-io-latency-io-max-disk-qos-vps\/\" \/>\n<meta property=\"og:site_name\" content=\"Virtual Servers VPS Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-30T23:10:45+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=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/cgroup-v2-io-latency-io-max-disk-qos-vps\/\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/cgroup-v2-io-latency-io-max-disk-qos-vps\/\",\"name\":\"Enforcing Disk I\/O QoS with cgroup v2 io.latency and io.max on Noisy Instances - Virtual Servers VPS Blog\",\"isPartOf\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\"},\"datePublished\":\"2026-09-30T23:10:45+00:00\",\"author\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\"},\"description\":\"Stop one runaway process from starving everything else on the disk. Use io.latency target-latency control and io.max bandwidth caps to enforce per-workload disk QoS on cgroup v2.\",\"breadcrumb\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/cgroup-v2-io-latency-io-max-disk-qos-vps\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/virtualserversvps.com\/blog\/cgroup-v2-io-latency-io-max-disk-qos-vps\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/cgroup-v2-io-latency-io-max-disk-qos-vps\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/virtualserversvps.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Enforcing Disk I\/O QoS with cgroup v2 io.latency and io.max on Noisy Instances\"}]},{\"@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":"Enforcing Disk I\/O QoS with cgroup v2 io.latency and io.max on Noisy Instances - Virtual Servers VPS Blog","description":"Stop one runaway process from starving everything else on the disk. Use io.latency target-latency control and io.max bandwidth caps to enforce per-workload disk QoS on cgroup v2.","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\/cgroup-v2-io-latency-io-max-disk-qos-vps\/","og_locale":"en_US","og_type":"article","og_title":"Enforcing Disk I\/O QoS with cgroup v2 io.latency and io.max on Noisy Instances","og_description":"Enforcing Disk I\/O QoS with cgroup v2 io.latency and io.max on Noisy Instances","og_url":"https:\/\/virtualserversvps.com\/blog\/cgroup-v2-io-latency-io-max-disk-qos-vps\/","og_site_name":"Virtual Servers VPS Blog","article_published_time":"2026-09-30T23:10:45+00:00","author":"Virtual-Servers-Vps-Editor","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Virtual-Servers-Vps-Editor","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/virtualserversvps.com\/blog\/cgroup-v2-io-latency-io-max-disk-qos-vps\/","url":"https:\/\/virtualserversvps.com\/blog\/cgroup-v2-io-latency-io-max-disk-qos-vps\/","name":"Enforcing Disk I\/O QoS with cgroup v2 io.latency and io.max on Noisy Instances - Virtual Servers VPS Blog","isPartOf":{"@id":"https:\/\/virtualserversvps.com\/blog\/#website"},"datePublished":"2026-09-30T23:10:45+00:00","author":{"@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0"},"description":"Stop one runaway process from starving everything else on the disk. Use io.latency target-latency control and io.max bandwidth caps to enforce per-workload disk QoS on cgroup v2.","breadcrumb":{"@id":"https:\/\/virtualserversvps.com\/blog\/cgroup-v2-io-latency-io-max-disk-qos-vps\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/virtualserversvps.com\/blog\/cgroup-v2-io-latency-io-max-disk-qos-vps\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/virtualserversvps.com\/blog\/cgroup-v2-io-latency-io-max-disk-qos-vps\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/virtualserversvps.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Enforcing Disk I\/O QoS with cgroup v2 io.latency and io.max on Noisy Instances"}]},{"@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\/1260","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=1260"}],"version-history":[{"count":1,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1260\/revisions"}],"predecessor-version":[{"id":1266,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1260\/revisions\/1266"}],"wp:attachment":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/media?parent=1260"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/categories?post=1260"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/tags?post=1260"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}