Route Tables
Inside a Virtual Private Cloud (VPC) — your own private network in AWS — nothing reaches the internet, another VPC, or an on-premises office by accident. Traffic only goes where a route table tells it to go. A route table is a simple list of rules that says “to reach this range of IP addresses, send the packet to this target.” Get the route tables right and your network just works; get them wrong and instances are either unreachable or, worse, exposed. This page explains how route tables decide traffic flow and how to edit them safely.
What a route table is
A route table is an ordered set of routes. Each route has two parts:
- Destination — a CIDR block (Classless Inter-Domain Routing notation, like
0.0.0.0/0or10.0.0.0/16) describing a range of IP addresses the traffic is headed to. - Target — where to send traffic for that destination. Targets include an internet gateway, a NAT gateway, a peering connection, a VPC endpoint, a network interface, and more.
When an instance sends a packet, AWS looks at the destination IP and picks the route with the most specific match (the longest prefix). For example, traffic to 10.0.5.20 matches a 10.0.0.0/16 route over a 0.0.0.0/0 route, because /16 is more specific than /0.
The local route
Every route table is created with one route you cannot remove or edit: the local route. It covers your entire VPC CIDR (for example 10.0.0.0/16) and its target is the keyword local. This is what lets every subnet in the VPC talk to every other subnet out of the box — no extra configuration needed. You can only ever add routes for destinations outside the VPC.
Common routes you add
The local route handles internal traffic. To reach anything beyond the VPC, you add routes pointing at a specific target.
| Destination | Target | What it enables |
|---|---|---|
0.0.0.0/0 | Internet gateway (igw-...) | Direct internet access (makes a subnet public) |
0.0.0.0/0 | NAT gateway (nat-...) | Outbound-only internet for private subnets |
172.31.0.0/16 | Peering connection (pcx-...) | Reach a peered VPC’s CIDR |
pl-xxxx (prefix list) | Gateway VPC endpoint (vpce-...) | Private access to S3/DynamoDB, no internet |
192.168.0.0/16 | Transit gateway (tgw-...) | Reach many VPCs / on-premises via a hub |
10.1.0.0/16 | Virtual private gateway (vgw-...) | Reach on-premises over VPN / Direct Connect |
Tip: A subnet becomes “public” simply because its route table sends
0.0.0.0/0to an internet gateway. There is no public/private checkbox — the route is the whole story.
Subnet associations and the main route table
A route table only affects a subnet once the two are associated. Each subnet is associated with exactly one route table at a time, but one route table can be shared by many subnets.
Every VPC has a special main route table. It is the default any subnet uses when you have not explicitly associated it with another (a “custom” or “explicit”) route table. The main route table starts with just the local route, so by default new subnets are private.
Gotcha: A subnet that you never explicitly associate silently falls back to the main route table. If someone later adds an internet route to the main route table, every unassociated subnet — possibly including ones holding databases — instantly gets that route. The reverse bites too: you create a public route table but forget to associate your subnet, so the instance stays unreachable. Always create an explicit association for every subnet with the route table you actually intend, and keep the main route table minimal (local route only).
When to use this
- Use one custom route table per tier (one for public subnets, one for private subnets per AZ) so each tier’s routing is obvious and isolated.
- Do not rely on the main route table for real workloads. Treat it as a safety default that should stay empty except for the local route.
Editing routes — Console
- Open the VPC console and choose Route tables in the left menu.
- Select the route table you want to change (e.g.
rtb-0a1b2c3d). Use the Name column to find your public/private table. - Open the Routes tab and click Edit routes.
- Click Add route. Enter the Destination (e.g.
0.0.0.0/0) and pick a Target from the dropdown (e.g. Internet Gateway thenigw-0a1b2c3d). - Click Save changes.
- Open the Subnet associations tab, click Edit subnet associations, tick the subnets that should use this table (e.g.
subnet-0a1b2c3d), and click Save associations.
Editing routes — AWS CLI
Create a route table, add an internet route, and associate a subnet:
# 1. Create a custom route table in the VPC
aws ec2 create-route-table \
--vpc-id vpc-0a1b2c3d \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=public-rt}]'
Output:
{
"RouteTable": {
"RouteTableId": "rtb-0a1b2c3d",
"VpcId": "vpc-0a1b2c3d",
"Routes": [
{
"DestinationCidrBlock": "10.0.0.0/16",
"GatewayId": "local",
"State": "active"
}
],
"Associations": []
}
}
Notice the local route is already there. Now add a default route to the internet gateway:
# 2. Send all internet-bound traffic to the internet gateway
aws ec2 create-route \
--route-table-id rtb-0a1b2c3d \
--destination-cidr-block 0.0.0.0/0 \
--gateway-id igw-0a1b2c3d
Output:
{
"Return": true
}
# 3. Explicitly associate a subnet with this route table
aws ec2 associate-route-table \
--route-table-id rtb-0a1b2c3d \
--subnet-id subnet-0a1b2c3d
Output:
{
"AssociationId": "rtbassoc-0a1b2c3d",
"AssociationState": {
"State": "associated"
}
}
To point private subnets at a NAT gateway instead, use --nat-gateway-id rather than --gateway-id:
aws ec2 create-route \
--route-table-id rtb-0b2c3d4e \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id nat-0a1b2c3d
To change or delete a route, use replace-route (same destination, new target) or delete-route:
aws ec2 delete-route \
--route-table-id rtb-0a1b2c3d \
--destination-cidr-block 0.0.0.0/0
Defining route tables as code
# CloudFormation: public route table + internet route + association
Resources:
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: vpc-0a1b2c3d
Tags:
- Key: Name
Value: public-rt
InternetRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: igw-0a1b2c3d
PublicSubnetAssoc:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: subnet-0a1b2c3d
Cost note: Route tables, routes, and associations are completely free — you are never billed for them. Cost only appears at the targets they point to: a NAT gateway is roughly $0.045/hour (~$32/month) per gateway plus ~$0.045/GB processed in
us-east-1, while an internet gateway is free.
Best practices
- Keep the main route table empty (local route only) so unassociated subnets default to private, never to the internet.
- Explicitly associate every subnet with the route table you intend — never let routing rely on the default fallback.
- Use separate route tables per tier (public vs private) and per AZ for NAT, so a single edit cannot expose the wrong subnet.
- Prefer specific destinations over broad ones; let the most-specific-match rule keep internal traffic on the local route.
- Use gateway VPC endpoints for S3 and DynamoDB so that traffic stays on the AWS network instead of routing through a paid NAT gateway.
- Tag route tables clearly (
public-rt,private-rt-1a) and manage them with CloudFormation or Terraform so changes are reviewed. - Audit routes with VPC Flow Logs when traffic does not flow as expected — it usually traces back to a missing route or wrong association.