Operational Excellence

    Should You Delete Unused Azure Virtual Networks?

    Identify and clean up orphaned Azure Virtual Networks that fragment IP address space, clutter network inventories, and anchor paid resources like VPN Gateways.

    Spotto
    February 2026
    20 min read

    A Virtual Network (VNet) with no subnets, or subnets with no connected resources, is operational dead weight. The VNet itself is free, but it reserves a chunk of your private IP address space, appears in every network inventory and topology view, and can anchor paid resources like VPN Gateways, ExpressRoute circuits, or Azure Firewall instances that continue to bill even when the network they sit in serves no workloads.

    Orphaned VNets accumulate naturally as projects are decommissioned, migrations complete, or proof-of-concept environments are abandoned. Left unchecked, they fragment your IP address plan, confuse network topology diagrams, and create stale peering connections that add latency to route lookups. This article walks through how to detect orphaned VNets, evaluate their dependencies, and safely remove them using a structured workflow that works at scale across MSP-managed tenants and enterprise subscriptions alike.

    Key Takeaways at a Glance:

    Empty VNets are free but reserve IP address space and add governance overhead -- stale address ranges, confusing topology views, and peering connections that reference networks with no workloads
    Two patterns to look for: VNets with no subnets at all, and VNets with subnets that have no connected resources (NICs, service endpoints, delegations)
    Dependencies such as peerings, VPN Gateways, Private DNS zone links, service endpoints, and Network Security Group associations must be resolved before deletion
    Private DNS zone links are auto-cleaned when the VNet is deleted -- Azure removes the link from the DNS zone automatically
    Structured workflow: discover orphaned VNets, tag for review, notify the owning team, wait for a grace period, delete, and document the change

    What Problem Does an Unused VNet Create?

    Operational Overhead

    Orphaned VNets impose real operational costs even though Azure does not bill for the VNet resource itself. The overhead compounds as your Azure estate grows:

    IP address space fragmentation -- Every VNet reserves a CIDR block from your private address plan. Orphaned VNets hold onto address ranges that cannot be reused without deletion, leading to artificially exhausted IP space. Organizations that run a /16 per region find themselves requesting additional ranges because orphaned VNets are squatting on blocks that no workload consumes.
    Inventory noise -- Every orphaned VNet appears in Azure Resource Graph queries, Network Watcher topology views, and third-party CMDB syncs. Engineers working through a network topology diagram have to distinguish between active VNets carrying production traffic and empty VNets left over from a completed migration. At scale, this slows incident response and change planning.
    Peering confusion -- A VNet peering connection is a two-sided relationship. If VNet-A is peered to an orphaned VNet-B, VNet-A's effective route table includes routes pointing at VNet-B. These routes are harmless if VNet-B has no subnets, but they clutter route tables, confuse network troubleshooting, and can mask routing issues by adding unnecessary entries to the effective routes list.
    Automation drift -- Infrastructure-as-code templates (ARM, Bicep, Terraform) may still declare VNets that are empty in production. This creates a gap between the declared state and the actual state, causing plan/apply confusion, deployment warnings, and a loss of trust in IaC as the source of truth for your network topology.

    Two Patterns of Empty VNets

    Orphaned VNets present in two distinct patterns, and the remediation path differs for each:

    Pattern 1: VNet with no subnets -- The VNet exists but contains zero subnets. This typically happens when a VNet was provisioned as part of a template or landing zone but never configured further. These are the simplest to clean up because there are no subnet-level dependencies to resolve.
    Pattern 2: VNet with subnets but no connected resources -- The VNet has one or more subnets, but no subnet contains any connected NICs, service endpoints, private endpoints, delegated services, or gateway associations. This pattern appears when VMs or services have been deleted but the network skeleton was left behind.

    Important distinction: A VNet with a GatewaySubnet that contains an active VPN Gateway or ExpressRoute Gateway is NOT orphaned, even if no other subnets have connected resources. The gateway itself is a connected resource, and it is a paid resource that bills hourly. These VNets require a different evaluation: determine whether the gateway is still needed, not whether the VNet is empty.

    How It Works

    Detection Logic

    A VNet qualifies as an orphan candidate when all three conditions are met:

    1. No connected NICs -- No network interface in any subnet is attached to a VM, VMSS instance, Private Endpoint, or other compute resource
    2. No active gateway -- No VPN Gateway, ExpressRoute Gateway, or Application Gateway is deployed in any subnet (including the GatewaySubnet)
    3. No delegated services -- No subnet is delegated to a PaaS service (such as Azure SQL Managed Instance, App Service VNet Integration, or Azure Container Instances)

    Dependencies That Block Deletion

    Before deleting an orphaned VNet, you must resolve or acknowledge these dependencies:

    VNet peering connections -- Both sides of a peering must be removed. Deleting VNet-A's peering to VNet-B does not automatically remove VNet-B's peering back to VNet-A. The remote side will show a "Disconnected" state, which creates alerts and confuses monitoring. Always delete both sides of the peering before deleting the VNet.
    VPN Gateway or ExpressRoute Gateway -- A GatewaySubnet with an active gateway cannot be deleted. The gateway must be removed first, which is a billable resource (VPN Gateway starts at ~$140/month for the Basic SKU). Removing the gateway stops the hourly charge.
    Private DNS zone links -- VNets can be linked to Private DNS zones for name resolution. When you delete a VNet, Azure automatically removes the DNS zone link. This is safe, but you should verify that no other resource depends on the resolution path through this VNet before deletion.
    Service endpoints -- Subnets may have service endpoints configured (e.g., Microsoft.Storage, Microsoft.Sql) even if no resources are connected. These must be removed from the subnet before the subnet can be deleted, or you can delete the entire VNet which removes them automatically.
    Network Security Group (NSG) and Route Table associations -- Subnets may have NSGs or route tables (UDRs) associated. Deleting the VNet detaches these associations but does not delete the NSG or route table resources themselves. You may want to clean up orphaned NSGs and route tables separately.

    Step-by-Step CLI Commands

    Pattern 1: VNets with No Subnets

    # Find all VNets with no subnets using Azure Resource Graph
    az graph query -q "
      Resources
      | where type =~ 'microsoft.network/virtualnetworks'
      | where array_length(properties.subnets) == 0
      | project name, resourceGroup, subscriptionId, location,
                addressSpace=properties.addressSpace.addressPrefixes
    " -o table
    
    # List peerings on an orphaned VNet before deletion
    az network vnet peering list \
      --resource-group <rg-name> \
      --vnet-name <vnet-name> \
      --query "[].{Name:name, RemoteVNet:remoteVirtualNetwork.id, State:peeringState}" \
      -o table
    
    # Remove peerings from both sides
    az network vnet peering delete \
      --resource-group <rg-name> \
      --vnet-name <vnet-name> \
      --name <peering-name>
    
    # Remove the corresponding peering on the remote VNet
    az network vnet peering delete \
      --resource-group <remote-rg-name> \
      --vnet-name <remote-vnet-name> \
      --name <remote-peering-name>
    
    # Delete the orphaned VNet
    az network vnet delete \
      --resource-group <rg-name> \
      --name <vnet-name>

    Pattern 2: VNets with Subnets but No Connected Resources

    # Find VNets where all subnets have no connected resources
    az graph query -q "
      Resources
      | where type =~ 'microsoft.network/virtualnetworks'
      | where array_length(properties.subnets) > 0
      | mv-expand subnet = properties.subnets
      | extend ipConfigCount = array_length(subnet.properties.ipConfigurations),
               delegationCount = array_length(subnet.properties.delegations),
               serviceEndpointCount = array_length(subnet.properties.serviceEndpoints)
      | summarize totalIpConfigs = sum(ipConfigCount),
                  totalDelegations = sum(delegationCount),
                  subnetCount = count()
        by name, resourceGroup, subscriptionId, location
      | where totalIpConfigs == 0 and totalDelegations == 0
      | project name, resourceGroup, subscriptionId, location, subnetCount
    " -o table
    
    # Check for Private DNS zone links to this VNet
    az network private-dns link vnet list \
      --resource-group <dns-zone-rg> \
      --zone-name <zone-name> \
      --query "[?virtualNetwork.id contains '<vnet-name>'].{LinkName:name, VNet:virtualNetwork.id}" \
      -o table
    
    # Check for active gateways in the GatewaySubnet
    az network vnet-gateway list \
      --resource-group <rg-name> \
      --query "[?contains(ipConfigurations[0].subnet.id, '<vnet-name>')].{Name:name, SKU:sku.name}" \
      -o table
    
    # If gateway exists and is no longer needed, delete it first
    az network vnet-gateway delete \
      --resource-group <rg-name> \
      --name <gateway-name>
    
    # Remove all peerings (both sides)
    az network vnet peering list \
      --resource-group <rg-name> \
      --vnet-name <vnet-name> \
      -o tsv --query "[].name" | while read peering; do
        az network vnet peering delete \
          --resource-group <rg-name> \
          --vnet-name <vnet-name> \
          --name "$peering"
    done
    
    # Delete the orphaned VNet (subnets are deleted with it)
    az network vnet delete \
      --resource-group <rg-name> \
      --name <vnet-name>

    Decision Tree:

    Orphaned VNet Decision Tree
    =============================
    
    [Start] Does the VNet have any subnets?
      |
      +-- NO  --> Any peerings?
      |     |
      |     +-- YES --> Remove peerings (both sides) --> DELETE VNet
      |     +-- NO  --> DELETE VNet
      |
      +-- YES --> Do any subnets have connected NICs, gateways, or delegations?
            |
            +-- YES --> Is the connected resource still active/needed?
            |     |
            |     +-- YES --> RETAIN (not orphaned)
            |     +-- NO  --> Remove the inactive resource first --> Re-evaluate
            |
            +-- NO  --> Any VNet peerings?
                  |
                  +-- YES --> Is the peering still serving traffic?
                  |     |
                  |     +-- YES --> RECONFIGURE (keep VNet, re-evaluate peering need)
                  |     +-- NO  --> Remove peerings (both sides) --> Continue
                  |
                  +-- NO  --> Any VPN/ExpressRoute Gateway in GatewaySubnet?
                        |
                        +-- YES --> Is the gateway still needed?
                        |     |
                        |     +-- YES --> RETAIN (gateway is a paid resource)
                        |     +-- NO  --> Delete gateway --> DELETE VNet
                        |
                        +-- NO  --> DELETE VNet

    Failure mode warning: Deleting a VNet that has an active peering to a hub network will leave the hub side in a "Disconnected" peering state. Azure does NOT automatically clean up the remote peering. Always delete both sides of every peering connection before deleting the VNet, or the stale peering will generate monitoring alerts and confuse network operations.

    VNet Tier and Configuration Reference

    ComponentAzure ChargeNotes
    VNet (core resource)FreeNo charge for the VNet itself; cost is operational (IP space, governance)
    SubnetsFreeNo charge; subnets are logical divisions within the VNet
    VNet Peering (same region)$0.01/GB ingress + $0.01/GB egressCharged per GB of data transferred across the peering
    VNet Peering (cross-region / global)$0.035-$0.075/GB depending on regionsHigher rate for cross-region; stale peerings to empty VNets incur no data cost but add route table entries
    VPN Gateway (Basic SKU)~$140/monthBilled hourly whether or not traffic flows; anchored to a GatewaySubnet in the VNet
    VPN Gateway (VpnGw1)~$260/monthCommon production SKU; significant cost if the VNet carries no traffic
    ExpressRoute Gateway (Standard)~$365/monthBilled hourly; often deployed in hub VNets that may outlive their spoke workloads
    Azure Firewall~$912/month (Standard)Deployed into AzureFirewallSubnet; continues to bill even with zero traffic
    Private DNS Zone LinkFreeNo charge; auto-cleaned when VNet is deleted
    NSG / Route TableFreeNo charge; detached (not deleted) when VNet is deleted

    Key insight: While the VNet itself is free, the paid resources anchored to it (VPN Gateways, ExpressRoute Gateways, Azure Firewall) continue to bill at full rate regardless of whether the VNet carries any traffic. Identifying orphaned VNets is often the first step to discovering these idle paid resources.

    Common Questions

    1. Does deleting a VNet cost anything?

    No. Deleting a VNet is a free operation. The VNet itself has no associated charge. However, if the VNet contains paid resources (VPN Gateway, ExpressRoute Gateway, Azure Firewall), those resources must be deleted first, and their deletion stops the associated hourly billing. The act of deletion does not incur any charge.

    2. Can I recover a deleted VNet?

    No. VNet deletion is permanent. There is no soft-delete or recovery mechanism for Azure Virtual Networks. Once deleted, you must recreate the VNet, subnets, peerings, DNS links, and all other configurations from scratch. This is why the structured workflow recommends tagging, notifying, and waiting before deleting. Always document the VNet's configuration (address space, subnets, peerings, DNS links) before deletion so it can be recreated if needed.

    3. What happens to peerings when I delete a VNet?

    When you delete a VNet, Azure removes the local side of any peering connections. The remote VNet's peering enters a "Disconnected" state. This disconnected peering will remain on the remote VNet indefinitely until someone manually removes it. It generates monitoring alerts, clutters the remote VNet's peering list, and prevents a new peering with the same name from being created. Always delete both sides of peering connections before deleting the VNet.

    4. How should I handle VNets that are part of a DR plan?

    VNets designated for disaster recovery should be tagged as DR resources and excluded from orphan cleanup workflows. Even if a DR VNet has no active resources today, it may be critical during a failover event. The recommended approach is to tag these VNets with a purpose:dr or retain:true tag and configure your cleanup automation to skip tagged resources. Review DR VNets quarterly to ensure the DR plan is still valid and the VNet configuration still matches the recovery requirements.

    5. What happens to Private DNS zone links when I delete a VNet?

    Azure automatically removes Private DNS zone virtual network links when the linked VNet is deleted. This is safe behavior -- the DNS zone itself is not affected, and other VNets linked to the same zone continue to resolve normally. However, if the deleted VNet was the only VNet linked to a DNS zone with auto-registration enabled, no new DNS records will be auto-registered until another VNet is linked. Verify DNS resolution requirements before deleting.

    6. Are there any hidden costs from orphaned VNets?

    The VNet itself is free, but orphaned VNets frequently anchor paid resources that continue to bill. The most common hidden costs are: VPN Gateways (~$140-$260+/month), ExpressRoute Gateways (~$365+/month), and Azure Firewall (~$912+/month). These resources bill hourly regardless of traffic. An orphaned VNet with a forgotten VPN Gateway costs over $1,680/year. Beyond direct costs, the operational overhead of managing stale network inventories, investigating false positives in monitoring, and maintaining IaC configurations for non-existent workloads is a real but harder-to-quantify cost.

    7. Are there limits on how many VNets I can have?

    Yes. The default limit is 1,000 VNets per subscription per region, which can be increased via a support request. While most organizations never hit this limit, MSPs managing many small tenants or organizations using subscription-per-workload patterns can approach it. Orphaned VNets unnecessarily consume this quota. More practically, the limit on VNet peerings per VNet is 500, and each orphaned VNet with a peering consumes one of those slots on the remote VNet.

    8. How should MSPs handle VNet cleanup across multiple tenants?

    MSPs should incorporate VNet cleanup into their quarterly tenant hygiene sweeps. Use Azure Lighthouse to run Resource Graph queries across all managed subscriptions from a single pane. Tag orphaned VNets with a cleanup-candidate tag and the discovery date. Notify the customer's technical contact with a list of orphaned VNets and their address space. Wait 14-30 days for acknowledgement before deletion. Document every deletion in the customer's change log. Spotto automates this cross-tenant detection and notification workflow.

    9. Can Azure Policy prevent orphaned VNets from accumulating?

    Azure Policy can enforce tagging requirements on VNet creation (e.g., requiring an owner and project tag), which makes it easier to identify the responsible team during cleanup. However, Azure Policy cannot currently detect and auto-delete orphaned VNets. You need a scheduled automation (Azure Automation runbook, Logic App, or third-party tool like Spotto) to periodically scan for VNets matching the orphan criteria and trigger the cleanup workflow.

    10. What permissions do I need to delete a VNet?

    You need Microsoft.Network/virtualNetworks/delete permission, which is included in the Network Contributor, Contributor, and Owner built-in roles. If the VNet has peerings, you also need Microsoft.Network/virtualNetworks/virtualNetworkPeerings/delete on both the local and remote VNets. For VPN Gateway deletion, you need Microsoft.Network/virtualNetworkGateways/delete. In MSP scenarios using Azure Lighthouse, ensure the delegated role assignment includes these permissions.

    11. Can I reuse the IP address space after deleting a VNet?

    Yes, immediately. Once a VNet is deleted, its CIDR block is released and can be assigned to a new VNet in the same or different subscription. There is no cooldown period. This is one of the primary benefits of cleaning up orphaned VNets -- reclaiming fragmented IP address space. If your organization uses an IP Address Management (IPAM) tool, update it to reflect the released range.

    12. How does Spotto detect orphaned VNets?

    Spotto continuously queries Azure Resource Graph across all connected subscriptions to identify VNets matching the orphan criteria: no connected NICs, no active gateways, and no delegated services. It cross-references peering connections, DNS zone links, and gateway deployments to build a complete dependency map for each VNet. Orphaned VNets are flagged with their full dependency context, estimated IP address space recovery, and any anchored paid resources -- giving your team everything needed to make a confident delete-or-retain decision without manual investigation.

    Real-World Scenarios

    Scenario 1: MSP Quarterly Tenant Hygiene Sweep

    Environment: MSP managing 35 tenants across Azure Lighthouse. Quarterly network hygiene review as part of managed services SLA.

    Discovery: Resource Graph query across all 35 tenants identified 12 orphaned VNets: 8 with no subnets, 4 with subnets but zero connected resources. Combined address space held by orphaned VNets: 3 x /16 blocks and 9 x /24 blocks.

    Dependencies found: 3 VNets had stale peering connections to hub networks (remote side showing "Disconnected"). 1 VNet had 2 Private DNS zone links. No gateways or paid resources were attached.

    Actions taken: Tagged all 12 VNets as cleanup-candidate. Notified 8 customer contacts (some tenants had multiple orphans). Waited 14-day grace period. Received acknowledgement from all 8 customers. Deleted stale peerings (both sides) on 3 VNets. Deleted all 12 VNets. Updated IPAM records to release address blocks.

    Outcome: Recovered 3 x /16 and 9 x /24 address blocks. Eliminated 3 stale peering connections from hub networks. Reduced network inventory noise across 35 tenants. Total time: 4 hours across the MSP team.

    Scenario 2: Enterprise Hub-Spoke Cleanup After Migration

    Environment: Enterprise with a hub-spoke network topology. Recently completed migration from on-premises to Azure, consolidating workloads from 6 spoke VNets down to 3.

    Discovery: 3 spoke VNets were empty after migration -- all VMs moved to the consolidated spoke VNets. Each orphaned spoke VNet still had: 1 peering to the hub VNet, 3-4 Private DNS zone links, and 2-3 subnets with NSG associations but no connected NICs.

    Dependencies found: Hub VNet had 6 peering connections (3 active, 3 to orphaned spokes). The 3 stale peerings were adding route entries to the hub's effective route table. UDR on the hub's firewall subnet included routes pointing at the orphaned spoke address ranges.

    Actions taken: Removed stale routes from the hub UDR. Deleted peerings on both sides (3 peerings x 2 sides = 6 peering deletions). Deleted the 3 orphaned spoke VNets. Cleaned up 4 orphaned NSGs that were detached after VNet deletion. Updated hub-spoke Bicep templates to remove references to decommissioned spokes.

    Outcome: Hub VNet's effective route table reduced by ~40%. Network topology diagram simplified from 7 VNets to 4. IaC templates now match production state. No cost savings on VNet itself (free), but eliminated confusion during incident response when engineers had to determine which spokes carried active traffic.

    Scenario 3: MSP Onboarding Audit Reveals Forgotten VPN Gateway

    Environment: New customer onboarding for an MSP. Customer has 4 subscriptions, previously self-managed.

    Discovery: Initial network audit found 2 orphaned VNets in a dev/test subscription. One VNet had no subnets. The second VNet had 3 subnets with no connected NICs, but the GatewaySubnet contained a VPN Gateway (VpnGw1 SKU).

    Dependencies found: The VPN Gateway had been deployed 18 months earlier for a proof-of-concept site-to-site connection that was never completed. No VPN connections were configured. The gateway was billing at ~$140/month (VpnGw1 in the dev/test subscription where Basic was not available).

    Actions taken: Confirmed with the customer that the VPN Gateway was no longer needed. Deleted the VPN Gateway (stopped $140/month billing). Deleted both orphaned VNets after removing one stale peering.

    Outcome: Immediate cost savings of $1,680/year from the forgotten VPN Gateway. This finding during onboarding demonstrated the MSP's value and justified the ongoing managed services engagement. The orphaned VNet was the breadcrumb that led to the paid resource -- a common pattern Spotto surfaces automatically.

    Scenario 4: SaaS Dev/Test Environment Sprawl

    Environment: SaaS company with a subscription-per-environment deployment model. Each feature branch gets its own resource group with a VNet, AKS cluster, and supporting resources. Resources are torn down after testing, but VNets are sometimes left behind.

    Discovery: Over 6 months, 40+ orphaned VNets accumulated across the dev/test subscription. Each VNet had a /22 address space (1,024 addresses). Combined, the orphaned VNets consumed over 40,000 IP addresses from the dev/test range, which was a /16 (65,536 total). Engineers were requesting additional address space because the dev/test /16 appeared nearly exhausted.

    Dependencies found: No peerings, no gateways, no DNS links. All 40+ VNets were Pattern 1 (no subnets) or Pattern 2 (subnets with no connected resources). The AKS clusters, databases, and other resources had been deleted but the VNets remained.

    Actions taken: Bulk-deleted all 40+ orphaned VNets using a scripted cleanup. Updated the CI/CD pipeline teardown stage to include VNet deletion as part of the environment cleanup. Added an Azure Policy to require a ttl tag on all VNets in the dev/test subscription.

    Outcome: Recovered over 40,000 IP addresses (~62% of the dev/test range). Eliminated the need for additional address space allocation. CI/CD pipeline now includes VNet cleanup, preventing future accumulation. No direct cost savings (VNets are free), but prevented a costly IP re-addressing project.

    Implementation Guidance

    Preconditions

    Azure CLI or PowerShell Az module installed and authenticated with Network Contributor (or higher) permissions
    Azure Resource Graph access for cross-subscription queries (Reader role across target subscriptions)
    An IP Address Management (IPAM) tool or documented address plan to update after VNet deletion
    Change management process and approval for production network modifications
    For MSP environments: Azure Lighthouse delegation with Network Contributor permissions across managed subscriptions

    Required Inputs

    List of subscription IDs to scan (or Azure Lighthouse scope for MSPs)
    Tag names used for DR, retained, or exempt VNets (e.g., purpose:dr, retain:true)
    Grace period duration (recommended: 14-30 days between tagging and deletion)
    Notification contacts for each subscription or tenant (team email, Slack/Teams channel, or ticketing system)
    Current network topology documentation (to update after cleanup)

    Rollout Approach

    Step 1: Discovery -- Run the Resource Graph queries for both Pattern 1 (no subnets) and Pattern 2 (subnets with no connected resources) across all target subscriptions. Export results to a spreadsheet or ticket.
    Step 2: Dependency mapping -- For each orphaned VNet, check for peerings, gateways, DNS zone links, and service endpoints. Document each dependency in the cleanup ticket.
    Step 3: Tagging -- Tag each orphaned VNet with cleanup-candidate and the discovery date. Exclude VNets tagged with DR or retention markers.
    Step 4: Notification -- Notify the owning team or customer with the list of VNets scheduled for deletion, their address space, and any dependencies. Include a deadline for objections.
    Step 5: Grace period -- Wait 14-30 days. If the owning team objects or identifies a use case, remove the cleanup-candidate tag and document the retention reason.
    Step 6: Deletion -- Delete dependencies first (peerings on both sides, gateways, then VNet). Run during a maintenance window for production subscriptions.
    Step 7: Documentation -- Update IPAM records, network topology diagrams, and IaC templates. Log the deletion in the change management system.

    Failure Modes

    Stale peering on remote VNet: Deleting a VNet without first removing its peering leaves the remote VNet with a "Disconnected" peering. This generates monitoring alerts and prevents reuse of the peering name. Always delete peerings on both sides before deleting the VNet.
    DNS resolution failure: If the deleted VNet was the only VNet linked to a Private DNS zone with auto-registration, new resources in other VNets will not auto-register DNS records. Verify DNS zone link coverage before deletion.
    DR failover failure: Deleting a VNet designated for disaster recovery prevents workload failover to that network. Always check for DR tags and consult the DR plan before deleting any VNet in a production subscription.
    IaC deployment failure: If a Terraform or Bicep template still references a deleted VNet, the next deployment will fail. Update IaC templates before or immediately after deletion to prevent pipeline failures.

    Rollback Strategy

    VNet deletion is permanent -- there is no undo. The rollback strategy is based on recreation:

    Document before deleting: Before each VNet deletion, export its configuration: address space, subnets, peerings, DNS links, NSG associations, route table associations, and any gateway configurations. Store this in your change management system or as a JSON export.
    Recreate from documentation: If a VNet needs to be restored, recreate it from the exported configuration. The address space can be reused immediately (no cooldown). Peerings must be recreated on both sides. DNS zone links must be re-established.
    IaC as backup: If the VNet was originally deployed via IaC (ARM, Bicep, Terraform), the template serves as the rollback artifact. Redeploying the template recreates the VNet and its subnets. Peerings and DNS links may need to be reapplied separately depending on the template scope.
    Grace period as prevention: The 14-30 day grace period between tagging and deletion is the primary rollback mechanism. During this window, the VNet still exists and can be un-tagged if a use case is identified. This is why the structured workflow is important.

    Automation: Weekly Orphaned VNet Cleanup Runbook

    The following PowerShell script can be deployed as an Azure Automation runbook on a weekly schedule. It discovers orphaned VNets, tags them, and optionally deletes them after the grace period has elapsed.

    # Azure Automation Runbook: Orphaned VNet Cleanup
    # Schedule: Weekly, during maintenance window
    # Requires: Az.Accounts, Az.Network, Az.ResourceGraph modules
    
    param(
        [string[]]$SubscriptionIds,           # Target subscriptions (empty = all accessible)
        [int]$GracePeriodDays = 14,           # Days between tagging and deletion
        [string]$CleanupTag = "cleanup-candidate",
        [string]$RetainTag = "retain",
        [string]$DRTag = "purpose",
        [string]$DRTagValue = "dr",
        [bool]$DryRun = $true,
        [bool]$AutoDeleteAfterGrace = $false  # Set to $true for fully automated cleanup
    )
    
    Connect-AzAccount -Identity
    
    # If no subscriptions specified, get all accessible
    if (-not $SubscriptionIds) {
        $SubscriptionIds = (Get-AzSubscription).Id
    }
    
    $report = @()
    
    foreach ($subId in $SubscriptionIds) {
        Set-AzContext -SubscriptionId $subId | Out-Null
        Write-Output "=== Scanning subscription: $subId ==="
    
        # Get all VNets in the subscription
        $vnets = Get-AzVirtualNetwork
    
        foreach ($vnet in $vnets) {
            # Skip VNets tagged for retention or DR
            $tags = $vnet.Tag
            if ($tags[$RetainTag] -eq "true" -or $tags[$DRTag] -eq $DRTagValue) {
                Write-Output "SKIP (tagged retain/DR): $($vnet.Name) in $($vnet.ResourceGroupName)"
                continue
            }
    
            $isOrphaned = $false
            $reason = ""
    
            # Pattern 1: No subnets
            if ($vnet.Subnets.Count -eq 0) {
                $isOrphaned = $true
                $reason = "No subnets"
            }
            else {
                # Pattern 2: Subnets with no connected resources
                $hasConnectedResources = $false
                foreach ($subnet in $vnet.Subnets) {
                    if ($subnet.IpConfigurations.Count -gt 0) {
                        $hasConnectedResources = $true
                        break
                    }
                    if ($subnet.Delegations.Count -gt 0) {
                        $hasConnectedResources = $true
                        break
                    }
                    # Check for gateway in GatewaySubnet
                    if ($subnet.Name -eq "GatewaySubnet") {
                        $gateways = Get-AzVirtualNetworkGateway -ResourceGroupName $vnet.ResourceGroupName -ErrorAction SilentlyContinue
                        foreach ($gw in $gateways) {
                            foreach ($ipConfig in $gw.IpConfigurations) {
                                if ($ipConfig.Subnet.Id -eq $subnet.Id) {
                                    $hasConnectedResources = $true
                                    break
                                }
                            }
                        }
                    }
                }
    
                if (-not $hasConnectedResources) {
                    $isOrphaned = $true
                    $reason = "Subnets with no connected resources ($($vnet.Subnets.Count) empty subnets)"
                }
            }
    
            if ($isOrphaned) {
                $addressSpace = ($vnet.AddressSpace.AddressPrefixes -join ", ")
                $peeringCount = $vnet.VirtualNetworkPeerings.Count
    
                # Check if already tagged for cleanup
                $taggedDate = $tags[$CleanupTag]
    
                if (-not $taggedDate) {
                    # First discovery -- tag it
                    Write-Output "[NEW ORPHAN] $($vnet.Name) | RG: $($vnet.ResourceGroupName) | Reason: $reason | Address: $addressSpace | Peerings: $peeringCount"
    
                    if (-not $DryRun) {
                        $newTags = $tags ?? @{}
                        $newTags[$CleanupTag] = (Get-Date -Format "yyyy-MM-dd")
                        Set-AzResource -ResourceId $vnet.Id -Tag $newTags -Force | Out-Null
                        Write-Output "  -> Tagged with $CleanupTag = $(Get-Date -Format 'yyyy-MM-dd')"
                    } else {
                        Write-Output "  -> [DRY RUN] Would tag with $CleanupTag"
                    }
    
                    $report += [PSCustomObject]@{
                        Action        = "Tagged"
                        VNetName      = $vnet.Name
                        ResourceGroup = $vnet.ResourceGroupName
                        Subscription  = $subId
                        Reason        = $reason
                        AddressSpace  = $addressSpace
                        Peerings      = $peeringCount
                    }
                }
                elseif ($AutoDeleteAfterGrace) {
                    # Already tagged -- check grace period
                    $tagDate = [datetime]::Parse($taggedDate)
                    $age = (Get-Date) - $tagDate
    
                    if ($age.TotalDays -ge $GracePeriodDays) {
                        Write-Output "[GRACE EXPIRED] $($vnet.Name) | Tagged: $taggedDate | Age: $([math]::Round($age.TotalDays)) days"
    
                        if (-not $DryRun) {
                            # Remove peerings first (both sides)
                            foreach ($peering in $vnet.VirtualNetworkPeerings) {
                                $remoteVNetId = $peering.RemoteVirtualNetwork.Id
                                # Delete local peering
                                Remove-AzVirtualNetworkPeering -VirtualNetworkName $vnet.Name \
                                    -ResourceGroupName $vnet.ResourceGroupName \
                                    -Name $peering.Name -Force
                                Write-Output "  -> Deleted local peering: $($peering.Name)"
    
                                # Delete remote peering
                                $remoteVNetName = ($remoteVNetId -split "/")[-1]
                                $remoteRG = ($remoteVNetId -split "/")[4]
                                $remotePeerings = Get-AzVirtualNetworkPeering -VirtualNetworkName $remoteVNetName \
                                    -ResourceGroupName $remoteRG -ErrorAction SilentlyContinue
                                foreach ($rp in $remotePeerings) {
                                    if ($rp.RemoteVirtualNetwork.Id -eq $vnet.Id) {
                                        Remove-AzVirtualNetworkPeering -VirtualNetworkName $remoteVNetName \
                                            -ResourceGroupName $remoteRG -Name $rp.Name -Force
                                        Write-Output "  -> Deleted remote peering: $($rp.Name) on $remoteVNetName"
                                    }
                                }
                            }
    
                            # Delete the VNet
                            Remove-AzVirtualNetwork -Name $vnet.Name \
                                -ResourceGroupName $vnet.ResourceGroupName -Force
                            Write-Output "  -> DELETED VNet: $($vnet.Name)"
                        } else {
                            Write-Output "  -> [DRY RUN] Would delete VNet and $peeringCount peering(s)"
                        }
    
                        $report += [PSCustomObject]@{
                            Action        = "Deleted"
                            VNetName      = $vnet.Name
                            ResourceGroup = $vnet.ResourceGroupName
                            Subscription  = $subId
                            Reason        = $reason
                            AddressSpace  = $addressSpace
                            Peerings      = $peeringCount
                        }
                    }
                    else {
                        Write-Output "[GRACE PENDING] $($vnet.Name) | Tagged: $taggedDate | Days remaining: $([math]::Round($GracePeriodDays - $age.TotalDays))"
    
                        $report += [PSCustomObject]@{
                            Action        = "Grace pending"
                            VNetName      = $vnet.Name
                            ResourceGroup = $vnet.ResourceGroupName
                            Subscription  = $subId
                            Reason        = $reason
                            AddressSpace  = $addressSpace
                            Peerings      = $peeringCount
                        }
                    }
                }
            }
        }
    }
    
    # Output summary
    Write-Output ""
    Write-Output "=== SUMMARY ==="
    Write-Output "Total orphaned VNets found: $($report.Count)"
    Write-Output "  Tagged (new):    $(($report | Where-Object Action -eq 'Tagged').Count)"
    Write-Output "  Deleted:         $(($report | Where-Object Action -eq 'Deleted').Count)"
    Write-Output "  Grace pending:   $(($report | Where-Object Action -eq 'Grace pending').Count)"
    Write-Output ""
    $report | Format-Table -AutoSize

    Failure mode warning: Always run the runbook with $DryRun = $true first. Review the output to confirm that no VNets tagged for retention, DR, or active use are included in the cleanup list. The $AutoDeleteAfterGrace flag defaults to $false -- set it to $true only after you have validated the discovery and tagging phases across at least two manual cycles.

    What to Do Next

    Step 1: Run the discovery queries -- Use the Azure Resource Graph queries above to identify all orphaned VNets (both Pattern 1 and Pattern 2) across your subscriptions. Export the results and note the total address space held by orphaned VNets.
    Step 2: Map dependencies -- For each orphaned VNet, check for peerings, gateways, DNS zone links, and service endpoints. Pay particular attention to VPN Gateways -- these are the most common source of hidden cost anchored to an orphaned VNet.
    Step 3: Tag, notify, and wait -- Follow the structured cleanup workflow: tag orphaned VNets, notify owning teams, wait for the grace period, then delete. Do not skip the grace period for production subscriptions.
    Step 4: Automate and monitor -- Deploy the weekly cleanup runbook to prevent orphaned VNets from accumulating again. Consider Spotto for continuous, cross-tenant VNet monitoring that surfaces orphaned networks and their dependencies automatically.

    Summary

    VNets are free but not cost-free -- The VNet resource itself has no Azure charge, but orphaned VNets fragment IP address space, clutter network inventories, and frequently anchor paid resources (VPN Gateways, ExpressRoute Gateways, Azure Firewall) that bill regardless of traffic.
    Two detection patterns -- VNets with no subnets are the simplest orphans to identify and clean up. VNets with subnets but no connected resources require checking NICs, delegations, and gateways before confirming orphan status.
    Dependencies must be resolved first -- Peerings (both sides), VPN Gateways, and service endpoints must be cleaned up before the VNet can be deleted. Private DNS zone links are auto-cleaned by Azure on VNet deletion.
    Stale peerings are the most common side effect -- Deleting a VNet without first removing its peerings leaves "Disconnected" peerings on remote VNets. Always delete both sides of every peering before VNet deletion.
    Deletion is permanent -- There is no soft-delete or recovery for VNets. Document the configuration before deleting and use a 14-30 day grace period as the primary safety mechanism.
    IP address reclamation is immediate -- Once a VNet is deleted, its CIDR block is immediately available for reuse. This is often the most valuable outcome in environments experiencing IP address space exhaustion.
    Automation prevents recurrence -- A weekly cleanup runbook with tagging, notification, and grace-period logic keeps orphaned VNets from accumulating. Spotto provides continuous monitoring across all subscriptions and tenants.

    The challenges outlined in this article — orphaned VNets fragmenting IP address space, stale peering connections, VPN Gateways anchored to empty networks — 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.

    Stop Letting Orphaned Networks Clutter Your Azure Environment

    Spotto continuously analyzes your Azure networking footprint to detect orphaned VNets, stale peering connections, and unused gateways — giving you clear, actionable recommendations to keep your network inventory clean across every subscription.

    Free Trial

    Disclaimer: This article is provided for informational purposes only and does not constitute professional advice. Azure pricing, features, and service behaviour 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.