VPC Peering
A VPC (Virtual Private Cloud, your own isolated network inside AWS) is normally walled off from every other VPC. VPC peering pokes a private, point-to-point hole between two VPCs so their resources can talk using private IP addresses, as if they were on the same network. Traffic never touches the public internet, so it stays fast and secure. This matters when you split workloads across VPCs (for example, a shared “services” VPC and several “application” VPCs) but still want them to reach each other privately.
What a VPC peering connection is
A peering connection is a 1:1 private link between exactly two VPCs. The two VPCs can be:
- In the same AWS account or in different accounts.
- In the same Region or in different Regions (called inter-Region peering).
Once the link exists and routing is in place, an instance in VPC A can reach an instance in VPC B by its private IP (for example 10.1.0.15) directly. There is no gateway to manage, no bandwidth bottleneck, and no per-hour charge for the connection itself.
When to use this (and when not to)
| Scenario | Best choice |
|---|---|
| Connect just 2 or 3 VPCs privately | VPC peering |
| Connect many VPCs in a hub-and-spoke mesh | Transit Gateway |
| Reach an AWS service (S3, DynamoDB) privately | VPC endpoints |
| Connect a VPC to your on-premises data center | VPN or Direct Connect |
Use peering when the topology is small and simple. The moment you have more than a handful of VPCs that all need to talk to each other, the number of peering links grows fast, and you should switch to Transit Gateway (a central router that every VPC attaches to once).
Two rules you must satisfy first
Before peering will work, two conditions must be true:
- CIDR blocks must not overlap. A CIDR (Classless Inter-Domain Routing block, the IP range of a VPC like
10.0.0.0/16) defines which addresses live in each VPC. If both VPCs use10.0.0.0/16, AWS cannot tell which one a packet is destined for, and peering is rejected. Each VPC needs a distinct range, e.g.10.0.0.0/16and10.1.0.0/16. - You must add routes on BOTH sides. Creating and accepting the connection only builds the pipe. Traffic does not flow until each VPC’s route table has an entry pointing the other VPC’s CIDR at the peering connection.
Gotcha — peering is non-transitive. If A is peered with B, and B is peered with C, that does not mean A can reach C. Each pair needs its own peering connection and its own routes. This is the single most common surprise with peering.
Creating a peering connection
The VPC that starts the request is the requester; the other is the accepter. Here we peer vpc-0a1b2c3d (requester, 10.0.0.0/16) with vpc-0d4e5f6a (accepter, 10.1.0.0/16).
Console steps
- Open the VPC console and choose Peering connections in the left menu.
- Click Create peering connection.
- Give it a Name tag (e.g.
app-to-services). - Under VPC ID (Requester), select
vpc-0a1b2c3d. - Under Select another VPC to peer with, choose My account (or Another account and enter the 12-digit account ID) and the Region. For cross-Region, pick the other Region and enter the VPC ID
vpc-0d4e5f6a. - Click Create peering connection. Its status becomes Pending acceptance.
CLI equivalent
aws ec2 create-vpc-peering-connection \
--vpc-id vpc-0a1b2c3d \
--peer-vpc-id vpc-0d4e5f6a \
--tag-specifications 'ResourceType=vpc-peering-connection,Tags=[{Key=Name,Value=app-to-services}]'
Output:
{
"VpcPeeringConnection": {
"VpcPeeringConnectionId": "pcx-0c1d2e3f4a5b6c7d",
"Status": { "Code": "initiating-request", "Message": "Initiating Request" },
"RequesterVpcInfo": { "VpcId": "vpc-0a1b2c3d", "CidrBlock": "10.0.0.0/16" },
"AccepterVpcInfo": { "VpcId": "vpc-0d4e5f6a", "CidrBlock": "10.1.0.0/16" }
}
}
Accepting the connection
The connection stays inactive until the accepter approves it. If the other VPC is in a different account, an admin of that account performs this step.
Console steps
- In the accepter’s account/Region, open VPC → Peering connections.
- Select the connection (status Pending acceptance).
- Choose Actions → Accept request, then confirm.
- The status changes to Active.
CLI equivalent
aws ec2 accept-vpc-peering-connection \
--vpc-peering-connection-id pcx-0c1d2e3f4a5b6c7d
Output:
{
"VpcPeeringConnection": {
"VpcPeeringConnectionId": "pcx-0c1d2e3f4a5b6c7d",
"Status": { "Code": "active", "Message": "Active" }
}
}
Adding routes on both VPCs
This is the step people forget. Each VPC’s route table needs to know that traffic for the other VPC’s CIDR goes through the peering connection.
# In VPC A's route table: send traffic for VPC B (10.1.0.0/16) over the peering link
aws ec2 create-route \
--route-table-id rtb-0a1b2c3d \
--destination-cidr-block 10.1.0.0/16 \
--vpc-peering-connection-id pcx-0c1d2e3f4a5b6c7d
# In VPC B's route table: send traffic for VPC A (10.0.0.0/16) back over the same link
aws ec2 create-route \
--route-table-id rtb-0d4e5f6a \
--destination-cidr-block 10.0.0.0/16 \
--vpc-peering-connection-id pcx-0c1d2e3f4a5b6c7d
In the console, do this under VPC → Route tables → (select table) → Routes → Edit routes → Add route, with the target set to the peering connection ID.
Also confirm your security groups and network ACLs allow the traffic — peering opens the path, but your firewall rules still apply. Security groups can even reference a group in the peered VPC (same Region only).
Defining peering with infrastructure as code
Terraform keeps the connection, acceptance, and routes in one place:
resource "aws_vpc_peering_connection" "app_to_services" {
vpc_id = "vpc-0a1b2c3d"
peer_vpc_id = "vpc-0d4e5f6a"
auto_accept = true # works only when both VPCs are in the same account and Region
tags = { Name = "app-to-services" }
}
resource "aws_route" "a_to_b" {
route_table_id = "rtb-0a1b2c3d"
destination_cidr_block = "10.1.0.0/16"
vpc_peering_connection_id = aws_vpc_peering_connection.app_to_services.id
}
Cost note
There is no hourly charge for a peering connection. You pay only for data transfer across it. Same-Region, same-AZ traffic over a peering connection is free; traffic that crosses Availability Zones or Regions is billed at standard data-transfer rates (roughly $0.01–$0.02 per GB cross-AZ, and higher cross-Region). Keeping chatty services in the same AZ keeps the bill near zero.
Best practices
- Plan non-overlapping CIDRs across all VPCs from day one — overlaps cannot be fixed later without re-IPing.
- Always add routes on both route tables, and verify the subnets actually use those tables.
- Remember peering is non-transitive; if you find yourself building a mesh of
pcx-*links, move to Transit Gateway. - Scope security groups tightly — reference the peer VPC’s security group where possible instead of opening whole CIDR ranges.
- Tag every connection with a clear name so it is obvious which VPCs it links.
- Use VPC Flow Logs on both sides to confirm and troubleshoot traffic crossing the link.