Every packet that leaves a Windows machine is matched against the local IP routing table before anything touches the wire. The table decides which interface the packet goes out of and which next hop it is handed to. When traffic to one subnet is slow, blackholed, or leaving through the wrong adapter on a multi-homed server, the routing table is the first place with a real answer.
route is the built-in tool for reading and editing that table. It has been in Windows since NT and has not changed, which is why almost every guide reduces it to two lines: route print to look and route add to change. That is not where people get stuck. They get stuck because the route they added disappeared after a reboot, or because the same route shows up under Persistent Routes but never takes effect.
This page covers the part the reference does not: how to read each column of route print, and why the three tools that add a static route on Windows have three different persistence defaults. If you arrived here from tracert and pathping because hop 1 was not the gateway you expected, the 0.0.0.0 row explained below is the answer.
Applies to: Windows 10 / 11, Windows Server 2016 / 2019 / 2022 / 2025
Quick answer
Reading the table needs no elevation. Changing it does. Open an elevated Command Prompt first: press Start, type cmd, then press Ctrl+Shift+Enter.
rem Confirm the prompt is actually elevated before you try to change anything.
rem S-1-16-12288 is the High Mandatory Level SID, present only in an elevated token.
whoami /groups | findstr /c:"S-1-16-12288"
With that prompt open, this is the whole workflow: look at the table, add a route that survives a reboot, and confirm it landed in both places.
rem 1. Read the current table, including the Persistent Routes section
route print
rem 2. Send 10.60.40.0/24 through the secondary router at 10.10.20.253
rem /p writes the route to the registry so it survives a restart
route /p add 10.60.40.0 mask 255.255.255.0 10.10.20.253
rem 3. Confirm it is live now AND stored for next boot
route print 10.60.*
/p the route is gone at the next restart of the TCP/IP stack. This is the single most common reason a static route “stopped working overnight”.
What route does
The routing table is a list of destination prefixes. For an outbound packet Windows finds the entries that match the destination address, prefers the most specific match, and uses the metric to break ties between equally specific entries. Microsoft’s own wording is that the metric “is used when choosing among multiple routes in the routing table that most closely match the destination address”, and that the route with the lowest metric wins.
The full syntax, verbatim from the Windows Commands reference:
route [/f] [/p] [<command> [<destination>] [mask <netmask>] [<gateway>] [metric <metric>]] [if <interface>]
| Parameter | What it does |
|---|---|
print | Prints the routing table. Accepts a wildcard destination to filter the output. |
add | Adds a route. |
change | Modifies an existing route. |
delete | Deletes a route or routes. |
/p | With add, writes the route to the registry so it is restored whenever TCP/IP starts. With print, displays the persistent route list. Ignored by every other command. |
/f | Clears the table of everything that is not a host route, the loopback route, or a multicast route. Runs before the command it is combined with. |
mask <netmask> | Subnet mask of the destination. Defaults to 255.255.255.255, a host route, if omitted. |
<gateway> | Next hop address. Must be directly reachable from this machine. |
metric <metric> | Cost from 1 to 9999. Lower wins. |
if <interface> | Interface index to use. Decimal, or hexadecimal prefixed with 0x. Omit it and Windows derives the interface from the gateway address. |
/p and /f. This article uses that form throughout so every example matches the documented syntax.
Reading route print output
The examples below all run on SRV-APP-01, a server with one NIC at 10.10.20.31/24, a default gateway of 10.10.20.254, and a second router at 10.10.20.253 that fronts the backup network 10.60.40.0/24. This is the IPv4 half of route print on that host.
IPv4 Route Table
===========================================================================
Active Routes:
Network Destination Netmask Gateway Interface Metric
0.0.0.0 0.0.0.0 10.10.20.254 10.10.20.31 26
10.10.20.0 255.255.255.0 On-link 10.10.20.31 281
10.10.20.31 255.255.255.255 On-link 10.10.20.31 281
10.10.20.255 255.255.255.255 On-link 10.10.20.31 281
127.0.0.0 255.0.0.0 On-link 127.0.0.1 331
224.0.0.0 240.0.0.0 On-link 10.10.20.31 281
255.255.255.255 255.255.255.255 On-link 10.10.20.31 281
===========================================================================
Persistent Routes:
None
Five columns, and two of them are routinely misread.
| Column | What it actually holds |
|---|---|
| Network Destination | The destination network or host. 0.0.0.0 with a 0.0.0.0 netmask is the default route, the entry used when nothing more specific matches. |
| Netmask | How much of the destination has to match. 255.255.255.255 is a single host. |
| Gateway | The next hop. On-link means there is no next hop: the destination is directly attached and the packet is put on the wire as-is. |
| Interface | The local IP address of the outgoing adapter, not the adapter name. On a multi-homed host this column is how you tell which NIC a route uses. |
| Metric | Tie-breaker between equally specific routes. Lower wins. |
The Metric column is a single combined number, which is why the values look arbitrary. Windows adds the metric of the route itself to the metric of the interface it uses. In the table above the interface metric is 25: the default route reads 26, which is a route metric of 1 plus 25, and every on-link row reads 281, which is the default on-link route metric of 256 plus the same 25. The loopback rows read 331 because the loopback interface carries a metric of 75. Get-NetRoute exposes the two halves separately as RouteMetric and InterfaceMetric.
Two entries carry most of the diagnostic value. The 0.0.0.0 row names the gateway that everything off-subnet is handed to, which is the value tracert shows as hop 1. The On-link row for your own subnet, 10.10.20.0 here, confirms which adapter owns that subnet.
To pull just the default route out of the table, anchor the match at the start of the line. A plain substring search does not work, and it is worth seeing why.
rem WRONG: "0.0.0.0" is also a substring of the 240.0.0.0 multicast netmask,
rem so this returns the multicast rows as well as the default route
route print | findstr "0.0.0.0"
rem RIGHT: /r switches findstr to regex, ^ anchors at the start of the line,
rem and the escaped dots stop . from matching any character
route print | findstr /r /c:"^ *0\.0\.0\.0 "
Practical examples
1. Test a route before you commit to it
The problem: Backup traffic from SRV-APP-01 to 10.60.40.0/24 is going out of the default gateway and crossing the firewall, when it should take the direct router at 10.10.20.253.
The solution: Add the route without /p first. A non-persistent route is a free trial: if it breaks something, a reboot undoes it.
rem No /p, so this route exists only until TCP/IP restarts.
rem That is the point: it is reversible without touching the registry.
route add 10.60.40.0 mask 255.255.255.0 10.10.20.253
rem Confirm the path changed. Hop 1 should now be 10.10.20.253, not 10.10.20.254.
tracert -d 10.60.40.15
If the path is wrong, remove it and you are back where you started.
rem delete does not need the gateway, only the destination and mask
route delete 10.60.40.0 mask 255.255.255.0
2. Make the route survive a reboot
The problem: The route works, but it has to still be there after the monthly patch reboot.
The solution: Re-add it with /p, which writes it to HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\PersistentRoutes and replays it whenever TCP/IP starts.
rem /p goes before the command word, exactly as in Microsoft's own example.
rem metric 1 is set explicitly so the stored value is predictable rather than
rem whatever route.exe picks for you.
route /p add 10.60.40.0 mask 255.255.255.0 10.10.20.253 metric 1
rem The wildcard filters the printout to routes beginning with 10.60
route print 10.60.*
A persistent route appears twice, once in each section. That double listing is the confirmation you want.
Active Routes:
Network Destination Netmask Gateway Interface Metric
10.60.40.0 255.255.255.0 10.10.20.253 10.10.20.31 26
===========================================================================
Persistent Routes:
Network Address Netmask Gateway Address Metric
10.60.40.0 255.255.255.0 10.10.20.253 1
3. Change a metric instead of deleting and re-adding
The problem: Two paths reach 10.60.40.0/24 and the wrong one is preferred, so the route needs a different metric rather than a different gateway.
The solution: route change edits the entry in place. Deleting and re-adding drops traffic for the moment in between, which matters on a link an application is actively using.
rem Lower metric = preferred. 1 to 9999 is the accepted range.
route change 10.60.40.0 mask 255.255.255.0 10.10.20.253 metric 5
rem Pin the route to a specific adapter by interface index when the
rem gateway is reachable from more than one NIC. Get indexes from route print.
route change 10.60.40.0 mask 255.255.255.0 10.10.20.253 if 8
route change does not accept /p. Changing a route that was added with /p updates the live table only; the registry copy keeps the old values and comes back at the next boot. Delete the persistent route and re-add it with /p when the change needs to stick.
4. Clean up a stale persistent route
The problem: A decommissioned router is still listed under Persistent Routes and the entry keeps reappearing after every reboot.
The solution: Delete it by destination and mask. The delete removes the entry from both the live table and the registry.
rem Always print first: the wildcard shows exactly what the delete will hit
route print 10.60.*
route delete 10.60.40.0 mask 255.255.255.0
rem Verify: neither section should list 10.60.40.0 any more
route print 10.60.*
route delete 10.* is valid syntax and deletes every route beginning with 10, including the on-link route for your own subnet. On a remotely managed server that ends the session. Print the wildcard before you delete it, every time.
The lab host is now back to its original table. The next section adds the same 10.60.40.0/24 route again with two other tools, so run those on the clean table rather than on top of the route you just created.
Three tools, three persistence defaults
Windows offers three supported ways to add a static route, and each one defaults differently. This is the detail that produces routes which vanish on reboot and routes which refuse to go away.
| Tool | Default persistence | How to get the other behaviour |
|---|---|---|
route add | Active only, lost when TCP/IP restarts | Add /p to write it to the registry |
netsh interface ipv4 add route | Persistent, per the documented default of store=persistent | Pass store=active for a change that lasts until next boot |
New-NetRoute | Both stores, so it survives a reboot | Pass -PolicyStore ActiveStore to create it in the active store only |
The same route, expressed three ways. All three take a prefix, an interface and a next hop; only the notation differs.
rem CMD: mask notation, explicitly persistent
route /p add 10.60.40.0 mask 255.255.255.0 10.10.20.253
rem netsh: CIDR notation, interface by name, already persistent by default
netsh interface ipv4 add route prefix=10.60.40.0/24 interface="Ethernet" nexthop=10.10.20.253
rem netsh needs the interface name or index. This lists both.
netsh interface ipv4 show interfaces
# PowerShell: CIDR notation, saved to both stores unless you say otherwise
New-NetRoute -DestinationPrefix "10.60.40.0/24" -InterfaceAlias "Ethernet" -NextHop 10.10.20.253
# Same route, but deliberately temporary
New-NetRoute -DestinationPrefix "10.60.40.0/24" -InterfaceAlias "Ethernet" -NextHop 10.10.20.253 -PolicyStore ActiveStore
netsh interface ipv4 delete route removes the entry from both stores by default, which is usually what you want. Add store=active if you only mean to drop it until the next reboot.
Hidden gems
Persistent Routes never tells you the interface
The Persistent Routes section has four columns and none of them is an interface. Microsoft documents this as by design: route.exe reads the persistent store, and that store does not record which adapter a route belonged to. On a host where a NIC was later added to a NIC team, two default gateways can sit in that section at once, only one of which is in the active table. This is Microsoft’s own sample of exactly that state.
===========================================================================
Persistent Routes:
Network Address Netmask Gateway Address Metric
0.0.0.0 0.0.0.0 10.0.0.1 Default
0.0.0.0 0.0.0.0 192.168.0.1 Default
===========================================================================
Microsoft’s recommendation for this case is to stop using route.exe and query the two stores separately, because Get-NetRoute does report the interface.
# What is stored for next boot, with the owning interface
Get-NetRoute -AddressFamily IPv4 -PolicyStore PersistentStore
# What the stack is using right now
Get-NetRoute -AddressFamily IPv4 -PolicyStore ActiveStore
On-link and 0.0.0.0 are the same thing
A route with no next hop shows as On-link in route print and as a NextHop of 0.0.0.0 in Get-NetRoute. Microsoft documents 0.0.0.0 for IPv4, and :: for IPv6, as the value meaning a local subnet route. Knowing they are the same value stops a lot of confusion when you move between the two tools mid-investigation.
Metrics in the hundreds are not a misconfiguration
Metrics like 281 and 331 look arbitrary next to the small values you would set by hand. They are automatic. Windows measures interface speed and adjusts route metrics so the fastest interface produces the lowest-metric routes. Microsoft notes that the large values can be removed by disabling automatic interface metric determination in the advanced TCP/IP properties of each LAN connection. On a healthy single-NIC server, leave them alone.
Stop guessing which route wins
Reading the table and applying longest-prefix match by eye is slow and error-prone on a host with a VPN client attached. Find-NetRoute asks the stack directly and returns both the source address it would use and the route it selected.
# Returns two objects: the local NetIPAddress, then the NetRoute actually selected
Find-NetRoute -RemoteIPAddress "10.60.40.15"
# The next hop for that destination, on its own
Find-NetRoute -RemoteIPAddress "10.60.40.15" |
Select-Object -ExpandProperty NextHop
PowerShell equivalents
The NetTCPIP module covers everything route.exe does and returns objects instead of text, which is what you want in a script.
| Task | CMD | PowerShell |
|---|---|---|
| Show the table | route print | Get-NetRoute |
| Show the default gateway | route print filtered with findstr | Get-NetRoute -DestinationPrefix "0.0.0.0/0" |
| Add a route | route /p add ... | New-NetRoute |
| Delete a route | route delete ... | Remove-NetRoute |
| Which route will be used | read the table by eye | Find-NetRoute -RemoteIPAddress |
A readable view of the active IPv4 table, sorted so the entries that decide off-subnet traffic come first:
# RouteMetric on its own is not what route print shows. Adding InterfaceMetric
# to it reproduces the single Metric column, so sort on the sum to get the same
# ordering you would read off route print.
Get-NetRoute -AddressFamily IPv4 |
Select-Object DestinationPrefix, NextHop, InterfaceAlias,
@{Name='Metric'; Expression={$_.RouteMetric + $_.InterfaceMetric}} |
Sort-Object Metric |
Format-Table -AutoSize
If you have to stay in CMD but want structured output, route print can be parsed. Anchor on the shape of a row rather than on line numbers, which shift as soon as an adapter is added.
# Five fields: destination, netmask, gateway or On-link, interface IP, metric.
# Persistent Routes rows have four fields and no interface IP, so they do not
# match. IPv6 rows and the Interface List do not match either.
$row = '^\s{2,}(\d{1,3}(?:\.\d{1,3}){3})\s+(\d{1,3}(?:\.\d{1,3}){3})\s+(On-link|\d{1,3}(?:\.\d{1,3}){3})\s+(\d{1,3}(?:\.\d{1,3}){3})\s+(\d+)\s*$'
route print | Select-String -Pattern $row | ForEach-Object {
$g = $_.Matches[0].Groups
[pscustomobject]@{
Destination = $g[1].Value
Netmask = $g[2].Value
Gateway = $g[3].Value
Interface = $g[4].Value
Metric = [int]$g[5].Value
}
} | Sort-Object Metric | Format-Table -AutoSize
Where this matters
- Backup traffic on the wrong link: a single persistent route pushes nightly backups onto the storage network instead of through the firewall.
- Multi-homed servers: the Interface column is the only place that tells you which NIC a subnet is actually reached through.
- VPN clients that break internal access: a client that installs a 0.0.0.0/0 route captures everything, and the routing table is where you see it happen.
- Migrated or re-teamed adapters: old default gateways linger under Persistent Routes long after the NIC configuration changed.
- Lab and DMZ hosts with no default gateway: targeted static routes give access to exactly the management subnet and nothing else.
- Build automation: a route added by an install script without
/pworks until the first reboot, then fails in a way nobody connects back to the install.
Tips and limitations
route printruns unelevated.add,change,deleteand/fall need an elevated prompt.- The gateway must be directly reachable. A next hop on a subnet this host cannot reach produces a stored route that never becomes active.
- An unsupported destination and netmask combination returns “Route: bad gateway address netmask”. That message means host bits are set in the destination, not that the gateway is wrong.
- Omitting
maskcreates a host route with a 255.255.255.255 netmask, not a network route. This is a common typo with a silent effect. - Wildcards work for
printanddeleteonly. An asterisk matches any string and a question mark matches a single character. route /fflushes the table immediately, including the default route. Do not run it on a remote session.- Use
-WhatIfwithRemove-NetRouteto see which entries a filter matches before anything is removed. - Routes added by a VPN or virtualisation client are owned by that client. Deleting them by hand usually lasts until the client reconnects.
Official documentation
- route: Windows Commands | Microsoft Learn
- netsh interface: Windows Commands | Microsoft Learn
- Get-NetRoute: NetTCPIP | Microsoft Learn
- New-NetRoute: NetTCPIP | Microsoft Learn
- Find-NetRoute: NetTCPIP | Microsoft Learn
- Additional gateways appear in persistent routes | Microsoft Learn
Related tools
- IP Subnet Calculator: converts a CIDR prefix into the dotted netmask that
route addexpects, and back again for netsh. - Network Diagnostics Tool: confirms a destination is reachable before you decide the routing table is at fault.
- IP Info: shows the public address your traffic actually exits with, which is the outside view of your default route.
Related guides
- tracert and pathping on Windows: reads the path a packet takes once the routing table has chosen the first hop.
- netstat command in Windows:
netstat -rprints the same routing table without leaving the connection view. - netsh command in Windows: the wider netsh context that owns
interface ipv4 add route. - Test-NetConnection in PowerShell: confirms a route works end to end, on the port that matters.
- ipconfig command in Windows: gives the interface addresses that appear in the Interface column of
route print. - IP subnetting cheat sheet: the mask and prefix arithmetic behind every route you add.