Reduce Azure Compute Gallery storage costs by 60-90% through image version cleanup, replication right-sizing, and Citrix DaaS replica optimization.
Azure Compute Gallery does not have its own line item on your invoice, which is exactly why gallery costs tend to drift. Every image replica in every target region is a managed disk charge buried in your storage bill, and cross-region replication adds egress on top. Organizations that replicate images to regions where no VMs consume them, or retain outdated image versions indefinitely, accumulate storage costs that scale with replica count, region count, and version count.
Based on what we typically see, the two most effective optimizations are removing legacy image versions that no deployment references and right-sizing replica counts and target regions to match actual provisioning demand. Spotto's cloud optimization platform continuously identifies these overprovisioned gallery configurations and orphaned image versions, surfacing targeted recommendations that can reduce gallery storage costs by around 20-30% without impacting deployment performance.
Key Takeaways at a Glance:
Azure Compute Gallery has no standalone service fee. Every cost is an underlying managed disk charge per image replica per region. This indirect pricing model creates a blind spot.
Two problems compound:
Business Impact:
Financial example: Starting with 20 image definitions, 8 versions, 4 regions, 3 replicas = 1,920 total replicas. After optimization: 2 versions, 2 regions, demand-matched replicas = over 70% eliminated.
| Tier | Redundancy | Use Case | Cost |
|---|---|---|---|
| Standard_LRS | Local redundancy | Dev/test/archival | Lowest cost |
| Standard_ZRS | Zone redundancy | Production/HA | ~2x LRS |
| Premium_LRS | Local | High-IOPS workloads | ~2-3x Standard |
Notes: Default is Standard_LRS. ZRS is only available in regions with Availability Zones. Archive storage is NOT supported for VHD page blobs.
Legacy image versions accumulate because Azure Compute Gallery does not enforce retention policies natively. The end-of-life date property is informational only.
Gallery Cost Growth Model:
Gallery Cost Growth Model
=========================
Month 1: 5 definitions x 1 version x 3 regions x 2 replicas = 30 managed disks
Month 6: 5 definitions x 6 versions x 3 regions x 2 replicas = 180 managed disks
Month 12: 5 definitions x 12 versions x 3 regions x 2 replicas = 360 managed disks
Month 24: 5 definitions x 24 versions x 3 regions x 2 replicas = 720 managed disks
At 128 GB per disk (Standard_LRS @ ~$5.89/mo per disk):
Month 1: $177/mo
Month 6: $1,060/mo
Month 12: $2,120/mo
Month 24: $4,240/mo
Without cleanup, costs grow linearly with each new image version.Detection Logic -- 3 criteria:
Decision Tree:
Image Version Cleanup Decision Tree
=====================================
[Start] Is this the latest or second-latest version?
|
+-- YES --> RETAIN (rollback safety)
|
+-- NO --> Is it older than the retention window (90-180 days)?
|
+-- NO --> RETAIN (within retention window)
|
+-- YES --> Does any active VM/VMSS/AVD/pipeline reference this version?
|
+-- YES --> RETAIN but right-size target regions
|
+-- NO --> Is there a compliance hold on this version?
|
+-- YES --> ARCHIVE to Standard_LRS in a single region
|
+-- NO --> DELETEImplementation -- Inventory CLI commands:
# List all galleries in the subscription
az sig list --query "[].{Name:name, ResourceGroup:resourceGroup, Location:location}" -o table
# List all image definitions in a gallery
az sig image-definition list \
--gallery-name <gallery-name> \
--resource-group <rg-name> \
--query "[].{Name:name, OsType:osType, Publisher:identifier.publisher}" -o table
# List all image versions for a definition with age and replication details
az sig image-version list \
--gallery-name <gallery-name> \
--gallery-image-definition <definition-name> \
--resource-group <rg-name> \
--query "[].{Version:name, Created:publishingProfile.publishedDate, \
EOL:publishingProfile.endOfLifeDate, \
Regions:publishingProfile.targetRegions[].name, \
ReplicaCount:publishingProfile.targetRegions[].regionalReplicaCount, \
StorageType:publishingProfile.storageAccountType}" -o table
# Count total replicas across all galleries
az sig list --query "[].name" -o tsv | while read gallery; do
rg=$(az sig show --gallery-name "$gallery" --query "resourceGroup" -o tsv)
az sig image-definition list --gallery-name "$gallery" --resource-group "$rg" \
--query "[].name" -o tsv | while read def; do
az sig image-version list --gallery-name "$gallery" \
--gallery-image-definition "$def" --resource-group "$rg" \
--query "[].publishingProfile.targetRegions[].regionalReplicaCount" -o tsv
done
done | paste -sd+ | bcCleanup PowerShell Runbook:
# Azure Automation Runbook: Gallery Image Version Cleanup
# Schedule: Weekly, during maintenance window
param(
[string]$GalleryName,
[string]$ResourceGroupName,
[int]$RetentionDays = 180,
[int]$KeepLatestVersions = 2,
[bool]$DryRun = $true
)
Connect-AzAccount -Identity
$definitions = Get-AzGalleryImageDefinition -GalleryName $GalleryName \
-ResourceGroupName $ResourceGroupName
foreach ($def in $definitions) {
$versions = Get-AzGalleryImageVersion -GalleryName $GalleryName \
-ResourceGroupName $ResourceGroupName \
-GalleryImageDefinitionName $def.Name |
Sort-Object -Property Name -Descending
# Always keep the latest N versions
$candidates = $versions | Select-Object -Skip $KeepLatestVersions
foreach ($ver in $candidates) {
$age = (Get-Date) - $ver.PublishingProfile.PublishedDate
if ($age.TotalDays -gt $RetentionDays) {
# Check for active VM references
$refCount = (az graph query -q "Resources \
| where type =~ 'microsoft.compute/virtualmachines' \
| where properties.storageProfile.imageReference.id \
contains '$($ver.Id)'" --query "count_" -o tsv)
if ([int]$refCount -eq 0) {
if ($DryRun) {
Write-Output "[DRY RUN] Would delete: $($def.Name) v$($ver.Name) \
(age: $([math]::Round($age.TotalDays)) days, refs: 0)"
} else {
Remove-AzGalleryImageVersion -GalleryName $GalleryName \
-ResourceGroupName $ResourceGroupName \
-GalleryImageDefinitionName $def.Name \
-Name $ver.Name -Force
Write-Output "Deleted: $($def.Name) v$($ver.Name)"
}
} else {
Write-Output "Skipped: $($def.Name) v$($ver.Name) \
(active refs: $refCount)"
}
}
}
}Failure mode warning: Always run the cleanup with $DryRun = $true first. Deleting an image version that is actively referenced by a VM scale set will not break running VMs, but it will prevent new instances from being provisioned from that version. Verify all references before deleting.
The Over-Replication Pattern:
Over-Replication Example
========================
Configured: 5 regions x 8 replicas = 40 disks per version
Actual: 2 regions actively deploying, max 30 concurrent VMs
Required: 2 regions x 3 replicas = 6 disks per version
Waste: 34 unnecessary disks (85%)
At 128 GB Standard_LRS (~$5.89/disk/mo):
Configured cost: $236/mo per version
Required cost: $35/mo per version
Savings: $201/mo per version
With 10 active versions: $2,010/mo in unnecessary storageDetection Logic:
Replica Sizing Flow:
Replica Sizing Decision Flow
=============================
[Start] For each target region:
|
+-- Any VM/VMSS/AVD deployments in this region?
|
+-- NO --> Is this a DR-only region?
| |
| +-- YES --> Keep 1 replica, Standard_LRS
| +-- NO --> REMOVE region from target list
|
+-- YES --> Calculate required replicas:
|
+-- VM-based: ceil(concurrent_VMs / 20)
+-- VMSS-based: concurrent_scale_sets + 1
|
+-- Is this a production workload?
|
+-- YES --> max(calculated, 3) + 1-2 buffer replicas
+-- NO --> max(calculated, 1) (dev/test)Right-sizing implementation:
# Update replica count for an image version
az sig image-version update \
--gallery-name <gallery-name> \
--gallery-image-definition <definition-name> \
--gallery-image-version <version> \
--resource-group <rg-name> \
--target-regions <region1>=<replica-count>=<storage-type> \
<region2>=<replica-count>=<storage-type>
# Example: Reduce from 5 regions/8 replicas to 2 regions/3 replicas
az sig image-version update \
--gallery-name gallery-prod \
--gallery-image-definition win11-enterprise \
--gallery-image-version 1.0.0 \
--resource-group rg-images \
--target-regions eastus=3=Standard_LRS westus2=3=Standard_LRS
# Remove a region entirely by omitting it from --target-regions
# (the image version must retain at least one target region)Important: Reducing replica counts below the concurrent deployment demand will cause provisioning throttling. Always validate concurrent VM creation rates in each region before reducing replicas. The 1:20 ratio (1 replica per 20 concurrent VMs) is a minimum guideline, not a hard limit.
Citrix DaaS (formerly Virtual Apps and Desktops service) uses Azure Compute Gallery for machine catalog images and applies its own replica settings on top of Azure defaults.
Default Citrix Replica Settings:
| Catalog Type | Default Ratio (VMs per Replica) | Max Replicas |
|---|---|---|
| Non-Persistent | 40 | 100 |
| Persistent | 1000 | 100 |
Key optimization: For 200 non-persistent machines with the default ratio of 40, Citrix creates 5 replicas (200 / 40 = 5). If the actual concurrent demand is only 60 machines, setting the ratio to 60 reduces the replica count to 4. For larger catalogs, the savings scale proportionally.
Adjusting Citrix DaaS replica ratio:
# Adjust Citrix DaaS machine catalog replica ratio
# Run from Citrix Cloud PowerShell or Remote PowerShell SDK
# View current provisioning scheme settings
Get-ProvScheme -ProvisioningSchemeName "MyCatalog" |
Select-Object ProvisioningSchemeName,
MachineCount,
UseSharedImageGallery,
SharedImageGalleryReplicaRatio,
SharedImageGalleryReplicaMaximum
# Update the replica ratio from default 40 to 60
Set-ProvScheme -ProvisioningSchemeName "MyCatalog" \
-CustomProperties @"
<CustomProperties xmlns="http://schemas.citrix.com/2014/xd/machinecreation">
<Property xsi:type="StringProperty" Name="SharedImageGalleryReplicaRatio" Value="60" />
<Property xsi:type="StringProperty" Name="SharedImageGalleryReplicaMaximum" Value="10" />
</CustomProperties>
"@
# Verify the change
Get-ProvScheme -ProvisioningSchemeName "MyCatalog" |
Select-Object SharedImageGalleryReplicaRatio, SharedImageGalleryReplicaMaximumFailure mode warning: Setting the replica ratio too high (too few replicas) will cause Citrix provisioning failures during burst periods when many machines power on simultaneously. Monitor Citrix Cloud provisioning logs for throttling errors after adjusting the ratio. Start conservatively and reduce gradually.
Azure Compute Gallery itself is a free orchestration layer. The costs come from the underlying managed disks that store each image replica. These charges appear under "Managed Disks" or "Storage" in your invoice, not as a separate gallery line item. This makes gallery costs difficult to identify without cross-referencing disk resources against gallery image versions.
Total cost = (number of image definitions) x (versions per definition) x (target regions) x (replicas per region) x (managed disk cost for the image size and storage tier). Use the inventory CLI commands above to count your total replicas, then multiply by the per-disk cost for your image size and storage tier.
Yes, provided no running VM, scale set, AVD host pool, or CI/CD pipeline references that version. Deleting an unreferenced version has no impact on running workloads. However, deleting a version that is still referenced will not break running VMs -- it will only prevent new VMs from being provisioned from that specific version.
Most organizations retain the latest 2 versions and set a retention window of 90-180 days for older versions. The latest 2 versions provide rollback capability. Anything beyond the retention window that has no active deployment references is a deletion candidate. Compliance requirements may extend this window.
Azure recommends 1 replica per 20 concurrent VM deployments. For production workloads, use a minimum of 3 replicas plus 1-2 buffer replicas. For dev/test, 1 replica is usually sufficient. The formula is: ceil(concurrent_VMs / 20) for VMs, or concurrent_scale_sets + 1 for VMSS.
If the replica count drops below the concurrent deployment demand, Azure will throttle VM provisioning requests. This manifests as slower scale-out times and potential provisioning failures during burst periods. The impact is most visible in auto-scaling scenarios and Citrix DaaS power management.
Use Standard_ZRS for production images in regions with Availability Zones where you need zone-resilient deployments. Use Standard_LRS for dev/test, archival, and DR-only regions. ZRS costs approximately 2x LRS, so downgrading non-critical replicas from ZRS to LRS provides immediate savings.
Citrix DaaS manages its own replica settings through the provisioning scheme. The default ratio of 40 VMs per replica for non-persistent catalogs is often more generous than needed. You can adjust the ratio using Set-ProvScheme to match your actual concurrent boot demand. Changes take effect on the next image update cycle.
Yes. Deploy the PowerShell runbook provided above as an Azure Automation runbook on a weekly schedule. Start with DryRun = $true to validate the cleanup candidates, then switch to $false for automated deletion. Pair this with Azure Policy to enforce tagging on gallery resources for better tracking.
Every time you replicate an image version to a new region, Azure charges egress for the data transfer. Removing unused target regions eliminates both the ongoing managed disk storage cost and the one-time egress cost for future image versions. This is a double saving.
Spotto continuously monitors your Azure Compute Gallery configurations, identifies orphaned image versions, detects over-replicated images, and surfaces right-sizing recommendations. Rather than running one-off audits, Spotto provides ongoing visibility and alerts when gallery configurations drift from optimal -- especially valuable for MSPs managing multiple customer galleries.
Yes. MSPs can script the inventory and cleanup across all tenants using Azure Lighthouse or by iterating over subscriptions. The cleanup runbook supports parameterized gallery names and resource groups, making it straightforward to deploy across multiple environments. Spotto automates this cross-tenant visibility natively.
Before
$18,000/mo
15 galleries, avg 12 versions, 4 regions, 5 replicas
After
$3,600/mo
2 versions retained, 2 regions, demand-matched replicas
Actions taken: Removed 10 legacy versions per gallery. Eliminated 2 unused target regions. Right-sized replica counts from 5 to 2-3 based on actual concurrent deployments. Downgraded dev/test replicas from ZRS to LRS.
Before
$8,500/mo
500 non-persistent VDAs, default ratio 40, 13 replicas
After
$3,400/mo
Ratio adjusted to 80, 7 replicas, old versions cleaned
Actions taken: Analyzed actual concurrent boot demand (peak 350 VDAs). Adjusted Citrix replica ratio from 40 to 80. Reduced replicas from 13 to 7. Cleaned 6 obsolete image versions. Moved archival versions to Standard_LRS single-region.
Before
$4,200/mo
Weekly image builds, 52 versions/year, no cleanup
After
$420/mo
Automated cleanup to 4 versions, 1 region for dev
Actions taken: Implemented automated cleanup runbook in CI/CD pipeline. Retained only latest 4 versions (1 month of weekly builds). Reduced dev/test gallery to single region with 1 replica. Production gallery kept at 2 regions with 3 replicas.
Before
$6,800/mo
3 host pools, 8 versions each, ZRS in 3 regions
After
$1,700/mo
2 versions, ZRS in primary region, LRS in DR
Actions taken: Reduced retention to 2 versions per host pool image definition. Kept ZRS only in the primary deployment region. Switched DR region to Standard_LRS with 1 replica. Removed the third region where no AVD sessions were hosted.
DryRun = $true. Review the list of versions that would be deleted. Validate that no active references are missed.Every optimization should have a clear rollback path:
az sig image-version update. Replication to the target region takes time proportional to image size. Allow 30-60 minutes for a 128 GB image.Set-ProvScheme replica ratio to the previous value. The change takes effect on the next image update or catalog update cycle.| Recommendation | Typical Savings | Complexity |
|---|---|---|
| Remove legacy image versions | 20-30% | Low |
| Remove unused target regions | 15-40% | Low |
| Right-size replica counts | 10-25% | Medium |
| Downgrade ZRS to LRS (non-critical) | ~50% per replica | Low |
| Citrix DaaS replica ratio optimization | 30-60% | Low |
| Combined optimizations | 60-90% | Medium |
ceil(concurrent_VMs / 20) as your baseline.The challenges outlined in this article — image version sprawl, over-replicated galleries, hidden managed disk charges — are the same ones our customers work through across their Azure environments. The difference is they use Spotto to surface these opportunities automatically, rather than running the analysis by hand.
Spotto continuously analyzes your Azure Compute Gallery configurations to detect orphaned image versions, over-replicated galleries, and storage tier mismatches — giving you clear, actionable recommendations to eliminate gallery waste across every subscription.
Free TrialDisclaimer: This article is provided for informational purposes only and does not constitute professional advice. Azure pricing, features, and service behavior may have changed since publication. Always validate recommendations against your specific environment and consult Microsoft's official documentation before making changes to production infrastructure. Spotto is not liable for any actions taken based on the content of this article.