Routing in a VPC - Route Tables, IGW and NAT
- 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
/16local route beats a/0default 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/0pointing 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
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.
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:
| Target | Used for | Cloud names |
|---|---|---|
| local | Traffic to any address inside the VPC CIDR | AWS: local. GCP: subnet routes (generated per subnet). Azure: system route, next hop type Virtual network. |
| Internet gateway | Bidirectional internet for instances with a public IPv4 address, or any IPv6 address | AWS: igw-. GCP: next hop default-internet-gateway. Azure: next hop type Internet. |
| NAT gateway | Outbound-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 gateway | Outbound-only IPv6 internet | AWS: eigw-. GCP and Azure: no separate object; firewall rules and NSGs do this job. |
| Peering | A second VPC, one hop, non-transitive | AWS: pcx-. GCP: VPC Network Peering (routes exchanged automatically). Azure: VNet peering. |
| Transit gateway | Hub for many VPCs and on-premises links | AWS: tgw-. GCP: Network Connectivity Center. Azure: Virtual WAN or a hub VNet. |
| VPN or virtual private gateway | Encrypted tunnel to a data centre | AWS: vgw- or tgw-. GCP: next hop VPN tunnel. Azure: next hop type Virtual network gateway. |
| Network interface or appliance | Send traffic through a firewall or router instance | AWS: eni-. GCP: next hop instance. Azure: next hop type Virtual appliance. |
| Gateway endpoint | Private path to S3 or DynamoDB, added as a prefix-list route | AWS: 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.
| Subnet | CIDR | Route table rows | Reachable from the internet? |
|---|---|---|---|
| public-a | 10.0.1.0/24 | 10.0.0.0/16 local; 0.0.0.0/0 igw-1 | Yes, if the instance has a public IP and the security group allows it |
| private-a | 10.0.11.0/24 | 10.0.0.0/16 local; 0.0.0.0/0 nat-1 | No. Outbound only, through the NAT gateway in public-a |
| data | 10.0.21.0/24 | 10.0.0.0/16 local | No, and no outbound either. This is an isolated 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.
10.0.11.5:51514destination 93.184.216.34:443→ sent10.0.0.0/16local (cannot be removed)0.0.0.0/0::/010.0.0.0/16local (cannot be removed)0.0.0.0/0::/010.0.0.0/16local (cannot be removed)10.0.0.0/16local (cannot be removed)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:
| Destination | Matches 10.0.11.5? | Matches 93.184.216.34? | Target |
|---|---|---|---|
| 10.0.0.0/16 | Yes, 16 bits | No | local |
| 10.8.0.0/16 | No, first difference at bit 13 | No | pcx-1 |
| 0.0.0.0/0 | Yes, 0 bits | Yes, 0 bits | nat-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.)
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/0and next hopdefault-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/0with 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.
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 says | Production does | Why |
|---|---|---|
| Each host has a default gateway, a real router with an interface on the subnet | The gateway is an implicit virtual router at the subnet base address plus 1; there is no box to log into | The cloud implements routing in the virtualisation layer, so the router scales with the fleet |
| NAT happens at the edge router | The internet gateway is a managed, distributed service, not a device; the NAT gateway is a separate managed service in a specific subnet | Both are horizontally scaled by the provider; you attach them with routes |
| An IP address identifies a host and lives on its interface | A public IPv4 address is never on the instance's interface; the gateway translates it 1:1 | Public IPv4 is scarce and mapped by the provider at the edge |
| The routing table is per host or per router | The route table is an object you attach to a subnet, shared by every instance in it | One 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
- 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.
Look at State: blackhole means the target no longer exists. A subnet with no association row is on the main 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:
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
Interview questions
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/24can be public or private depending only on its table.
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:
- How do you test each step? Use the layered approach in debugging by layer; the cloud-specific tools are in debugging cloud networks.
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.
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.
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.
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.
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
- 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/24in this page is a documentation-only range (RFC 5737), safe to print in any tutorial.
Cheat sheet
| Concept | AWS | GCP | Azure | Gotcha |
|---|---|---|---|---|
| Route table | Route table, associated per subnet | Routes on the VPC network, selected by tag and priority | Route table (UDR) associated per subnet | AWS: unassociated subnets use the main table |
| Implicit local routing | Local route | Subnet routes | System route, next hop Virtual network | Cannot be deleted in AWS |
| Internet path | Internet gateway | Default route, next hop default-internet-gateway | System route, next hop Internet | Public IP alone is not enough |
| Outbound-only NAT | NAT gateway, route target | Cloud NAT on a Cloud Router, not a route target | NAT Gateway attached to the subnet | Deleting it leaves a blackhole route in AWS |
| Tie-break, same prefix | Static over propagated | Lower priority number | UDR over BGP over system | Prefix length is checked first |