vSphere CPU limit hit (%MLMTD): the forgotten MHz cap that silently throttles a VM
A VM is slow. The application team reports degraded throughput. You check the usual suspects: guest CPU utilization is high, but that is expected for a busy workload. Host CPU utilization is moderate, nowhere near saturated. %RDY, the standard vSphere CPU contention signal, is low. Everything looks healthy from the hypervisor’s perspective, yet the VM is underperforming.
The missing signal is %MLMTD, the CPU max-limited counter. A CPU limit is an administrative MHz ceiling that silently throttles the VM. The guest OS cannot see it. The host has spare capacity. The scheduler is not contended. The VM hit a cap that someone configured and forgot about.
CPU limits get set during testing, inherited from templates, or applied via infrastructure-as-code pipelines, then never revisited. Teams that only monitor %RDY will never catch them because a limit-constrained VM shows low ready time. The throttle appears exclusively in cpu.maxlimited.summation.
What it means
%MLMTD measures the time a vCPU was runnable, had work to do, but was not scheduled because the VM hit its configured CPU limit. The limit is an absolute MHz ceiling. When the VM’s CPU demand exceeds that ceiling, the scheduler holds the vCPU back even if physical CPUs are idle.
The diagnostic pattern: a limit-constrained VM shows LOW %RDY but HIGH %MLMTD. Ready time tracks scheduling contention from the host side. Max-limited tracks throttling from the administrative cap. They are separate categories. If you only monitor ready time, the VM looks healthy because the host is not contended.
The fix is completely different from host contention. Host contention is solved by adding capacity, migrating VMs, or right-sizing vCPU counts. A limit is solved by removing or raising the cap. Applying the wrong fix wastes time.
esxtop shows %MLMTD in the CPU view. The vCenter performance counter is cpu.maxlimited.summation, a summation in milliseconds. Convert to a percentage the same way as ready time: (maxlimited_ms / (interval_ms * num_vCPUs)) * 100. Any nonzero value means the limit is being hit. Whether that is a problem depends on intent: a documented limit on a test VM is expected behavior. A forgotten limit on a production database is a silent incident.
flowchart TD
A["VM slow, guest CPU busy"] --> B["%RDY low or moderate"]
B --> C{"%MLMTD greater than 0?"}
C -->|"Yes, sustained"| D["CPU limit is the throttle"]
C -->|"No"| E["Not a limit issue"]
D --> F{"VM has explicit limit?"}
F -->|"Yes"| G["Remove or raise VM limit"]
F -->|"No"| H["Check parent resource pool"]
H --> I{"Pool has limit?"}
I -->|"Yes"| J["Remove or raise pool limit"]
I -->|"No"| K["Check template or IaC source"]Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Forgotten VM-level limit | Single VM slow, %MLMTD high, sibling VMs on same host fine | VM settings, CPU allocation, Limit field |
| Resource pool limit cascading to children | Multiple VMs in same pool show %MLMTD simultaneously | Parent resource pool CPU limit setting |
| Template-deployed limit | New VMs from a specific template throttle under load | Template CPU allocation settings |
| IaC-deployed explicit limit | Batch of VMs deployed together all show same throttle pattern | Resource definition in Terraform or other IaC |
| -1 vs 0 confusion | VM with limit set to 0 is effectively frozen, near-zero CPU throughput | Actual limit value in API or PowerCLI, not UI label |
Quick checks
# esxtop: check %MLMTD alongside %RDY per VM
# SSH to the ESXi host running the affected VM, then:
esxtop
# Press 'c' for CPU view
# Look at %MLMTD and %RDY columns per VM world
# Press 'f' to add or remove fields if %MLMTD is not visible
# A healthy VM with no limit shows %MLMTD = 0
# PowerCLI: check cpu.maxlimited.summation for a specific VM
Get-Stat -Entity (Get-VM "myvm") -Stat cpu.maxlimited.summation -Realtime -MaxSamples 6
# Convert: value_ms / (20000 * vCPU_count) * 100 = %MLMTD
# PowerCLI: check VM-level CPU limit (-1 means unlimited)
Get-VM "myvm" | Select Name, @{N='CpuLimitMHz';E={$_.ExtensionData.Config.CpuAllocation.Limit}}
# PowerCLI: find ALL VMs with a CPU limit (anything other than -1)
Get-VM | Where-Object {$_.ExtensionData.Config.CpuAllocation.Limit -ne -1} |
Select Name, @{N='CpuLimitMHz';E={$_.ExtensionData.Config.CpuAllocation.Limit}}
# PowerCLI: check resource pool CPU limits
Get-ResourcePool | Select Name, @{N='CpuLimitMHz';E={$_.ExtensionData.CpuAllocation.Limit}}
How to diagnose it
Confirm the VM is hitting a limit. In esxtop CPU view, check %MLMTD for the affected VM world. Any sustained nonzero value means the CPU limit is the throttle. The column may not be visible by default; press ‘f’ to add it.
Distinguish from host contention. Compare %MLMTD to %RDY for the same VM. If %MLMTD is elevated while %RDY is low, the host has spare capacity and the limit is the sole cause. If %RDY is also elevated, the VM may have both a limit problem and host contention.
Check the VM-level limit. In the vSphere Client, edit the VM settings, expand CPU, and check the Limit field. In PowerCLI, check ExtensionData.Config.CpuAllocation.Limit. A value of -1 means unlimited. Any positive value is a MHz cap.
If the VM itself has no limit, check the parent resource pool. Resource pool limits cascade to all child VMs. A VM can show high %MLMTD even though its own limit is unlimited, because the parent pool has a cap. Walk up the resource pool hierarchy to the cluster level.
Check the deployment source. If the VM was deployed from a template or via IaC, inspect the source. Search the codebase for any
cpu_limitorlimitassignment in Terraform, Pulumi, or other automation.
- Verify the unlimited value. The vSphere API uses -1 for unlimited, not 0. Setting a limit to 0 in the API creates a literal 0 MHz cap, which effectively freezes the VM. If a VM appears frozen with near-zero CPU throughput, check for a 0 limit value.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
| cpu.maxlimited.summation (%MLMTD) | Direct measure of limit throttling | Any sustained nonzero value on a production VM without documented intent |
| cpu.ready.summation (%RDY) | Context for interpreting the throttle | Low while %MLMTD is high confirms artificial cap, not host contention |
| VM CPU limit setting | The configuration that causes the throttle | Any value other than -1 without documentation |
| Resource pool CPU limit | Invisible cap inherited by child VMs | Any value other than -1 on pools containing production VMs |
| Host CPU utilization | Confirms whether the host has spare capacity | Moderate utilization (under 80%) with high %MLMTD confirms the throttle is administrative |
| Guest CPU utilization | Appears high inside the guest because the VM works hard within its throttled allocation | High guest CPU with low host CPU and high %MLMTD is the signature pattern |
Fixes
Remove or raise the VM-level limit
Set the VM’s CPU limit to unlimited (-1) or to a value that accommodates peak demand. This change takes effect without a reboot. Removing a limit only allows the VM to use what the host can provide. The risk is that the VM can now consume more host CPU during contention periods, which is better managed through shares.
Remove or raise the resource pool limit
If the throttle comes from a parent resource pool, raising or removing the pool’s limit affects all child VMs. Consider whether the pool was intentionally capped for multi-tenant isolation or cost allocation. If so, raise the cap rather than removing it, and document the new value. Walk the full pool hierarchy: a pool can inherit from a parent pool.
Audit IaC pipelines
If limits are deployed via Terraform, Pulumi, or other IaC tools, find and fix the source. Search the codebase for cpu_limit or limit assignments. Add a check to code review for any new VM or pool definition that sets a limit other than -1.
Use shares instead of limits
CPU limits are sometimes used as a crude capacity management tool: cap this VM at 4000 MHz so it cannot consume more. This is almost always the wrong approach. Shares provide proportional allocation during contention without artificial ceilings. The scheduler dynamically allocates CPU based on share priority when the host is busy, and lets VMs run freely when the host has spare capacity. If absolute isolation is needed, use reservations, not limits. Reservations guarantee minimum CPU without capping maximum CPU.
Prevention
- Audit all CPU limits quarterly. Run a PowerCLI sweep for any VM or resource pool with a limit other than -1. Confirm each one is intentional and documented.
- Monitor %MLMTD alongside %RDY. Any monitoring system that alerts on ready time must also collect max-limited. A limit-constrained VM looks healthy on ready time alone.
- Default templates to unlimited. VM templates used for production deployments should have CPU limit set to -1. If a template needs a limit for test environments, create a separate production clone.
- Review IaC defaults in code review. Any Terraform, Pulumi, or automation template that creates VMs or resource pools should explicitly set
cpu_limitto -1 unless there is a documented, reviewed reason to cap. - Alert on %MLMTD for production VMs. Any sustained nonzero value on a production VM warrants investigation. Treat it as a TICKET initially. Escalate if the VM owner reports performance issues. INFO severity applies if the limit is intentional and documented.
- Train teams on -1 vs 0. The unlimited value in the vSphere API is -1, not 0. A limit of 0 is a literal 0 MHz cap.
How Netdata helps
- Netdata collects cpu.maxlimited.summation and cpu.ready.summation together, making the low-ready-plus-high-max-limited pattern immediately visible without manual esxtop sessions on each host.
- High-frequency collection catches transient throttling spikes that vCenter’s rollups average away, which matters when intermittent workloads hit a limit for seconds out of every aggregation window.
- ML anomaly detection flags unexpected %MLMTD changes even before static thresholds fire, useful when a previously-unlimited VM inherits a limit from a resource pool reconfiguration.
- Correlating %MLMTD with host CPU utilization confirms whether the throttle is artificial (moderate host CPU, high %MLMTD) versus genuine contention (high host CPU, high %RDY with low %MLMTD).
- Alerting on both counters together prevents the most common blind spot: teams that only watch %RDY will never see a limit throttle because ready time stays low.






