Skip to main content

Routing in a VPC - Route Tables, IGW and NAT

TL;DR
  • A subnet behaves the way its route table says, not the way its CIDR looks. The table is attached to the subnet, and every packet leaving an instance in it is matched against that table.
  • Longest prefix wins. A /16 local route beats a /0 default route for in-VPC traffic, always, because it is more specific. Static versus propagated only breaks ties between routes of the same prefix.
  • Public subnet means its table has 0.0.0.0/0 pointing at an internet gateway. A public IP on an instance does nothing without that route. Private subnet means no such route; its default route is a NAT gateway or nothing.
  • The internet gateway does a 1:1 NAT between an instance's private and public IPv4 address. The operating system never sees the public address.
  • New subnets silently use the main route table unless you associate another one. That is a classic outage source.

Mental model​

ANALOGYEveryday

A big office building has a floor directory in the lobby. It does not care who you are or what your name badge looks like. It only says "for floors 1 to 20, take the lift; for anything else, go out the front door". Each floor of tenants has its own directory card, and the card is what decides whether a visitor from that floor can leave the building at all. A floor whose card has no "go out the front door" line is a floor nobody can walk out of, however many street addresses the tenants print on their business cards.

Where it breaks: a building directory is read by a person once. A route table is read for every packet, and the most specific line wins even when a broader line also fits.

ANALOGYFor engineers

It is ip route on a host, except the table is attached to the subnet instead of living on each machine, and the cloud's virtual router consults it for you. Each row is destination prefix -> target, the same shape as the forwarding table in forwarding and longest-prefix match. The host itself still follows its own simple rule from the host forwarding decision: on-link goes direct, everything else goes to the default gateway. In the cloud that gateway is the subnet's implicit router at the base address plus 1, and the subnet's route table is what the router does next.

Where it breaks: on a Linux host you can add any route you like, including one more specific than the connected subnet. In AWS the local route is protected, and what you may place more specific than it is tightly limited (see the deep dive). In Azure and GCP the rules differ again.

How it works​

A VPC (the cloud network model page covers VPCs and subnets) is a set of subnets plus routing. Each subnet is associated with exactly one route table. A route table is an ordered-by-specificity list of destination CIDR -> target rows. The first row in every table is the local route: the VPC's own CIDR, target local. AWS creates it automatically, you cannot delete it, and it is why instances in different subnets of one VPC can reach each other without any configuration (AWS route table concepts).

Targets are the things a packet can be handed to:

TargetUsed forCloud names
localTraffic to any address inside the VPC CIDRAWS: local. GCP: subnet routes (generated per subnet). Azure: system route, next hop type Virtual network.
Internet gatewayBidirectional internet for instances with a public IPv4 address, or any IPv6 addressAWS: igw-. GCP: next hop default-internet-gateway. Azure: next hop type Internet.
NAT gatewayOutbound-only internet for private instances (see the NAT gateways page)AWS: nat- as a route target. GCP: Cloud NAT is configured on a Cloud Router, not a route target. Azure: NAT Gateway attached to the subnet.
Egress-only internet gatewayOutbound-only IPv6 internetAWS: eigw-. GCP and Azure: no separate object; firewall rules and NSGs do this job.
PeeringA second VPC, one hop, non-transitiveAWS: pcx-. GCP: VPC Network Peering (routes exchanged automatically). Azure: VNet peering.
Transit gatewayHub for many VPCs and on-premises linksAWS: tgw-. GCP: Network Connectivity Center. Azure: Virtual WAN or a hub VNet.
VPN or virtual private gatewayEncrypted tunnel to a data centreAWS: vgw- or tgw-. GCP: next hop VPN tunnel. Azure: next hop type Virtual network gateway.
Network interface or applianceSend traffic through a firewall or router instanceAWS: eni-. GCP: next hop instance. Azure: next hop type Virtual appliance.
Gateway endpointPrivate path to S3 or DynamoDB, added as a prefix-list routeAWS: vpce- gateway endpoint. GCP: Private Google Access. Azure: service endpoint. See private connectivity.

A route target is never another subnet. Subnet-to-subnet traffic inside one VPC is covered by the local route, so there is nothing to point at.

The worked example​

One VPC 10.0.0.0/16, three subnets, three route tables. All numbers here are chosen for the example; 203.0.113.10 is from the documentation range reserved by RFC 5737, so it stands in for a real Elastic IP.

SubnetCIDRRoute table rowsReachable from the internet?
public-a10.0.1.0/2410.0.0.0/16 local; 0.0.0.0/0 igw-1Yes, if the instance has a public IP and the security group allows it
private-a10.0.11.0/2410.0.0.0/16 local; 0.0.0.0/0 nat-1No. Outbound only, through the NAT gateway in public-a
data10.0.21.0/2410.0.0.0/16 localNo, and no outbound either. This is an isolated subnet
Internet gateway igw-1NAT GW nat-aNAT GW nat-bNAT GW nat-capp instancesapp instancesapp instancesVPC 10.0.0.0/16AZ aAZ bAZ cpublic-a 10.0.1.0/24public-b 10.0.2.0/24public-c 10.0.3.0/24local, 0.0.0.0/0 igw-1local, 0.0.0.0/0 igw-1local, 0.0.0.0/0 igw-1private-a 10.0.11.0/24private-b 10.0.12.0/24private-c 10.0.13.0/24local, 0.0.0.0/0 nat-alocal, 0.0.0.0/0 nat-blocal, 0.0.0.0/0 nat-c
Each AZ has its own public and private subnet. Public subnets send 0.0.0.0/0 to the one internet gateway; each private subnet sends 0.0.0.0/0 to the NAT gateway sitting in its own AZ's public subnet.

Try it on this example. Pick a source and a destination, step the packet, then break something: delete the NAT gateway, remove a route, or move a subnet to the main table, and read the verdict.

current hopwinner (longest prefix)skipped (shorter prefix)blackhole (target deleted)no match
▶ InstanceVPCrouterNATgateway 10.0.1.200InternetgatewayDestination
source 10.0.11.5:51514destination 93.184.216.34:443→ sent
public-a table
10.0.0.0/16local (cannot be removed)
0.0.0.0/0
::/0
private-a table
10.0.0.0/16local (cannot be removed)
0.0.0.0/0
::/0
data table
10.0.0.0/16local (cannot be removed)
main table
10.0.0.0/16local (cannot be removed)
Rows with ::/0 are the IPv6 default route. The local route for the VPC CIDR is always present. S3 CIDR shown here is illustrative; the real prefix list has many.
Instance in private-a sends. The instance's operating system only knows its private address 10.0.11.5.
step 0 / 6

Longest prefix match, applied to cloud routes​

When two rows both contain the destination, the router uses the one with the longer prefix. The theory and the binary worked example live in forwarding and longest-prefix match; here is the cloud version. AWS states the rule directly: the most specific route that matches the traffic determines the target (route priority). Take a private-a table with one extra row, a peering connection to a second VPC at 10.8.0.0/16:

DestinationMatches 10.0.11.5?Matches 93.184.216.34?Target
10.0.0.0/16Yes, 16 bitsNolocal
10.8.0.0/16No, first difference at bit 13Nopcx-1
0.0.0.0/0Yes, 0 bitsYes, 0 bitsnat-1

For 10.0.11.5 two rows match and the 16-bit local row beats the 0-bit default. For 93.184.216.34 only the default matches, so it goes to the NAT gateway. (Both matches were checked with Python's ipaddress module; 10.0.11.5 and 10.8.0.0 share exactly 12 leading bits.)

dest 10.0.11.510.0.0.0/1610.8.0.0/160.0.0.0/016 bits: wins (local)stops at bit 130 bits: loses100115octet boundaries at bit 8, 16, 24 (dashed); one cell is 15 px = 1 bit
Each bar fills one cell per prefix bit that must match. The destination 10.0.11.5 satisfies the first 16 bits of 10.0.0.0/16 and the first 0 bits of 0.0.0.0/0. The peering route diverges after 12 bits. The longest satisfied prefix wins.

Deep dive​

Main versus custom route tables​

Every VPC has a main route table. A subnet with no explicit association uses it, and that includes subnets you create later (AWS route table concepts). If someone adds 0.0.0.0/0 igw-1 to the main table, every unassociated subnet, present and future, becomes public. The safe pattern is to keep the main table minimal (local only), give each tier its own custom table, and associate explicitly.

Ties: static versus propagated​

Longest prefix is decided first. Only when two routes have the same prefix does anything else matter. In AWS, a route table can receive propagated routes from a virtual private gateway (VPN or Direct Connect BGP). When a static route and a propagated route have the same destination, AWS prefers the static one, then prefix-list routes, then propagated routes in the order Direct Connect BGP, VPN static, VPN BGP (route priority). Azure orders same-prefix routes as user-defined first, then BGP, then system (Azure routing overview). GCP picks the longest prefix and then the lowest priority number, where a smaller number wins (GCP routes).

Can you hijack in-VPC traffic with a more specific route?​

The local route cannot be deleted. Historically AWS did not allow any route more specific than the local route. AWS now lets you add one, with a Gateway Load Balancer endpoint, NAT gateway, Network Firewall endpoint or network interface as the target, and the destination must match the entire CIDR block of a subnet in the VPC; that is how you steer east-west traffic through an inspection appliance (AWS example routing options). You can also replace the target of the local route itself (AWS docs). GCP and Azure each have their own rules about overlap with the generated subnet or VNet routes; read their routing pages before designing around this.

GCP and Azure differ in shape​

  • GCP routes belong to the whole VPC network, not to a subnet. A system-generated default route has destination 0.0.0.0/0 and next hop default-internet-gateway, and each subnet gets a subnet route. Static routes carry a priority from 0 to 65,535, and a lower number wins (GCP static routes). Custom static routes can be limited to instances by network tag. Egress NAT is the Cloud NAT service, configured on a Cloud Router and applied to instances that have no external IP; it is not a next hop (GCP routes). The GCP-product view, including firewall rules and common designs, lives in the gcp-ace VPC page and the common network designs page.
  • Azure ships with system routes for the VNet address space, for the internet (0.0.0.0/0 with next hop type Internet), and for private ranges with next hop type None. A user-defined route (UDR) with the same prefix overrides a system route (Azure routing overview).

Blackholes and asymmetric paths​

If you delete a NAT gateway or an ENI that a route targets, the route stays and its state flips to blackhole. Packets matching it are dropped, silently from the sender's point of view. Always read the State column when debugging.

Asymmetric routing appears when a packet goes out via one path and the reply returns via another. Stateful appliances, such as a firewall in an inspection VPC, then see only half of the conversation and drop it. With AWS Transit Gateway, the fix is appliance mode on the VPC attachment, which keeps both directions of a flow on the same appliance in the same AZ (TGW appliance scenario).

How a public IP works​

An instance with a public IPv4 address does not have that address on its network interface. The internet gateway performs a one-to-one NAT between the private and public address: outbound, it rewrites the source from private to public; inbound, it rewrites the destination from public to private (AWS internet gateway). The operating system sees only the private address. IPv6 is different: the address is already global, so there is no translation, and an egress-only internet gateway gives IPv6 instances outbound-only access, the IPv6 counterpart of a NAT gateway (AWS egress-only IGW).

Walking one packet out and back​

Private instance 10.0.11.5 opens a connection to 93.184.216.34:443. The port numbers are illustrative.

App instanceNAT gatewayInternet gatewayServer12345610.0.11.510.0.1.201:1 NAT93.184.216.34stepsourcedestinationwhat happened12345610.0.11.5:4031210.0.1.20:1024203.0.113.10:102493.184.216.34:44393.184.216.34:44393.184.216.34:44393.184.216.34:44393.184.216.34:44393.184.216.34:443203.0.113.10:102410.0.1.20:102410.0.11.5:40312private-a table: 0.0.0.0/0 to nat-1NAT rewrote src; public-a table: 0.0.0.0/0 to igw-1IGW rewrote src to the NAT gateway's Elastic IPreply is addressed to the Elastic IPIGW rewrote dst back to the private addressNAT matched its table and restored the original dst
Source and destination at every hop. The NAT gateway rewrites the source to its own private address, then the internet gateway rewrites that to the NAT gateway's Elastic IP. Replies reverse both rewrites.

The mechanics of the rewrite and the translation table are in NAT. Which hop pays for what, and how many NAT gateways to run, is in NAT gateways and egress.

Textbook vs real world​

The textbook saysProduction doesWhy
Each host has a default gateway, a real router with an interface on the subnetThe gateway is an implicit virtual router at the subnet base address plus 1; there is no box to log intoThe cloud implements routing in the virtualisation layer, so the router scales with the fleet
NAT happens at the edge routerThe internet gateway is a managed, distributed service, not a device; the NAT gateway is a separate managed service in a specific subnetBoth are horizontally scaled by the provider; you attach them with routes
An IP address identifies a host and lives on its interfaceA public IPv4 address is never on the instance's interface; the gateway translates it 1:1Public IPv4 is scarce and mapped by the provider at the edge
The routing table is per host or per routerThe route table is an object you attach to a subnet, shared by every instance in itOne policy per tier is easier to review than per-host tables

In production​

  • The common shape is a 3-tier layout (public, private application, isolated data) across three availability zones, with one NAT gateway per AZ. AWS documents the reasoning: a NAT gateway is zonal, so if its AZ fails, instances routing through it elsewhere lose egress, and cross-AZ traffic can add data-transfer charges (NAT gateway docs). Prices and quotas change; check the VPC pricing page (as of 2026-10).
  • Inspection VPCs place firewall appliances behind a Gateway Load Balancer and use Transit Gateway routes to steer traffic through them; see the cloud load balancers coverage and the AWS multi-VPC network whitepaper.
  • Isolated subnets (local route only) hold databases so a misconfigured security group cannot expose them to the internet, because there is no path to it.

History and evolution​

HISTORY
  • 2006 EC2 launches with a flat network shared by all customers (later called EC2-Classic). Every instance gets a private and a public address, and customers have no control over subnets or routing.
  • 2009 AWS announces VPC so customers can define their own address space, subnets and routing, with VPN back to a data centre. What broke: customers wanted private networks that look like their own.
  • 2013 New accounts get a default VPC in each region, so beginners no longer have to build a network before launching an instance. EC2-Classic is retired for existing users in 2022.
  • 2015 Managed NAT gateway launches, ending the era of self-managed NAT instances that were a single point of failure.
  • 2018 Transit Gateway announced at re:Invent: one hub with route tables instead of a mesh of peering connections.
  • Now Everything is route-table-driven across the three big clouds, with IPv6-first designs using egress-only gateways. AWS, GCP and Azure each document their routing model, linked above.
  • Next Application-layer connectivity services such as VPC Lattice reduce how much you hand-route, though the route table remains the foundation.

Hands-on​

The shape of each output below is real; IDs and addresses are placeholders, and the CLI output varies by tool version.

aws ec2 describe-route-tables \
--filters Name=association.subnet-id,Values=subnet-0abc \
--query 'RouteTables[].Routes[].[DestinationCidrBlock,GatewayId,NatGatewayId,State]' \
--output table
-----------------------------------------------------
| DescribeRouteTables |
+---------------+-------------+-------------+--------+
| 10.0.0.0/16 | local | None | active |
| 0.0.0.0/0 | None | nat-0123 | active |
+---------------+-------------+-------------+--------+

Look at State: blackhole means the target no longer exists. A subnet with no association row is on the main table.

gcloud compute routes list --filter="network=default"
az network route-table route list --resource-group rg --route-table-name rt-private -o table

On GCP, look for a default-route-... row with next hop default-internet-gateway. On Azure, user-defined routes are listed here; system routes are visible through the effective routes view on a network interface.

From inside an instance, the host's view and the internet's view differ:

ip route
ip addr show
curl -s https://ifconfig.me
default via 10.0.11.1 dev ens5
10.0.11.0/24 dev ens5 proto kernel scope link src 10.0.11.5
inet 10.0.11.5/24 brd 10.0.11.255 scope global ens5
198.51.100.7

The default gateway 10.0.11.1 is the implicit router. The interface carries only the private address, while ifconfig.me reports the translated address. On macOS use netstat -rn in place of ip route.

Gotchas​

GOTCHAS4
A route target can be another subnet
The main route table is a safe default
A public IP on an instance makes it reachable
Deleting a NAT gateway leaves a harmless route

Interview questions​

INTERVIEWWhat makes a subnet public?junior
Show answer

A subnet is public when its route table sends 0.0.0.0/0 to an internet gateway. Instances in it also need a public or Elastic IPv4 address, plus security group and network ACL rules that allow the traffic. The address range alone tells you nothing.

Follow-ups:

  • Can you tell from the CIDR? No. 10.0.1.0/24 can be public or private depending only on its table.
INTERVIEWAn instance has a public IP but cannot reach the internet. What do you check?mid
Show answer

Work from the instance outward. Is the OS firewall allowing it, does the security group permit egress, and does the network ACL allow both the outbound packet and the return on ephemeral ports? Then check the subnet's route table: is there a 0.0.0.0/0 route to an attached internet gateway, and is the subnet actually associated with that table or silently using the main one?

Follow-ups:

INTERVIEWTwo routes match a destination. Which one wins?mid
Show answer

The longest prefix, the most specific route. Only if two routes have the same prefix does the tie-break apply, and then the cloud decides: AWS prefers static over propagated, GCP prefers the lower priority number, and Azure prefers user-defined over BGP over system.

Follow-ups:

  • Local versus 0.0.0.0/0? Local is longer, so it wins for in-VPC traffic.
INTERVIEWHow does an instance with a public IP send a packet to the internet?mid
Show answer

It sends it with its private source address to the subnet's router. The route table sends 0.0.0.0/0 to the internet gateway, which rewrites the private source to the public address, one-to-one. Replies are rewritten back the other way.

Follow-ups:

  • Does the instance see its public IP in ip addr? No. The interface only carries the private address.
INTERVIEWA private instance must reach the internet but must not be reachable from it. Design it.senior
Show answer

Put a NAT gateway in a public subnet, with an Elastic IP, and point the private subnet's 0.0.0.0/0 at it. The private subnet gets no internet gateway route, so unsolicited inbound traffic has no path. Run one NAT gateway per AZ so an AZ failure does not take out egress for other zones and cross-AZ traffic is avoided.

Follow-ups:

  • What about cost? NAT gateways bill by the hour and per GB processed; check the pricing page (as of 2026-10).
  • And IPv6? Use an egress-only internet gateway; no NAT is needed because addresses are already global.
INTERVIEWWhy does an inspection VPC drop traffic with asymmetric routing?senior
Show answer

A stateful firewall must see both directions of a flow. If the request crosses appliance A and the reply returns through appliance B, B has no state for it and drops it.

Follow-ups:

  • What fixes it on Transit Gateway? Enable appliance mode on the VPC attachment so both directions use the same appliance.
INTERVIEWWalk a packet from a private instance to the internet and back, with addresses at each hop.senior
Show answer

The instance sends 10.0.11.5 to 93.184.216.34. Its subnet table matches 0.0.0.0/0 to the NAT gateway, which rewrites the source to its own private address. The public subnet's table sends it to the internet gateway, which rewrites that to the Elastic IP. The reply comes to the Elastic IP, the gateway rewrites to the NAT gateway's private address, and the NAT gateway restores the original destination.

Follow-ups:

  • Where is each rewrite recorded? The NAT gateway keeps its own translation table; the internet gateway holds a fixed private to public mapping.

Fun facts​

FACTSTrivia worth knowing
  • AWS describes the internet gateway as horizontally scaled, redundant and highly available, with no bandwidth constraints on your traffic (docs).
  • In an AWS subnet the VPC router lives at the base address plus 1; the first four addresses and the last are reserved (subnet sizing).
  • 203.0.113.0/24 in this page is a documentation-only range (RFC 5737), safe to print in any tutorial.

Cheat sheet​

ConceptAWSGCPAzureGotcha
Route tableRoute table, associated per subnetRoutes on the VPC network, selected by tag and priorityRoute table (UDR) associated per subnetAWS: unassociated subnets use the main table
Implicit local routingLocal routeSubnet routesSystem route, next hop Virtual networkCannot be deleted in AWS
Internet pathInternet gatewayDefault route, next hop default-internet-gatewaySystem route, next hop InternetPublic IP alone is not enough
Outbound-only NATNAT gateway, route targetCloud NAT on a Cloud Router, not a route targetNAT Gateway attached to the subnetDeleting it leaves a blackhole route in AWS
Tie-break, same prefixStatic over propagatedLower priority numberUDR over BGP over systemPrefix length is checked first