I Found Three NVv3 VMs Still Running the Day After Retirement. Here’s the Hunt.

The migration tracker said 100%. Green across every subscription. Then the morning after NVv3 retirement, a render node in a forgotten subscription was still running on Standard_NV24s_v3.

The migration tracker said 100%. Green across every subscription. Signed off in August, closed the Jira epic, moved on.

Then on October 1st — the morning after the NVv3-series retirement — a render node in a subscription I’d half-forgotten about was still chewing through frames on a Standard_NV24s_v3. Two more sat deallocated nearby, created in early September by someone cloning a template that pinned the old SKU. None of them appeared in the migration plan because none of them existed when the plan was written.

That’s the thing about a tidy tracker. It audits what you knew about in June. It says nothing about what got spun up in September.

What actually retired, and when

On September 30, 2026, Microsoft retired the entire NVv3-series virtual machine family. Six SKUs, VDI and light-rendering GPU workhorses:

  • Standard_NV12s_v3
  • Standard_NV12hs_v3
  • Standard_NV24s_v3
  • Standard_NV24ms_v3
  • Standard_NV32ms_v3
  • Standard_NV48s_v3

Retirement is not the same as deletion. Your VMs did not vanish at midnight. What vanished is the obligation to keep them healthy. No SLA commitment you can lean on, no support case that ends in a fix, and — the part people underestimate — no guarantee the underlying hardware capacity stays available for that family in your region. A deallocated NVv3 VM that you try to start next month may simply fail to allocate because the host pool it depended on is being drained. You don’t get a warning the morning that happens.

So the running stragglers are a problem, and the deallocated ones are arguably worse, because they look fine until you need them.

Where the migration goes

Microsoft’s recommended landing spots depend on what the NVv3 was actually doing — and the honest answer is most people don’t know, which is half of why stragglers exist. The guidance points at three current GPU families:

  • NCads_A100_v4 and NC A100 v4 — A100-class compute for ML inference and heavier rendering. Overkill for a VDI session host, right-sized for the training job someone quietly moved onto a render node.
  • NCasT4_v3 — NVIDIA T4-backed, the sensible target for inference and lighter graphics work where you don’t need an A100’s price tag.

Do not reflexively map every NV12 to the cheapest thing that boots. The NVv3 family was a graphics SKU — these are compute-leaning families with different driver stacks, different licensing assumptions, and in the A100 case a very different bill. If the workload was genuinely visualization or VDI, check whether an NVadsA10 v5-class SKU fits better before you commit; the point is to match the workload, not to clear a dashboard. Test the driver install on the target before you touch production. GPU driver mismatches are where “quick resize” becomes a four-hour incident.

The query that finds what the tracker missed

Before anything else, know the true blast radius. This is Azure Resource Graph, it is read-only, and it crosses every subscription your identity can see.

run.shbash — zsh
az graph query -q "
resources
| where type =~ 'microsoft.compute/virtualmachines'
| extend vmSize = tostring(properties.hardwareProfile.vmSize)
| where vmSize in~ (
    'Standard_NV12s_v3','Standard_NV12hs_v3','Standard_NV24s_v3',
    'Standard_NV24ms_v3','Standard_NV32ms_v3','Standard_NV48s_v3')
| extend powerState = tostring(properties.extended.instanceView.powerState.code)
| project name, vmSize, powerState, resourceGroup, location, subscriptionId, id
| order by subscriptionId asc, name asc
" --first 1000 -o table

A few things make this worth running even if you’re sure you’re clean. The in~ and =~ operators are case-insensitive, because someone, somewhere, has a SKU string with inconsistent casing. The powerState projection separates your two problems: the VMs actively running on retired hardware, and the deallocated ones that will fail to allocate later. And it covers every subscription in scope — the dev/test sub the finance team owns, the sandbox a departed contractor set up, the DR subscription nobody logs into.

If you manage tenants through Lighthouse or just want to be sure of your scope, confirm what you’re actually searching first:

run.shbash — zsh
az account list --query "[].{name:name, id:id, state:state}" -o table

Resource Graph only sees subscriptions your principal has read access to. A VM in a subscription you can’t read is still a VM that will break — if your org has disconnected subscriptions, that’s a governance finding in its own right.

The checklist I ran on all three

Order matters here. I’ve seen people resize first and discover the backup was stale after the window closed.

  1. Confirm snapshot and backup coverage before you touch anything. A resize can fail mid-operation, and a redeploy to a new family is not reversible by hitting undo. Verify the VM is in a Recovery Services vault with a recent restore point, or take an explicit OS and data disk snapshot. Check the date of the last successful backup, not just that a policy is attached. A policy attached to a VM that’s been failing backups for three weeks is a false sense of security.
  2. Find the workload owner — a human, not a tag. Half the stragglers exist precisely because nobody owns them. If the owner field is empty, that VM is the one most likely to be a surprise-critical render node for a deadline you’ve never heard of. Deallocating “orphaned” resources without an owner conversation is how you take down production you didn’t know was production.
  3. Test the target SKU out of band. Stand up one instance of the target family, install the GPU driver, run the actual workload against it. Confirm the region even offers the target — A100 families are not in every region the old NVv3 lived in, and a cross-region move is a different, bigger project than a resize.
  4. Then execute — resize if the family is available in-place, redeploy if not. Default to --what-if / plan first, review the diff, then run it in a change window with the owner watching.
run.shbash — zsh
# Check the target SKU is actually available in the VM's region/zone first
az vm list-skus --location <region> --size Standard_NC --all -o table
​
# Resize — review before committing. Requires the target family in the region.
az vm resize \
  --resource-group <rg> \
  --name <vm> \
  --size Standard_NC24ads_A100_v4 \
  --no-wait --verbose

If you can’t migrate today — change freeze, owner on leave, target not yet validated — the only defensible holding action is to deallocate the straggler to stop it consuming retired-family capacity and to cap your exposure. Say it plainly to whoever owns it: deallocation is a tourniquet, not a fix. The support obligation is already gone, and the capacity you deallocate from is not guaranteed to be there when you want it back. A deallocated NVv3 is a migration you’ve deferred, not one you’ve avoided.

What I’d put in the postmortem

  1. Retirement trackers audit the past. Mine was accurate in August and wrong by October because new resources kept landing on a pinned SKU. Re-run the Resource Graph query on a schedule — a scheduled export or a daily pipeline job — not once at sign-off.
  2. The template was the real culprit. Two of my three stragglers came from an ARM/Bicep template that hard-coded Standard_NV24s_v3. Kill retired SKUs at the source: an Azure Policy deny on the retired sizes stops the next clone from recreating the problem faster than any audit can find it.
  3. Deallocated is not safe. Treat deallocated VMs on a retired family as higher risk than running ones, because they fail silently and only when you need them.
  4. Scope is governance. Every subscription your audit can’t see is a straggler you can’t find. If Resource Graph can’t read it, fix the access model — that gap is the actual vulnerability.

The tracker said 100%. The infrastructure said three. Infrastructure wins that argument every time.