A packet leaves a virtual machine bound for the public internet, and instead of arriving it vanishes. No security rule denied it. No firewall logged a drop. The application simply times out, and the engineer staring at the screen burns an afternoon blaming the network security group when the real culprit is a single Azure route table entry pointing the traffic at a next hop that no longer answers. This is the most common way Azure routing goes wrong, and it is invisible to anyone who has never learned how the platform actually forwards a packet. Azure route tables and the user-defined routes inside them are the steering wheel of a virtual network, and the difference between an engineer who can drive deliberately and one who guesses is the difference between a five-minute fix and a multi-hour incident.
The promise of this guide is concrete. By the end you will hold a working model of how Azure decides where every packet goes, you will know the precedence rules that let a user-defined route override the defaults, you will recognize each next-hop type and the behavior it produces, and you will be able to read the effective routes on a network interface to see the real forwarding decision rather than the one you assumed. That last skill alone converts routing from guesswork into a deterministic read.

How Azure routing works before you touch anything
Every subnet in an Azure virtual network sits behind an invisible router. There is no appliance you can SSH into, no box in a rack, and no configuration file you edit directly. The platform implements a distributed virtual router for you, and it populates that router with a starting set of forwarding entries the moment the network exists. These starting entries are the system routes, and understanding them is the foundation for everything a user-defined route later changes.
When you create a virtual network with the address space 10.0.0.0/16 and carve a subnet at 10.0.1.0/24, Azure immediately gives that subnet the ability to reach every other address inside the same virtual network. It does this with a system route whose destination prefix is the full virtual network address space and whose next hop is the special value the platform labels Virtual network. No configuration on your part creates that entry. It is born with the subnet, and it is why two virtual machines in different subnets of the same virtual network can talk the instant they boot, with no peering, no gateway, and no manual entry.
The platform seeds three more default entries alongside it. One sends everything destined for 0.0.0.0/0, the catch-all that matches any address not matched more specifically, toward a next hop the portal calls Internet. That single entry is why a fresh virtual machine can reach a public endpoint without any work from you. A second entry covers the reserved ranges 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 with a next hop of None, which silently discards traffic to private ranges you have not explicitly claimed, a guard against leaking private traffic out the default internet path. A third entry handles the 100.64.0.0/10 carrier-grade NAT range the same way. These are housekeeping defaults, and most engineers never think about them until a user-defined route interacts with one in a surprising way.
What is the difference between a system route and a user-defined route?
A system route is created automatically by the platform and cannot be deleted, only overridden. A user-defined route is one you author inside a route table and associate with a subnet to change the default forwarding decision. The platform always provides system routes; user-defined routes are optional and exist only to steer traffic deliberately.
That distinction matters because you never remove a system route. You shadow it. When you want traffic that the platform would have sent straight to the internet to instead pass through a firewall, you do not delete the default internet route. You write a more specific or higher-precedence user-defined route that wins the forwarding decision, and the system route quietly stands aside. The mental model to hold is a layered one: the platform lays down a baseline, and your route table writes over the parts of that baseline you care about, leaving the rest untouched.
The container for your overrides is the route table resource. A route table is a first-class Azure object, with its own name, resource group, and region, that holds a set of individual routes. Each route names a destination address prefix, a next-hop type, and, for some next-hop types, a next-hop IP address. A route table on its own changes nothing. It begins to steer traffic only when you associate it with one or more subnets. Association is the act that wires your table into the subnet’s effective forwarding logic, and a single table can serve many subnets, which is how teams keep routing consistent across a whole tier of an application.
If the entry-then-filter model is new to you, the broader treatment in our guide to Azure networking fundamentals walks through how a packet moves from a client to a service and where each layer gets a say. Routing decides the path; filtering decides whether the packet survives that path. This article concentrates on the path.
The mechanics of a forwarding decision
To reason about routing you have to know exactly how the virtual router picks one entry from many. Azure does not evaluate routes in the order you wrote them, and it does not pick the first match it finds. It applies two rules in sequence, and those two rules together explain almost every routing outcome you will ever debug.
The first rule is longest-prefix match. When a packet needs forwarding, the router gathers every route whose destination prefix contains the packet’s destination address, then selects the one with the longest prefix, meaning the most specific match. A path for 10.0.2.0/24 beats a rule for 10.0.0.0/16 for any address inside that smaller block, because the /24 describes 256 addresses while the /16 describes more than sixty-five thousand, and the narrower description is the more precise instruction. This is the same logic every IP router on earth uses, and it is the reason a single highly specific user-defined route can carve a small exception out of a broad default without disturbing anything else.
The second rule resolves what happens when two routes are equally specific, when their prefixes are identical in length. Here Azure applies a fixed source priority: a user-defined route wins over a directive learned from BGP, and a BGP route wins over a system route. So the precedence order, from strongest to weakest among equal-length prefixes, is user-defined, then BGP, then system. This is the heart of how a user-defined route overrides the platform. Write a forwarding entry for 0.0.0.0/0 in your table, associate it with the subnet, and your entry and the default internet entry now have identical prefixes, at which point the source-priority rule hands the win to your user-defined route and every internet-bound packet follows your steering instead.
How does route precedence work between UDR, BGP, and system routes?
Azure first selects the entry with the longest matching prefix. When two routes share the same prefix length, it breaks the tie by source priority: user-defined routes outrank BGP-learned routes, and BGP routes outrank system routes. So a UDR always beats a system route of equal specificity, which is exactly how you override a default.
This two-stage logic, longest-prefix first and source-priority second, is the single most important thing to internalize, and it is worth naming so you can carry it into every incident. Call it the longest-prefix-and-precedence rule: Azure picks the most specific matching route, and ties go to the higher-priority source, so a misrouted packet is read from the effective routes by precedence rather than guessed at. The InsightCrunch routing precedence map below is the artifact to keep beside you when you debug, because once you can place a path on this map you stop arguing about why a packet went where it went.
| Selection step | What the router compares | Winner |
|---|---|---|
| 1. Longest-prefix match | Prefix length of every route whose destination contains the packet address | The most specific prefix (largest mask) |
| 2. Source priority (equal prefix length) | The origin of each equally specific rule | User-defined route over BGP over system route |
| Next-hop resolution | The next-hop type of the winning entry | Determines where the packet is actually sent |
Read the map top to bottom. The router never reaches step two until step one produces a tie, and it never resolves a next hop until a single winning path survives both steps. A wrong forwarding decision is therefore always traceable: either a more specific rule exists than you expected, or an equally specific higher-priority route is shadowing the one you intended. There is no third explanation, which is what makes routing a deterministic read rather than a mystery.
One subtlety trips up engineers coming from on-premises networking. BGP enters this picture when you connect a virtual network gateway to an on-premises network over a VPN or ExpressRoute, or when you enable route propagation from a gateway. The gateway advertises learned prefixes into the subnet’s route set, and those appear as BGP-sourced routes sitting between your user-defined routes and the system defaults in priority. If you have a hybrid connection and a directive is behaving oddly, the propagated BGP routes are the layer most people forget to inspect, and they are visible in the effective routes alongside everything else.
The next-hop types and the behavior each one produces
A forwarding entry answers two questions: which packets it applies to, set by the destination prefix, and where those packets go, set by the next-hop type. The destination prefix is just an address range. The next-hop type is where the real behavior lives, and Azure offers a small, fixed set of them. Knowing exactly what each one does is the difference between writing a entry that works and writing one that quietly black-holes a production workload.
The first and most consequential type is Virtual appliance. A path with this next hop forwards matching packets to a private IP address you specify, almost always the inbound interface of a network virtual appliance such as a firewall, a proxy, or a routing instance. The platform does not inspect or modify the packet. It simply hands it to the address you named and trusts that whatever lives there will forward it onward. This is the workhorse of advanced Azure networking, because it is how you insert any inspection or egress control into the path. When you route 0.0.0.0/0 to the private IP of an Azure Firewall, every internet-bound packet from the subnet lands on the firewall first, gets inspected, and continues only if policy allows. The appliance must have IP forwarding enabled on its network interface, a setting separate from the rule itself, and forgetting it is a classic reason traffic reaches the appliance and dies there.
The second type is Virtual network gateway. A entry with this next hop sends matching traffic to a VPN or ExpressRoute gateway in the virtual network, which is how you steer specific prefixes toward an on-premises network or toward forced tunneling. You name the gateway by type rather than by a private IP, because the gateway is a managed resource with platform-assigned addressing. This is the type that connects routing to hybrid connectivity, and it is the lever behind sending all internet egress back through a corporate network for inspection.
The third type is Internet. A path with this next hop sends matching traffic out to the public internet through the platform’s default egress path. You rarely write one explicitly, because the default system route already covers 0.0.0.0/0 with this next hop, but you do write it as a precise exception. A common pattern routes everything to a firewall with a broad user-defined route, then writes a narrow user-defined route for one trusted public prefix straight to Internet, so that one destination skips the appliance. Longest-prefix match makes that exception work: the narrow rule wins for its addresses, the broad directive catches everything else.
What does a next-hop type of None actually do to my traffic?
A next hop of None drops the packet silently. There is no rejection message, no log entry on the data path, and no error returned to the sender; the traffic is simply discarded as if it fell into a hole. Engineers use None deliberately to block a prefix, but an accidental None is the single most confusing routing failure to diagnose.
That silence is exactly why None deserves its own warning. When a forwarding entry with next hop None wins the forwarding decision, the application sees a timeout, never a refusal, and a timeout points the on-call engineer toward latency, capacity, or a firewall rather than toward routing. The platform’s own private-range defaults use None for good reason, but a hand-written None route, often created to temporarily isolate a subnet and then forgotten, will black-hole production traffic the moment its prefix becomes relevant. Whenever you face a timeout with no corresponding deny anywhere in the security layer, the effective routes are the first place to look, and a winning entry with None as its next hop is the answer more often than anyone expects.
The remaining type is Virtual network, the next hop of the default intra-virtual-network route. You can write it explicitly to restore default behavior for a prefix that a broader route would otherwise capture, which is occasionally useful when you have peered networks and want to keep certain traffic local. The set is deliberately small. Virtual appliance for inspection and custom forwarding, Virtual network gateway for hybrid and tunneling, Internet for explicit public egress, None for an intentional drop, and Virtual network for staying inside the local address space. Five behaviors, and every route you ever write picks exactly one of them.
It helps to see the next-hop types as a single reference you can map any requirement onto. Routing a subnet’s egress through a firewall is Virtual appliance pointed at the firewall’s private IP. Sending one tier’s traffic to on-premises is Virtual network gateway. Carving a public exception out of a firewall-all rule is Internet on a narrow prefix. Quarantining a range during an incident is None. Pulling a specific peered prefix back to local delivery is Virtual network. The requirement dictates the type, and the type dictates the behavior, with no ambiguity once you have the table memorized.
Creating a route table and writing your first user-defined route
Theory becomes useful the moment you can reproduce it, so here is the full lifecycle in working commands. The flow is always the same: create the route table, add one or more routes to it, then associate it with the subnets that should obey it. Reversing that order or skipping the association is the most frequent reason a freshly written route appears to do nothing.
With the Azure CLI, you create the table and add a path to send all egress through a firewall appliance at 10.0.0.4 like this:
# Create the route table in the same region as the virtual network
az network route-table create \
--resource-group net-rg \
--name rt-spoke-egress \
--location eastus
# Add a user-defined route that sends all traffic to the firewall NVA
az network route-table route create \
--resource-group net-rg \
--route-table-name rt-spoke-egress \
--name to-firewall \
--address-prefix 0.0.0.0/0 \
--next-hop-type VirtualAppliance \
--next-hop-ip-address 10.0.0.4
# Associate the route table with the workload subnet
az network vnet subnet update \
--resource-group net-rg \
--vnet-name vnet-spoke \
--name snet-workload \
--route-table rt-spoke-egress
The next-hop type string is case-sensitive and must be one of VirtualAppliance, VirtualNetworkGateway, VirtualNetwork, Internet, or None. The next-hop IP address is required for VirtualAppliance and omitted for the others. The association step on the last command is the one that activates everything; until the route table is bound to the subnet, the routes you added sit inert.
PowerShell expresses the same lifecycle with a slightly different shape, building the rule into the table object and then writing the object back:
$rt = New-AzRouteTable -ResourceGroupName net-rg -Name rt-spoke-egress -Location eastus
Add-AzRouteConfig -RouteTable $rt -Name to-firewall `
-AddressPrefix 0.0.0.0/0 `
-NextHopType VirtualAppliance `
-NextHopIpAddress 10.0.0.4 | Set-AzRouteTable
$vnet = Get-AzVirtualNetwork -ResourceGroupName net-rg -Name vnet-spoke
Set-AzVirtualNetworkSubnetConfig -VirtualNetwork $vnet -Name snet-workload `
-AddressPrefix 10.0.1.0/24 -RouteTableId $rt.Id | Set-AzVirtualNetwork
For anything beyond a one-off experiment you want the route table as code, because a route table edited by hand drifts the moment two engineers touch it. Bicep captures the table, its routes, and the subnet association in a single declarative unit:
resource routeTable 'Microsoft.Network/routeTables@2023-05-01' = {
name: 'rt-spoke-egress'
location: location
properties: {
disableBgpRoutePropagation: false
routes: [
{
name: 'to-firewall'
properties: {
addressPrefix: '0.0.0.0/0'
nextHopType: 'VirtualAppliance'
nextHopIpAddress: '10.0.0.4'
}
}
{
name: 'trusted-public-direct'
properties: {
addressPrefix: '20.150.0.0/16'
nextHopType: 'Internet'
}
}
]
}
}
That template encodes the two-route pattern described earlier: a broad entry to the firewall and a narrow exception straight to the internet, with longest-prefix match guaranteeing the exception wins for its addresses. The same structure is expressible in Terraform with the azurerm_route_table and azurerm_subnet_route_table_association resources, and our hands-on Azure labs and command library on VaultBook keeps tested versions of each of these so you can write a route table, associate it, and read the effective routes against a live subnet rather than against a diagram.
One property in that Bicep deserves a closer look. The field disableBgpRoutePropagation, when set to true, stops BGP routes learned from a virtual network gateway from being injected into this subnet’s route set. Teams set it on subnets that must never see on-premises-advertised routes, for example a perimeter subnet that should only ever egress through its own appliance regardless of what the gateway learns. Leaving it false, the default, lets propagated routes flow in and participate in the precedence logic. The setting is a per-association control, and getting it wrong is a subtle way to either leak or starve a subnet of the routes it needs.
Forced tunneling and why egress is the hardest thing to get right
Forced tunneling is the deliberate redirection of internet-bound traffic away from the platform’s default egress and toward an inspection point, usually an on-premises edge or a firewall appliance. The default behavior of an Azure subnet is to let any virtual machine reach the public internet directly through the platform’s egress, which is convenient and, for many security postures, unacceptable. Regulated environments often require that every byte leaving the estate passes a corporate inspection stack, and forced tunneling is the mechanism that bends Azure egress to that requirement.
The implementation is a user-defined route for 0.0.0.0/0 whose next hop sends traffic somewhere other than the internet. When the destination is on-premises, the next hop is a Virtual network gateway and the gateway carries the traffic across the VPN or ExpressRoute connection to the corporate edge, where the existing inspection stack handles it exactly as it handles traffic from a physical office. When the destination is a firewall inside Azure, the next hop is a Virtual appliance pointed at the firewall’s private IP, and the firewall enforces egress policy before forwarding allowed traffic onward. Either way the principle is identical: shadow the default internet route with a higher-priority route that aims somewhere you control.
Forced tunneling is also where engineers create their most spectacular outages, because a 0.0.0.0/0 route is the broadest possible instruction and it captures traffic you did not consciously think about. The moment you redirect all egress to an appliance, you have made that appliance a hard dependency for everything: package downloads, certificate revocation checks, time synchronization, platform health probes, and the management traffic some Azure services rely on. If the appliance is undersized, misconfigured, or simply down, the entire subnet loses the internet in one stroke, and because the failure presents as universal timeouts it can look like a regional outage rather than a single misrouted prefix.
When should I use forced tunneling instead of a direct egress route?
Use forced tunneling when policy or compliance requires that all outbound internet traffic pass a controlled inspection point, such as a corporate edge or a central firewall, before leaving the estate. If you only need to inspect a subset of destinations, a narrower route to the appliance plus a direct internet exception is leaner and removes the appliance as a dependency for unrelated traffic.
The design discipline forced tunneling demands is sizing the dependency you just created. Before you write the 0.0.0.0/0 route, you decide what happens when the appliance is unavailable, because the answer is no longer nothing. You provision the appliance for the aggregate egress of every subnet you point at it, you give it a health model you can watch, and you plan the rollback: a path deletion or an association removal that restores the platform default in seconds. The central-egress pattern that forced tunneling enables is the backbone of the hub-spoke design covered in our explanation of Azure hub-spoke network topology, where spoke subnets route their egress to a firewall living in a shared hub, and that article is the right next read once you understand the routing layer underneath it.
A frequently missed detail is that forced tunneling interacts with the platform’s own service traffic. Some Azure services expect to reach their control plane or storage over the internet path, and routing 0.0.0.0/0 to an appliance can break them unless the appliance permits that traffic or you add specific exceptions. The disciplined approach pairs the broad redirect with narrow Internet-next-hop routes for the platform prefixes that must bypass inspection, again leaning on longest-prefix match so the exceptions win for their addresses while the broad rule governs everything else. The exact prefixes are the kind of value that shifts over time, so confirm them against the current official guidance rather than treating any list as permanent.
How routing fails, and the diagnostic that ends the argument
Routing failures share a signature: traffic that should arrive does not, and the security layer is innocent. Once you have ruled out a deny rule, the failure is almost always one of a small number of routing patterns, and each one has a confirming read. The single tool that resolves all of them is the effective routes view on the network interface, because it shows the actual, merged route set the platform is enforcing rather than the route table you think you configured.
The effective routes are the union of system routes, your user-defined routes, and any BGP-propagated routes, after precedence has been applied, as seen from one specific network interface. Because association and propagation can differ per subnet, you always read effective routes from the interface attached to the machine that is failing, never from the route table in isolation. The Azure CLI exposes it directly:
az network nic show-effective-route-table \
--resource-group net-rg \
--name vm-workload-nic \
--output table
The output lists each effective route with its source, its address prefix, its next-hop type, and its next-hop IP, and the row that matches your failing destination by longest prefix is the forwarding decision the platform is making right now. Reading that one row resolves the argument: if the next hop is None, you are black-holing; if it is a Virtual appliance whose address is unreachable, you are sending into a dead end; if it is a more specific prefix than you expected, some other route is winning. There is no guessing left after this read.
The first failure pattern is the silent None drop. A rule with next hop None wins for the destination, and the packet disappears with no log and no rejection. This is the pattern behind the opening story of this article, and it is the most disorienting because every other layer looks healthy. The confirming read is an effective-routes row whose next hop is None matching your destination, and the fix is to delete or narrow that directive so a useful route wins instead.
The second pattern is the appliance black hole. A 0.0.0.0/0 route points at a virtual appliance, the appliance is down or unreachable, and all egress dies. The effective routes look correct, which is what makes this one cruel; the forwarding entry is doing exactly what you asked, but the thing it points at is not answering. The confirming read combines the effective route, which shows the appliance IP as the next hop, with a reachability check to that IP and a look at the appliance’s own health. The fix is to restore the appliance, or to fail over to a healthy one, or in an emergency to remove the entry and accept the temporary loss of inspection in exchange for restored connectivity.
How do I read the effective routes for a specific network interface?
Run the effective-routes command against the failing machine’s network interface, not against the route table, because association and BGP propagation can differ per subnet. The output merges system, user-defined, and BGP routes after precedence is applied, so the row whose prefix most specifically matches your destination is the live forwarding decision the platform is enforcing.
The third pattern is the forgotten IP-forwarding flag on an appliance. Traffic reaches the appliance correctly, the path is right, the appliance is up, and yet nothing forwards onward, because the appliance’s network interface has IP forwarding disabled and the platform refuses to let it pass traffic destined for an address other than its own. The confirming read is that packets arrive at the appliance, visible in its own captures, but never leave; the fix is enabling IP forwarding on the interface, a property of the network interface resource rather than of the rule. The fourth pattern is the unintended specific route, where a route someone added for one purpose has a prefix that also captures traffic for an unrelated destination, and longest-prefix match faithfully sends the unrelated traffic the wrong way. The confirming read is an effective-routes row more specific than you expected winning the destination, and the fix is to scope the offending route more narrowly.
The fifth pattern is a propagation surprise. A virtual network gateway is advertising on-premises prefixes into the subnet, one of them overlaps a destination you assumed was internet-bound or local, and the BGP route steers it toward the gateway. Because BGP routes outrank system routes, this can redirect traffic you never wrote a route for. The confirming read is a BGP-sourced row in the effective routes, and the fix is either to scope the on-premises advertisement, to add a higher-priority user-defined route that reclaims the prefix, or to set disableBgpRoutePropagation on the subnet if it should ignore learned routes entirely.
The sixth pattern is the asymmetry trap. Traffic flows out through an appliance correctly but the return path takes a different route that bypasses the appliance, and a stateful firewall, seeing only one direction of the conversation, drops the return packets. This is not a single wrong route but a mismatch between the outbound and inbound paths, and it is common when only one side of a peered or hub-spoke relationship has the steering route. The confirming read is comparing the effective routes on both ends and finding that the return subnet lacks the symmetric route to the appliance; the fix is to make the routing symmetric so both directions traverse the same stateful device. For deeper diagnosis across all of these, the next-hop and connection-troubleshoot tooling in our walkthrough of Azure Network Watcher and diagnostics confirms the live next hop for any source and destination pair, which complements the effective-routes read with an end-to-end path test.
How routing interacts with the rest of the network
A route table never works alone. It sits inside a virtual network alongside network security groups, peering relationships, gateways, and private endpoints, and the interactions among them produce the behavior an engineer actually experiences. Understanding routing in isolation is necessary but not sufficient; you have to know how the path your route defines meets the filter, the peering, and the resolution layers it crosses.
The most common confusion is between a route table and a network security group, because both can stop traffic and both are attached to subnets. They do entirely different jobs at entirely different stages. The route table answers where a packet goes; the security group answers whether a packet is allowed to make a particular hop. Routing happens first, deciding the next hop, and then the security group on the relevant interface evaluates the packet against its priority-ordered rules and either permits or denies it. A packet can be perfectly permitted by every security rule and still never arrive because a route black-holed it, and a packet can be routed flawlessly and still die because a security rule denied it. When you debug, you separate the two questions: first read the effective routes to confirm the path, then read the effective security rules to confirm the permission. Conflating them is why so many afternoons are lost blaming a security group for a routing fault.
Why does blaming the NSG for a routing problem waste so much time?
Because a network security group and a route table fail in superficially similar ways, both producing traffic that does not arrive, but only the security group leaves a deny in its flow logs. A routing black hole shows no deny anywhere, so an engineer who only inspects security rules finds nothing and keeps looking in the wrong layer while the real fault sits in the effective routes.
The rule of thumb that saves the most time is this: a deny in the security layer is loud and logged, while a routing fault is silent. If you see a recorded deny, you have a filtering problem and the route is probably fine. If you see a timeout with no deny anywhere in the security flow logs, suspect routing first and read the effective routes before touching a single rule. The filtering layer itself is covered end to end in our deep dive on network security groups, and the clean mental separation between the two layers, route first then filter, is what lets you debug each without contaminating the other.
Peering changes routing in a way that surprises people who expect it to behave like a router. When two virtual networks are peered, the platform automatically adds system routes so that addresses in the peer network resolve to a next hop of Virtual network, and traffic flows directly between them. Peering does not transit by default, which means if network A peers with a hub and the hub peers with network B, A cannot reach B simply because both touch the hub. Reaching B requires either a direct peering or a user-defined route in A that sends B’s prefix to a routing device in the hub, paired with gateway transit or an appliance that forwards it. This is precisely the steering that makes a hub-spoke topology work: spokes do not peer with each other, they route through a shared device in the hub, and the route tables on the spokes are what create that transit. The peering model and its non-transitive nature are detailed further in the networking fundamentals article, and the routing layer in this guide is what you add on top of peering to build deliberate paths between spokes.
Private endpoints add a wrinkle worth calling out. A private endpoint projects an Azure service into your virtual network as a private IP, and the platform installs a highly specific system route, a /32 for that endpoint’s address, that you generally must not override. If a broad user-defined route for a large prefix happens to contain the private endpoint’s address, longest-prefix match still favors the platform’s /32, but problems arise when a forced-tunneling route and an appliance interact with private-endpoint traffic in ways the appliance does not expect. The disciplined pattern keeps private-endpoint traffic on its native, most-specific route and only steers the broader traffic, which longest-prefix match makes straightforward as long as you do not write a competing /32. Service endpoints behave differently again, because they do not introduce a private IP and instead tag traffic at the subnet level, so they interact with routing far less and rarely require a user-defined route to function.
Gateways tie the routing layer to hybrid connectivity. A virtual network gateway for VPN or ExpressRoute both originates BGP routes into your subnets, when propagation is enabled, and serves as a valid next-hop type for traffic you want to send on-premises. The interplay is bidirectional: the gateway tells your subnets about remote prefixes, and your routes can tell traffic to use the gateway. When forced tunneling sends 0.0.0.0/0 to the gateway, you are relying on the gateway and the connection behind it to carry every internet-bound packet to the corporate edge, which is why gateway capacity and connection health become part of your egress availability story. Reasoning about these interactions deliberately, rather than discovering them during an incident, is the habit this series tries to build: you derive the behavior from how the pieces actually compose rather than guessing from a symptom.
Designing route tables for production
Writing a single route that works is easy. Designing a routing layout that stays correct as the environment grows, survives multiple engineers editing it, and fails safely is the harder skill, and it rests on a few principles that pay off repeatedly.
Keep route tables few and shared rather than many and bespoke. A single route table associated with every subnet that should follow the same egress policy is far easier to reason about than a unique table per subnet, because one change updates them all and there is one place to read the intent. Teams that create a table per subnet end up with dozens of nearly identical tables that drift apart over time, and a drifted table is exactly where the silent failures hide. Name tables and routes for their purpose, not their mechanics, so that rt-spoke-egress and a route named to-firewall tell the next engineer what the intent is without reverse-engineering the prefixes.
Treat the broad 0.0.0.0/0 route as a load-bearing dependency and design its failure mode explicitly. Because that route makes whatever it points at a hard dependency for all egress, you decide in advance how you detect that the target is unhealthy, how you fail over, and how fast you can roll back. The fastest rollback is removing the subnet association, which restores the platform defaults in seconds without deleting your carefully built table, and rehearsing that rollback before you need it turns a potential multi-hour outage into a brief blip. Pair the broad route with the narrow exceptions your platform genuinely needs, the trusted public prefixes and any service traffic that must bypass the appliance, and keep that exception list under change control so it does not silently rot.
Make routing symmetric on purpose. Any path that crosses a stateful device must be traversed in both directions by the same device, or the device will drop the return traffic of a conversation it only half saw. In a hub-spoke design this means the spoke routes its egress to the hub appliance and the hub, or the return path, routes the spoke’s prefix back through the same appliance, so the firewall sees the full conversation. Asymmetry is the failure that works in testing, because a single flow happens to take a consistent path, and breaks under real load when paths diverge, which makes it one of the most expensive routing bugs to discover late.
Manage every route table as code and review changes the way you review application changes. A route table edited by hand in the portal has no history, no review, and no rollback beyond memory, and routing changes are among the highest-blast-radius edits in a cloud environment because a wrong 0.0.0.0/0 can take down a whole tier. Expressing the table in Bicep or Terraform, with the routes and the subnet associations in the same module, gives you a diff to review, a record of who changed what, and a deterministic way to recreate the layout in another environment. The decision to set disableBgpRoutePropagation belongs in that code too, documented with the reason, because a future engineer staring at a subnet that ignores on-premises routes deserves to know it was deliberate.
Finally, instrument the routing layer so a fault announces itself rather than waiting for a user report. Watching the health of any appliance a 0.0.0.0/0 route depends on, and alerting when a subnet’s effective routes change unexpectedly, converts the silent class of routing failures into something observable. The whole thesis of reasoning from the mechanism pays off here: when you understand that routing failures are silent by nature, you compensate by adding the observation the data path does not give you for free, and you stop being surprised by timeouts that no log explains.
A worked diagnosis from symptom to fix
Principles land harder when you watch them resolve a real incident, so here is one end to end. A team runs a hub-spoke estate. The hub holds a firewall appliance at 10.0.0.4. A spoke subnet, snet-workload at 10.1.1.0/24, is supposed to send all egress through that firewall, and for weeks it did. One morning a deployment in the spoke begins timing out on every outbound call. Nothing was changed in the application, the firewall dashboard shows it healthy, and the security group on the subnet shows no denies. The on-call engineer opens a ticket blaming the firewall.
The disciplined first move is to read the effective routes on the failing machine, not to log into the firewall. The engineer runs the effective-routes command against the workload virtual machine’s interface and scans for the row that governs 0.0.0.0/0. What appears is revealing. There are two competing entries near the top of the egress decision. The expected one is a user-defined route for 0.0.0.0/0 with next hop VirtualAppliance pointing at 10.0.0.4. Sitting beside it, however, is a more specific user-defined route, 0.0.0.0/1 and 128.0.0.0/1, a pair that together covers the entire address space but with longer prefixes than 0.0.0.0/0, and that pair points at next hop None. Someone had added the two-halves trick a week earlier to quarantine the subnet during a maintenance window and never removed it. Because each half is a /1 and therefore more specific than the /0, longest-prefix match hands every internet-bound packet to the None routes, and the firewall route never gets a chance to win.
This is the longest-prefix-and-precedence rule doing exactly what it should, and it is also the reason the firewall looked innocent: traffic never reached it, because it was being dropped one step earlier by a more specific route. The confirming read took under a minute and pointed at the precise rows responsible. No firewall login, no packet capture, no escalation. The fix is equally precise: delete the two /1 None routes from the route table, after which 0.0.0.0/0 to the appliance becomes the most specific egress route again and traffic resumes flowing through the firewall. The engineer verifies by re-reading the effective routes and confirming the appliance route now governs the catch-all, then watches a test call succeed.
Why did the more specific route win even though it was added by accident?
Azure does not care about intent or creation order; it cares about prefix length. The two /1 routes are more specific than the /0 firewall route, so longest-prefix match selects them regardless of why they exist. Intent lives in your change process, not in the router, which is exactly why routing edits belong under review.
The lesson generalizes past this one incident. Every routing mystery has the same shape: a packet went somewhere unexpected, and the effective routes on the failing interface contain the row that explains it. The skill is not memorizing failure stories but trusting the read. The moment you suspect routing, you stop theorizing and you look at the merged route set the platform is actually enforcing, because that view is ground truth and your mental picture of the route table is not. The two diverge precisely when an extra route, a propagated prefix, or a forgotten quarantine entry has crept in, which is to say they diverge exactly when you have an incident.
Walking the same scenario in the opposite direction is instructive too. Suppose instead the effective routes show the firewall route winning correctly, next hop 10.0.0.4, and yet egress still fails. Now the routing layer is exonerated and the next read is whether 10.0.0.4 is reachable and forwarding. The engineer checks that the appliance interface has IP forwarding enabled, confirms the appliance is up, and tests reachability to its private IP from the subnet. If the appliance is down, the route is faithfully steering traffic into a dead end, and the choice under pressure is to fail over to a healthy appliance or, as an emergency measure, remove the association to restore default egress while the appliance is repaired. The point of separating the two reads, first the route then the target, is that each answers a different question and conflating them sends you in circles.
The real-world routing patterns engineers keep hitting
Beyond single incidents, a handful of routing patterns recur across almost every Azure estate, and recognizing them by shape lets you skip straight to the confirming read. Each is worth describing as a pattern the routing model explains rather than as a one-off war story, because the same five or six shapes account for the overwhelming majority of routing tickets.
The quarantine that outlived its purpose is the first. A None route, or a pair of /1 None routes, gets added to isolate a subnet during maintenance, an investigation, or a security event, and then the urgency passes and nobody removes it. Weeks later a new workload lands in the subnet and cannot reach anything, presenting as universal silent timeouts. The pattern is so common that any silent egress failure should trigger an immediate effective-routes read looking for a None row. The prevention is treating quarantine routes as temporary by construction: add them with a documented expiry and a ticket to remove them, ideally through code so the removal is a reviewed change rather than a memory.
The undersized or unmonitored appliance behind a forced-tunnel route is the second. A 0.0.0.0/0 route to a virtual appliance works beautifully until the appliance saturates under load, restarts, or fails, at which point the entire subnet loses egress at once. Because the route is correct, the effective routes look healthy and the instinct is to blame the network, when the real fault is the health of the target. The pattern teaches that a forced-tunnel route converts its target into a load-bearing dependency for all egress, so the target needs capacity sized for aggregate load and a health signal you actively watch. The confirming read pairs the effective route, which names the appliance IP, with a reachability and health check of that IP.
The spoke that cannot reach another spoke is the third, and it is pure peering non-transitivity. Two spokes peer with a hub, an engineer assumes they can therefore talk, and they cannot, because peering does not transit. The effective routes on each spoke show a next hop of Virtual network for the hub’s range but nothing that resolves the other spoke, so traffic to the far spoke falls to the default internet route and fails or leaves the estate. The pattern teaches that spoke-to-spoke connectivity is something you build with user-defined routes through a hub device, not something peering grants for free, and that the return path must be symmetric so a stateful firewall in the hub sees both directions. The hub-spoke topology guide covers the topology shape; the routes in this article are the connective tissue that makes the shape carry traffic.
The accidental broad capture is the fourth. An engineer adds a route for a large prefix to steer one destination, and the prefix turns out to contain other addresses that should have gone elsewhere, so longest-prefix match faithfully redirects the collateral traffic. The symptom is that one specific service breaks while everything else works, and the effective-routes read shows an unexpectedly broad route winning the broken destination. The pattern teaches scoping routes as narrowly as the requirement allows, because a route’s blast radius is exactly its prefix, and a route written one mask too wide quietly captures neighbors.
The propagation overlap is the fifth. A hybrid connection advertises an on-premises prefix that overlaps something you treated as internet-bound or local, and because BGP outranks system routes, the learned route silently steers the overlapping traffic toward the gateway. The symptom is a destination that used to work suddenly routing to on-premises after a network change on the corporate side that you did not make. The effective-routes read shows a BGP-sourced row winning the destination, and the resolution is to scope the advertisement, add a higher-priority user-defined route, or disable propagation on the subnet if it should ignore learned routes. The pattern teaches that your routing is not entirely under your control once a gateway propagates routes, so the BGP rows in the effective routes are a layer you inspect whenever a hybrid environment behaves unexpectedly.
The asymmetric stateful path is the sixth, and it is the one that passes every quick test and breaks in production. Outbound traffic traverses a stateful firewall through a route, the return traffic takes a path that bypasses it, and the firewall drops the return half of a conversation it never fully saw. The symptom is intermittent or load-dependent failure that vanishes when you try to reproduce it with a single flow, because a lone flow can happen to take a consistent path. The confirming read compares effective routes on both ends of the conversation and finds the return side lacks the symmetric route. The pattern teaches designing symmetry deliberately wherever a stateful device sits in the path, because the router has no notion of a conversation and will gladly send the two directions down different roads.
These six shapes, the stale quarantine, the dependent appliance, the non-transitive spoke, the broad capture, the propagation overlap, and the asymmetric path, are not exotic. They are the daily bread of Azure routing incidents, and an engineer who can name the shape from the symptom and confirm it with one effective-routes read resolves in minutes what an engineer guessing at the firewall resolves in hours.
Route table limits, scale, and the numbers to verify
Routing at scale runs into platform limits, and while the exact figures move over time and must be confirmed against the current official limits at read time, the shape of the constraints is durable enough to design around. A route table holds a bounded number of individual routes, a virtual network supports a bounded number of route tables, and a subscription has its own ceilings on networking resources. Designing as though these ceilings are generous is a mistake that surfaces only when a large environment hits a wall mid-growth, so it is worth knowing the dimensions even when you treat the specific numbers as values to verify.
The route count per table is the limit you brush against first in a complex egress design, particularly when you enumerate many specific prefixes for exceptions to a forced-tunnel rule rather than relying on a small number of broad routes plus narrow carve-outs. The discipline that keeps you well under any route ceiling is the same discipline that keeps a table readable: prefer a few broad routes with a handful of specific exceptions over a long catalog of individual prefixes, and let longest-prefix match do the work of resolving the exceptions. A table with five well-chosen routes is both easier to reason about and far from any limit; a table with two hundred hand-maintained prefixes is fragile in every sense.
The number of route tables and their associations matters when you adopt the shared-table discipline at scale, because a single table associated with many subnets counts its associations against the relevant ceiling. This is rarely a real constraint for sensible designs, since sharing a table across subnets reduces table count rather than inflating it, but it is worth knowing that associations are a counted dimension when you architect a large estate with many subnets following a common policy. The right instinct, fewer tables shared widely, both simplifies operations and stays comfortably within limits.
A subtler scale consideration is the interaction between route count and the effective-routes view’s readability. As BGP propagation injects on-premises prefixes, the effective route set on an interface can grow well beyond the routes you authored, sometimes into the hundreds when a hybrid environment advertises a large address plan. The effective-routes view still resolves deterministically by longest-prefix and priority, but a human reading it during an incident has more rows to scan, which is another argument for keeping your own authored routes few and purposeful so the rows you care about stand out against the propagated background. When the learned set is large and noisy, the next-hop diagnostic that answers a single source-and-destination question becomes the faster read than scanning the whole table.
Throughput and the data path are not a route table limit in the usual sense, because the route table itself is a control-plane construct that programs the distributed router and adds no per-packet cost of its own. What does carry a throughput ceiling is whatever your routes point at: a virtual appliance has a bandwidth and connection-rate capacity, a gateway has a SKU-bound throughput, and a forced-tunnel design inherits the smallest of those ceilings as the egress limit for every subnet it governs. So the scale question for routing is rarely about the routes and almost always about the capacity of the next hops they select, which is one more reason to treat a forced-tunnel target as a sized, monitored dependency rather than an afterthought. Confirm every appliance and gateway throughput figure against the current official specifications, because SKUs and their limits are revised regularly.
A repeatable routing triage you can run under pressure
When an incident is live, you do not want to improvise a diagnostic method; you want a sequence you have run before. The InsightCrunch routing triage is that sequence, four reads in a fixed order that take a routing symptom from confusion to a named cause without wandering into adjacent layers. It works because Azure routing is deterministic, so a disciplined read of the right views always converges.
The first read is the security flow logs for a deny. This sounds like the wrong place to start an investigation into routing, but it is the fastest way to rule routing out: if a security group recorded a deny for the failing flow, you have a filtering problem, not a routing problem, and you go work the rules. Only when the security layer shows no deny for a failure does the silence point you toward routing, and that silence is itself a strong signal because routing faults, unlike filtering denies, leave no data-path log. Establishing the absence of a deny early prevents the most common waste, which is debugging the route layer for a problem the security layer is actually causing, or the reverse.
The second read is the effective routes on the failing machine’s interface, scanning for the row that governs the failing destination by longest prefix. This is the central read of the entire triage, and most incidents resolve here. You are looking for one of the known shapes: a None next hop, an appliance address you did not expect, a more specific prefix than you wrote, or a BGP-sourced row steering traffic you did not route. The row that wins the destination is the forwarding decision, and naming it usually names the cause. You read from the interface and not the table because association and propagation make the interface view the only authoritative one.
The third read, when the winning route points at a virtual appliance, is the health and reachability of that appliance, plus the IP-forwarding flag on its interface. A correct route into a dead appliance presents identically to a misroute until you check the target, so this read distinguishes a routing fault from a target fault. You confirm the appliance is up, that its interface forwards, and that its own policy permits the traffic, and any of those failing explains a symptom that the effective routes alone made look healthy.
The fourth read, reserved for hybrid environments and propagation surprises, is the BGP-sourced rows in the effective routes and the on-premises advertisement behind them. When a destination routes somewhere unexpected and no user-defined route explains it, a learned prefix usually does, and this read finds it. You confirm which advertised prefix overlaps the destination and decide whether to scope the advertisement, reclaim the prefix with a higher-priority user-defined route, or disable propagation on the subnet.
Run in this order, the triage rarely needs all four reads, because most faults resolve at the second. The value of the fixed sequence is that it removes improvisation from a high-pressure moment and guarantees you never skip the cheap, decisive read in favor of an expensive guess. Practicing it against a live environment, the kind of hands-on repetition the VaultBook labs are built for, turns the sequence into reflex, so that when a real incident lands you execute the reads without having to remember them. That reflex, more than any single fact about next-hop types, is what separates an engineer who owns the routing layer from one who fears it.
The gateway subnet and the routing rules you should not break
Not every subnet is a normal subnet, and the one that most often punishes a careless route is the gateway subnet, the dedicated subnet named GatewaySubnet that hosts a VPN or ExpressRoute gateway. The platform programs specific routing behavior into that subnet so the gateway can do its job, and a user-defined route applied there can interfere with the control traffic the gateway depends on, breaking the very connectivity you were trying to shape. The safe default is to leave the gateway subnet’s routing alone unless you have a precise, documented reason and you understand exactly which prefixes you are touching.
The temptation to add a route to the gateway subnet usually comes from a desire to inspect or redirect traffic transiting the gateway, for example to send on-premises-bound traffic through a firewall before it reaches the gateway. This can be legitimate, but it has to be done with surgical prefixes that do not capture the gateway’s own management and BGP traffic, and it interacts with the platform’s expectations in ways that change over time. Because the consequences of getting it wrong include losing the hybrid connection entirely, this is the one place where the usual advice to experiment freely is replaced by advice to plan precisely, test in a non-production environment first, and keep the change reversible. If your goal is to inspect on-premises-bound traffic, it is frequently cleaner to apply the steering on the workload subnets that originate the traffic rather than on the gateway subnet itself, because the workload subnet is a normal subnet where user-defined routes behave predictably.
Can I put a route table on the GatewaySubnet to inspect VPN traffic?
You can associate a route table with the GatewaySubnet, but it is risky, because routes there can interfere with the gateway’s control and BGP traffic and break the connection. Prefer steering on the workload subnets that originate the traffic, where user-defined routes behave predictably, and reserve gateway subnet routing for precise, tested, reversible changes with a documented reason.
The gateway also shapes routing through propagation in a way that the gateway subnet caveat does not capture. When the gateway learns on-premises prefixes over BGP and propagation is enabled on a subnet, those prefixes appear in that subnet’s effective routes with the gateway as their next hop, which is how return traffic to on-premises finds its way back. This is the mechanism that makes a VPN or ExpressRoute connection bidirectional without you writing routes for every on-premises prefix by hand. The flip side is that disabling propagation on a subnet, through disableBgpRoutePropagation, severs that automatic return path, so a subnet that should reach on-premises must either keep propagation on or carry explicit user-defined routes to the gateway for the on-premises prefixes it needs. Choosing between propagation and explicit routes is a real design decision: propagation is automatic but couples your routing to whatever the corporate side advertises, while explicit routes are verbose but fully under your control, and the right choice depends on whether you value automatic adaptation or strict local determinism for that subnet.
A related subtlety is route summarization across the hybrid boundary. The on-premises side often advertises summarized prefixes, and the granularity of what the gateway learns determines how specific the effective routes become and therefore how they interact with your own user-defined routes by longest-prefix match. If the on-premises summary is broad and your user-defined route for the same region is more specific, your route wins for its addresses, which can be intended or accidental depending on whether you meant to keep that traffic local. Reasoning about this requires looking at the actual advertised prefixes in the effective routes rather than assuming the on-premises address plan, because the plan you think is in place and the prefixes actually advertised diverge surprisingly often, especially after a corporate network change you were not told about.
Validating route changes before they reach production
The highest-blast-radius edit in a virtual network is a change to a broadly scoped route, because a single wrong 0.0.0.0/0 can sever egress for an entire tier in one deployment. That risk argues for validating routing changes with the same rigor you apply to application releases, and Azure gives you enough tooling to do exactly that if you build the habit. The goal is to catch a bad route in a review or a staging environment rather than in a production incident.
The first validation layer is the diff your infrastructure-as-code produces. When the route table lives in Bicep or Terraform, every change appears as a reviewable diff that names the exact prefix, next-hop type, and next-hop address being added, changed, or removed, and a reviewer who understands longest-prefix match can often spot a dangerous edit on sight, for example a new route whose prefix is broader than intended or a next-hop address that does not correspond to a known appliance. This human read is cheap and catches a meaningful share of mistakes before they deploy, which is the entire argument for managing routing as code rather than editing it in the portal where changes have no diff and no reviewer.
The second layer is a staging environment that mirrors the production routing topology closely enough that a route change behaves the same way in both. You deploy the route change to staging, read the effective routes on a representative interface there, and confirm the winning route for your critical destinations is the one you intended before you promote the change. This is where the effective-routes read earns its keep proactively rather than reactively: instead of reading it during an incident to explain a failure, you read it during a deployment to confirm success, comparing the before and after route sets to verify the change did only what you meant. A route change that alters the winning route for an unexpected destination shows up immediately in that comparison.
The third layer is the next-hop diagnostic used as a pre-flight check. For each critical source-and-destination pair, you ask the platform what the next hop will be after the change, and you confirm it matches the design. Because this answers a specific question about a specific flow, it is faster to interpret than scanning a large effective-routes table, and it is well suited to scripting into a deployment pipeline as an automated gate that fails the release if a critical flow would route somewhere unexpected. Turning routing validation into a pipeline gate is the point at which routing stops being a source of surprise outages and becomes a controlled, tested part of delivery, the same way a failing test blocks a bad application change.
The fourth layer is the rehearsed rollback, which is less a validation than a safety net but belongs in the same discipline. Before you deploy a broad route change to production, you confirm you can remove the subnet association in seconds to restore the platform defaults, and ideally you script that rollback so it is a single, fast action under pressure rather than an improvisation. A change you can instantly reverse is a change you can deploy with far more confidence, because the worst case is a brief interruption rather than a prolonged outage while you reconstruct the previous state from memory. The combination of a reviewed diff, a staging confirmation, a next-hop pre-flight gate, and a rehearsed rollback turns the most dangerous edit in the network into one of the most controlled, which is exactly the inversion a mature platform practice aims for. Practicing these validation reads against a sandbox, rather than learning them during a real change window, is the kind of repetition that makes them automatic, and it is precisely the muscle that hands-on lab environments are designed to build.
Routing anti-patterns worth retiring
Some routing habits survive in estates not because they work well but because they have not yet caused an outage, and naming them helps you retire them before they do. Each anti-pattern is a violation of one of the principles this guide has built, and seeing the principle in the breach makes it stick.
The first anti-pattern is the portal-edited route table with no code behind it. A table changed by hand has no diff, no reviewer, and no reliable history, so a wrong edit deploys instantly with nothing standing between the engineer and a tier-wide outage, and the previous good state lives only in someone’s memory. The reasoning that retires this habit is simple: routing edits are among the highest-blast-radius changes in the network, and a change that dangerous deserves the same review and rollback you give application code. Move the table into Bicep or Terraform, and the anti-pattern dissolves into a reviewed diff.
The second is the sprawling per-subnet table. Creating a unique route table for every subnet feels tidy at first and becomes a liability as the count grows, because dozens of nearly identical tables drift apart under different hands and the drift is exactly where silent faults breed. The principle in the breach is that fewer shared tables are easier to reason about and to change safely, so the cure is to consolidate subnets with the same policy onto one table and reserve bespoke tables for genuinely distinct routing needs.
The third is the long catalog of specific prefixes where a few broad routes with narrow exceptions would do. Hand-maintaining a hundred individual destination routes is fragile, hard to read, and pushes toward platform route limits, and it usually exists because someone did not trust longest-prefix match to resolve a broad rule plus a few carve-outs. The principle in the breach is that the router already does specificity resolution for you, so leaning on it produces a table a human can actually understand. Replace the catalog with a broad route and the handful of exceptions that genuinely differ.
The fourth is the forgotten quarantine route, the None entry added under pressure and never removed. It is the single most common cause of a silent egress failure weeks later, and it persists because nothing forces its removal once the urgency that created it has passed. The principle in the breach is that temporary changes need to be temporary by construction, so the cure is to add quarantine routes through code with a tracked removal, never as an untracked emergency edit that depends on memory to undo.
The fifth is the unmonitored forced-tunnel dependency. A 0.0.0.0/0 route to an appliance is treated as set-and-forget, when in fact it has made that appliance a hard dependency for all egress whose health nobody is watching. The principle in the breach is that a load-bearing dependency needs a health signal and a rehearsed failover, so the cure is to size, monitor, and plan rollback for any target a broad route points at, rather than discovering its importance during the outage it causes.
The sixth is asymmetric routing across a stateful device, tolerated because it passes a quick test. Routing the two directions of a conversation down different paths works until a stateful firewall sees only one side and drops the rest, and it lingers because the failure is load-dependent and intermittent rather than immediate. The principle in the breach is that a stateful device must see both directions of a flow, so the cure is to design symmetry deliberately wherever such a device sits in the path, verifying it by comparing effective routes on both ends rather than trusting a single test flow.
Retiring these six habits does not require new tools, only the discipline to apply the principles consistently: manage routing as reviewed code, keep tables few and broad with narrow exceptions, treat temporary routes as temporary, monitor the dependencies your broad routes create, and design symmetry on purpose. The reasoning behind each is the same reasoning the whole series argues for, deriving the right practice from how the platform actually behaves rather than from habit or cargo-culted advice, and routing rewards that reasoning more directly than almost any other layer because its behavior is so strictly deterministic.
The verdict
Azure routing is deterministic, and that is the entire point. The platform lays down system routes, you shadow the ones you care about with user-defined routes, and the virtual router resolves every packet by longest-prefix match first and source priority second, with user-defined routes outranking BGP and BGP outranking system. Hold that one rule and the next-hop table beside it, and you can predict where any packet will go and explain where any packet went. The failures that feel like dark magic, the silent None drop, the appliance black hole, the asymmetry trap, the propagation surprise, all reduce to a single read of the effective routes on the failing interface, after which the answer is sitting in one row.
The engineers who struggle with Azure networking are usually the ones who learned the security layer and never learned the routing layer underneath it, so every routing fault arrives disguised as a firewall problem they cannot find. The engineers who move fast learned to ask the routing question first: where is this packet supposed to go, and where does the effective-routes view say it is actually going. Build route tables few and shared, treat the broad egress route as the load-bearing dependency it is, keep routing symmetric across stateful devices, manage the whole layer as code, and instrument it so silence becomes signal. Do that, and routing stops being the thing that ruins an afternoon and becomes the steering wheel you intended it to be.
The deeper payoff is transferable. Once you trust that the platform resolves every forwarding decision by the same two rules, you stop treating each new connectivity puzzle as a fresh mystery and start treating it as another instance of a model you already hold. A packet went somewhere unexpected, so you read the merged set on the failing interface, find the row that won by specificity or priority, and either correct the entry, scope the prefix, or restore the target it points at. That single habit, reasoning from the mechanism rather than pattern-matching to a past incident, is what the entire series is built to instill, and the deterministic nature of Azure forwarding makes this layer the cleanest place to practice it before carrying the same discipline into the messier layers above.
Frequently Asked Questions
Q: What are user-defined routes in Azure and how do they work?
A user-defined route is a forwarding entry you author inside a route table to override Azure’s automatic system routes for a subnet. Each one names a destination address prefix, a next-hop type such as VirtualAppliance or VirtualNetworkGateway, and, for an appliance, a next-hop IP address. The route does nothing until you associate its parent route table with one or more subnets. Once associated, the virtual router merges your route with the system and any BGP routes and applies it through longest-prefix match and source priority, where a user-defined route outranks an equally specific system route. The practical effect is deliberate steering: you can send all egress to a firewall, pull a prefix toward on-premises, or carve a direct internet exception out of a broad rule, all by writing routes that win the forwarding decision over the platform defaults.
Q: How does Azure decide which route wins when several routes match a packet?
Azure applies two rules in order. First it performs longest-prefix match, selecting the route whose destination prefix most specifically contains the packet’s destination address, so a /24 beats a /16 for any address inside the smaller block. Second, only when two matching routes have prefixes of identical length, it breaks the tie by source priority: a user-defined route outranks a BGP-learned route, which outranks a system route. The router never consults source priority until longest-prefix match produces a tie, and it resolves the next hop only after a single winning route survives both stages. This ordering is why a single highly specific user-defined route can carve a narrow exception without disturbing the broader defaults, and why writing a 0.0.0.0/0 user-defined route overrides the default internet system route of the same length.
Q: What next-hop types can an Azure route use and what does each one do?
Azure offers five next-hop types. VirtualAppliance forwards the packet to a private IP you specify, usually a firewall or proxy, and requires IP forwarding enabled on that appliance. VirtualNetworkGateway sends traffic to a VPN or ExpressRoute gateway, the path for hybrid connectivity and forced tunneling. Internet sends traffic out the platform’s public egress, used mainly as an explicit exception to a broader rule. None silently discards the packet with no log or rejection, used to block a prefix deliberately. VirtualNetwork keeps traffic on the default intra-virtual-network path. The destination prefix decides which packets a route applies to, and the next-hop type decides their fate, so every route you write picks exactly one of these five behaviors for its matched traffic.
Q: Why is my Azure traffic being dropped with no error or log entry?
A silent drop with no rejection and no flow-log deny almost always means a route with a next hop of None is winning the forwarding decision for that destination. None discards the packet as if it fell into a hole, so the sender only ever sees a timeout, which misleads engineers toward latency or capacity. Read the effective routes on the failing machine’s network interface and look for a None row whose prefix matches your destination by longest prefix. Such routes are often created to isolate a subnet temporarily and then forgotten, after which they black-hole production traffic the moment their prefix becomes relevant. The fix is to delete or narrow the None route so a useful route wins instead. Whenever a timeout has no corresponding deny anywhere in the security layer, suspect this pattern before anything else.
Q: What is forced tunneling in Azure and when should I use it?
Forced tunneling redirects internet-bound traffic away from the platform’s default egress toward a controlled inspection point, implemented as a user-defined route for 0.0.0.0/0 whose next hop is a virtual network gateway, sending traffic to on-premises, or a virtual appliance such as a central firewall. Use it when policy or compliance requires that all outbound traffic pass corporate inspection before leaving the estate. It is powerful but creates a hard dependency: the appliance or gateway now carries every byte of egress, so an outage there removes the internet for the whole subnet. Before enabling it, size the target for aggregate egress, give it a health model you can watch, pair the broad redirect with narrow exceptions for platform traffic that must bypass inspection, and rehearse the rollback of removing the route or the subnet association so you can restore default egress in seconds.
Q: How do I read the effective routes for a network interface in Azure?
Run the effective-routes view against the failing machine’s network interface rather than against the route table, because association and BGP propagation can differ per subnet. With the CLI the command is az network nic show-effective-route-table with the resource group and interface name, and table output. The result is the merged set of system, user-defined, and BGP routes after precedence has been applied, each row showing its source, address prefix, next-hop type, and next-hop IP. The row whose prefix most specifically matches your destination is the live forwarding decision the platform is enforcing right now. Reading that one row ends the debate: a None next hop means a black hole, an unreachable appliance address means a dead end, and a more specific prefix than expected means another route is winning. This is the single most useful diagnostic in Azure routing.
Q: How does longest-prefix matching choose between two overlapping routes?
Longest-prefix matching selects the route whose destination prefix is the most specific match for the packet’s destination, where specificity means the longer subnet mask. For a packet to 10.0.2.50, a route for 10.0.2.0/24 beats a route for 10.0.0.0/16 because the /24 describes a narrower set of addresses and therefore a more precise instruction. This is standard IP routing behavior, and it is what lets you layer exceptions: a broad route can govern most traffic while a narrow route carves out a single destination, with the narrow route automatically winning for its addresses. The mechanism only consults route source priority when two matching routes have prefixes of exactly the same length, so most everyday routing outcomes are decided purely by which route is more specific, before priority ever enters the picture.
Q: Do I need a route table to enable communication within a single virtual network?
No. The moment you create a virtual network and its subnets, Azure installs a system route covering the full virtual network address space with a next hop of Virtual network, so every subnet can reach every other subnet inside the same network with no configuration. You add a route table only when you want to change that default behavior, for example to force traffic between subnets through a firewall, to send egress to an appliance, or to steer specific prefixes toward a gateway. The system routes also give you direct internet egress and silently drop traffic to unclaimed private ranges. Route tables are an override mechanism layered on top of these defaults, never a prerequisite for basic connectivity, which is why two virtual machines in different subnets can talk the instant they boot.
Q: How do peering and routing interact when I want spoke-to-spoke traffic?
Peering adds system routes so each peered network reaches the other directly, but peering is not transitive: if two spokes both peer with a hub, they cannot reach each other through the hub automatically. To enable spoke-to-spoke traffic you write user-defined routes on each spoke that send the other spoke’s prefix to a routing device in the hub, typically a firewall’s private IP as a virtual appliance next hop, and you ensure the return path is symmetric so a stateful device sees both directions. This deliberate steering through a shared hub device is exactly what makes a hub-spoke topology function, since spokes intentionally do not peer with each other. The route tables on the spokes create the transit that peering alone does not provide, and getting the symmetry right is essential to avoid a stateful firewall dropping one-directional return traffic.
Q: What is the difference between a route table and a network security group?
They operate at different stages and answer different questions. A route table decides where a packet goes by selecting a next hop, and that decision happens first. A network security group decides whether a packet is allowed to make a hop, evaluating it against priority-ordered allow and deny rules on the relevant interface. A packet can be fully permitted by every security rule and still vanish because a route black-holed it, and it can be routed perfectly and still die because a rule denied it. The crucial diagnostic difference is that a security deny is logged and loud, while a routing fault is silent. When debugging, read the effective routes first to confirm the path, then read the effective security rules to confirm permission, and never conflate the two layers, because doing so is the classic way engineers waste hours blaming the wrong component.
Q: Why does traffic reach my network virtual appliance but never get forwarded?
The most common cause is that IP forwarding is disabled on the appliance’s network interface. Azure will not let an interface forward packets destined for an address other than its own unless IP forwarding is explicitly enabled, a property of the network interface resource rather than of the route table. So the route correctly delivers traffic to the appliance, the appliance receives it, and then the platform refuses to let the appliance pass it onward, producing a dead end that looks like an appliance fault. Confirm it by capturing on the appliance and observing that packets arrive but never leave, then enable IP forwarding on the interface. A secondary cause is the appliance’s own internal configuration failing to forward, so once IP forwarding is on, verify the appliance’s routing or firewall policy actually permits and forwards the traffic you are sending it.
Q: How do BGP-propagated routes affect my user-defined routes?
When a virtual network gateway connects to an on-premises network over VPN or ExpressRoute and route propagation is enabled, the gateway injects the prefixes it learns into your subnets as BGP-sourced routes. These sit between user-defined and system routes in priority, so a BGP route outranks a system route of equal length but loses to an equally specific user-defined route. The practical risk is a surprise: a learned on-premises prefix can overlap a destination you assumed was internet-bound and silently steer it toward the gateway, even though you wrote no route for it. Inspect the effective routes for BGP-sourced rows when traffic goes somewhere unexpected. To reclaim a prefix, add a higher-priority user-defined route, scope the on-premises advertisement, or set disableBgpRoutePropagation on the subnet so it ignores learned routes entirely when that is the intended behavior.
Q: Can a user-defined route break Azure platform services or private endpoints?
Yes, if it is too broad. A 0.0.0.0/0 route to an appliance captures traffic some platform services expect to send over the internet path, which can break them unless the appliance permits that traffic or you add narrow Internet-next-hop exceptions for the relevant platform prefixes. Private endpoints install a highly specific /32 system route that longest-prefix match normally protects, so a broad route does not usually override it, but you must avoid writing a competing /32 and avoid forcing private-endpoint traffic through an appliance that does not expect it. The disciplined pattern steers only the broad traffic and leaves private-endpoint and required platform traffic on their native, most-specific routes. Because the exact platform prefixes change over time, verify them against current official guidance rather than treating any list as a permanent constant.
Q: What does disableBgpRoutePropagation do on a route table association?
The disableBgpRoutePropagation property, set per subnet through the route table association, controls whether BGP routes learned from a virtual network gateway are injected into that subnet’s effective route set. Left at its default of false, learned on-premises prefixes flow in and participate in precedence alongside your user-defined and system routes. Set to true, the subnet ignores those propagated routes entirely, which teams use on perimeter or appliance subnets that must egress only through their own device regardless of what the gateway learns. It is a deliberate, documented choice, because a future engineer encountering a subnet that mysteriously ignores on-premises routes deserves to know the behavior was intentional. Keeping the setting in your infrastructure-as-code, with the reason recorded, prevents it from becoming an unexplained source of either route leakage or route starvation later.
Q: How do I roll back a routing change that took down a subnet’s connectivity?
The fastest safe rollback is removing the route table association from the affected subnet, which restores the platform’s default system routes within seconds and reestablishes direct internet egress and intra-network connectivity, without deleting the route table you built. You can do this with az network vnet subnet update and an empty route-table argument, or by removing the association in your infrastructure-as-code and redeploying. This is faster and less error-prone than trying to surgically delete the offending route under incident pressure, because it returns the subnet to a known-good baseline. Once connectivity is restored and the incident is calm, you diagnose the faulty route against the effective routes, correct it in code, review the change, and reassociate the corrected table. Rehearsing this association-removal rollback before you need it is what turns a broad-route mistake from a long outage into a brief interruption.
Q: Why does my firewall drop return traffic even though the outbound path works?
This is the asymmetry trap. Your outbound traffic correctly traverses a stateful firewall through a user-defined route, but the return traffic takes a different path that bypasses the firewall, so the firewall, having seen only one direction of the conversation, drops the unmatched return packets. It is common when only one side of a peered or hub-spoke relationship carries the steering route to the appliance. Confirm it by comparing the effective routes on both the source and the return subnets and finding that the return side lacks the symmetric route through the appliance. The fix is to make routing symmetric so both directions traverse the same stateful device. Asymmetry is insidious because a single test flow can take a consistent path and pass, then fail under real load when paths diverge, which is why it often surfaces late and costs disproportionate time to find.
Q: Should I create one route table per subnet or share tables across subnets?
Share tables across subnets that follow the same policy. A single route table associated with every subnet that needs the same egress behavior is far easier to reason about and to change, because one edit updates them all and there is exactly one place to read the intent. Creating a unique table per subnet produces dozens of nearly identical tables that drift apart as different engineers edit them, and a drifted table is precisely where silent routing failures hide. Name tables and routes for their purpose rather than their mechanics, so the next engineer understands the intent without reverse-engineering prefixes. Reserve a dedicated table for a subnet only when its routing genuinely differs, such as a perimeter subnet with its own appliance and propagation settings. Managing the shared tables as code, with reviewed changes, keeps the layout consistent and recoverable as the environment grows.
Q: How do service endpoints differ from routing changes for reaching Azure services privately?
Service endpoints and route tables solve adjacent problems differently. A service endpoint does not introduce a private IP or a user-defined route; it tags traffic at the subnet level so an Azure service recognizes it as coming from a trusted subnet and applies network rules accordingly, while the traffic still uses the platform’s optimized path. Because it adds no private address, it interacts with routing far less than a private endpoint does and rarely needs a supporting user-defined route. A private endpoint, by contrast, projects the service into your virtual network as a private IP and installs a specific system route for it. So when you only need a service to trust a subnet, a service endpoint is lightweight and routing-neutral, whereas when you need a private IP reachable across peering or on-premises, a private endpoint with its more specific route is the right tool, and you avoid steering its traffic through an appliance that does not expect it.
Q: How can I make Azure routing failures visible instead of waiting for user reports?
Because routing faults are silent by design, with no data-path log when a packet is dropped or misrouted, you compensate by adding observation the platform does not give you for free. Monitor the health of any appliance that a 0.0.0.0/0 route depends on, since that route makes the appliance a hard dependency for all egress, and alert when it degrades. Watch for unexpected changes to a subnet’s effective routes, which can catch a propagation surprise or an accidental override before it causes an incident. Periodically validate the live next hop for critical source-and-destination pairs using the next-hop diagnostic so a drifted route surfaces proactively. This instrumentation turns the silent class of failures into something you detect and act on, embodying the broader habit of reasoning from the mechanism: once you know routing fails quietly, you build the signal that makes the quiet audible.