↓ Skip to main content
  1. Posts/

Azure VM Price Increases: A Small App to Show What Your Estate Will Actually Cost

Author
Gregor Suttie
Passionate about all things Azure. Microsoft Azure MVP, blogger, speaker and community enthusiast based in Scotland.

When the notice about Azure VM and storage price changes landed, the first question I got from a couple of people was the obvious one: “OK, but what does that mean for us?” The announcement gives you a table of regions and percentages and a list of VM series that are exempt. What it doesn’t give you is an answer, because the answer depends on which VMs you actually run, in which regions, in which series, on which OS. West Europe at 9% sounds manageable until you remember half your estate is in North Europe at 17%, and that some of the VMs in there are on an excluded series and some aren’t.

You can work it out by hand. Export your VMs, look up each size, check each region against the table, check each series against the exclusion list, multiply, add up. I did it once for a single subscription and it took long enough that I decided never to do it again. So I wrote a small app that does it for me.

What the announced increase looks like
#

The figures come from the Microsoft Q&A clarification on VM and storage pricing changes, which says the increase takes effect on 1 February 2027 for these regions:

RegionIncrease
West Europe9%
North Europe17%
France Central11%
France South11%
Norway East9%
Norway West9%
Southeast Asia8%

Not every VM is hit. These series are excluded: Bv1, D, Ds, F, Fs, G, Gs, L, NP, HCS, Av2, Amv2, Dv2, Dsv2, Fsv2, Lsv2, Dsv7, Ddsv7, Dlsv7, Dldsv7, Esv7 and Edsv7. A VM in a listed region on one of those series pays the same as today. A VM on, say, a Dsv5 in North Europe pays 17% more.

So there are really three questions per VM: is it in an affected region, is its series excluded, and what does it cost today. Everything else is arithmetic.

What the app does
#

It’s a small Blazor Server app (.NET 10). You sign in with the Azure CLI, it finds every VM in every subscription you can see, prices each one, applies the increase where it applies, and shows you the result.

What you get on screen:

  • One table per subscription with VM name, resource group, region, size, series, OS, power state, current monthly cost, increase percentage, projected monthly cost, and the difference.
  • A status pill on every row: Affected, Excluded, Not in scope or Price unknown.
  • Subtotals per subscription and an overall summary: current spend, projected spend, and the monthly and annual increase.
  • A currency selector (USD, GBP, EUR and a handful of others).
  • A toggle for whether deallocated VMs count towards the totals.
  • A CSV download of everything, so you can hand it to someone who wants it in a spreadsheet anyway.

Running it is two commands:

az login
dotnet run --project src/VMPriceIncreases --launch-profile http

Then browse to http://localhost:5256. You need the .NET 10 SDK, the Azure CLI, and Reader on the subscriptions you want to see. It never changes anything in your environment.

How it works
#

There are only three moving parts, and each one had a small trap in it.

1. Finding the VMs with Resource Graph
#

The inventory comes from Azure Resource Graph rather than walking each subscription with the compute API. One query, across every subscription at once, paged with a skip token:

Resources
| where type =~ 'microsoft.compute/virtualmachines'
| project subscriptionId, resourceGroup, name, location,
          vmSize = tostring(properties.hardwareProfile.vmSize),
          osType = tostring(properties.storageProfile.osDisk.osType),
          powerState = tostring(properties.extended.instanceView.powerState.code)
| order by subscriptionId asc, resourceGroup asc, name asc

That one query gives me everything needed to price a VM: where it is, what size, which OS, and whether it’s running. It also means a tenant with a few thousand VMs comes back in a handful of calls rather than a few thousand.

2. Getting the price from the Retail Prices API
#

For the current price I use the public Azure Retail Prices API. It needs no authentication and supports a currency parameter, which is why the currency selector was cheap to add. The filter for each VM is:

serviceName eq 'Virtual Machines'
and armRegionName eq '{region}'
and armSkuName eq '{vmSize}'
and priceType eq 'Consumption'

The response is a list of meters, and it contains more than you want. Spot and Low Priority meters come back alongside the normal ones, and Windows and Linux are separate meters. The app throws away anything Spot or Low Priority, keeps only the hourly meters, and picks the Windows or Linux one depending on the VM’s OS. If both a plain VM meter and a Cloud Services meter match, it prefers the plain VM one.

Two things keep it quick. Prices are cached per currency, region, size and OS for the life of the process, so a hundred identical VMs cause one lookup, not a hundred. And lookups run in parallel with a throttle (eight at a time by default) so it doesn’t hammer the API. If a lookup fails, it’s removed from the cache so a refresh retries it, and the row shows as “Price unknown” rather than quietly showing zero.

Monthly cost is the hourly rate multiplied by 730 hours.

3. Turning a VM size into a series name
#

This was the fiddly bit. ARM gives you a size like Standard_DS2_v2. The notice talks about series, like Dsv2. The exclusion list is expressed in the notice’s naming, so I have to translate from one to the other before I can check it.

The sizes follow a convention, but with enough historical exceptions to be annoying:

  • Standard_D2s_v5 is family D, size 2, suffix s, version 5, so series Dsv5.
  • Standard_E4-2ds_v5 is a constrained-core size. The -2 has to be ignored, giving Edsv5.
  • Standard_B2ms has no version suffix at all. The notice calls it Bv1, and its ms isn’t part of that name.
  • Standard_DS2_v2 carries its premium-storage “S” as a capital letter in the family, so it has to be normalised to Dsv2. Same for GS4 becoming Gs.
  • _Promo sizes share the series of their base size.
  • Some letters, like r for RDMA and i for isolated, aren’t part of the published series name and are stripped.
  • HC and NP are named in the notice without following the suffix convention, so they’re special-cased.

It’s a single regular expression plus a handful of rules, and it has a test per rule. The test suite has around three dozen size-to-series cases covering the excluded v1, v2 and v7 families and a spread of affected ones, because getting this wrong silently means telling someone a VM is exempt when it isn’t (or the other way round).

Failing safe
#

The behaviour I spent the most time on is what happens when the app isn’t sure. A cost tool that quietly under-reports is worse than no tool, so the defaults lean towards over-counting:

  • If a size can’t be parsed into a series, the VM is treated as affected and the row is marked with a ? and a note saying why.
  • If there’s no list price for a size, region and OS combination, the row says “Price unknown” instead of £0.
  • VMs in regions that aren’t on the list show a 0% increase and a “Not in scope” status, so you can see they were considered rather than missed.

The numbers that drive all of this, the regional percentages, the effective date and the exclusion list, live in appsettings.json under PriceIncrease:

"PriceIncrease": {
  "EffectiveDate": "2027-02-01",
  "RegionIncreasePercent": {
    "westeurope": 9,
    "northeurope": 17,
    "francecentral": 11,
    "francesouth": 11,
    "norwayeast": 9,
    "norwaywest": 9,
    "southeastasia": 8
  },
  "ExcludedSeries": [ "Bv1", "D", "Ds", "F", "Fs", "G", "Gs", "L", "NP", "HCS", "Av2", "Amv2", "Dv2", "Dsv2", "Fsv2", "Lsv2", "Dsv7", "Ddsv7", "Dlsv7", "Dldsv7", "Esv7", "Edsv7" ]
}

If Microsoft adds regions, changes a percentage or updates the exclusion list, that’s a config edit, not a code change.

What it doesn’t tell you
#

It’s worth being clear about what the figures are, so nobody takes them to a finance meeting as gospel.

  • These are list prices, not your bill. The calculation uses pay-as-you-go retail rates times 730 hours. Reservations, savings plans, Azure Hybrid Benefit and dev/test pricing aren’t modelled, so if you’re covered by any of them your real number will be lower. Existing reservations keep their price until they expire.
  • Only the VM compute meter is priced. Disks, storage, networking and licences aren’t included, even though the same notice says some storage services are affected too. Treat the total as the compute portion of the increase, not the whole of it.
  • Deallocated VMs are excluded from totals by default, because they don’t incur a compute charge. They’re still listed, and there’s a toggle if you want them in.
  • It’s only as current as the source. If the announcement changes, the config has to change with it.

Final thoughts
#

This took very little code, and it answered the question I was being asked in a way a table of percentages couldn’t. The useful moment is usually the first run: you see which subscription carries most of the increase, you spot the handful of large VMs in North Europe that account for most of it, and you find a few VMs on an old series that turn out to be exempt. From there, the conversation moves on from “how bad is it” to “what do we do about it”, which might be moving workloads, committing to reservations, or simply knowing the number in advance.

If you run VMs in any of those seven regions, point it at your tenant, tick or untick the deallocated toggle, and have a look at the totals. It’s read-only, so there’s no risk in trying it.

Share this:

Related