VPC Peering vs Transit Gateway: A Complete Cost Analysis
When to use VPC peering versus Transit Gateway with detailed cost modeling for multi-account AWS architectures.

The VPC peering versus Transit Gateway decision is one that every growing AWS organization faces. At 3-4 VPCs, peering is simple and cheap. At 15+ VPCs, the mesh becomes unmanageable. But the crossover point — where Transit Gateway becomes more cost-effective — depends heavily on your traffic patterns, not just your VPC count. This is closely related to understanding cross-region latency penalties that also vary by architecture choice.
After migrating a 23-VPC architecture from full-mesh peering to Transit Gateway, then partially rolling it back for high-traffic pairs, I can share concrete cost data on where each approach wins.
The Fundamental Trade-off
VPC Peering:
- No hourly charge
- $0.01/GB data transfer (same region, cross-AZ)
- Free for same-AZ traffic
- Maximum 125 peering connections per VPC
- No transitive routing
Transit Gateway:
- $0.05/hour per attachment (~$36/month per VPC)
- $0.02/GB data processed
- Supports transitive routing
- Centralized route management
- Up to 5,000 attachments
The per-GB cost difference is significant: Transit Gateway costs 2x per GB compared to peering for same-region traffic. But operational complexity costs money too.
Cost Model: The Math
from dataclasses import dataclass
@dataclass
class NetworkTopology:
num_vpcs: int
monthly_gb_per_pair: float # Average GB transferred between any two VPC pairs
active_pairs_ratio: float # % of possible pairs that actually communicate
hours_per_month: int = 730
def peering_cost(topology: NetworkTopology) -> dict:
"""Calculate monthly cost of full-mesh VPC peering."""
# Number of peering connections needed for full mesh
num_connections = (topology.num_vpcs * (topology.num_vpcs - 1)) // 2
active_connections = int(num_connections * topology.active_pairs_ratio)
# Peering has no hourly cost, only data transfer
total_gb = active_connections * topology.monthly_gb_per_pair
data_cost = total_gb * 0.01 # $0.01/GB same-region
# Operational cost: each connection needs route table entries in both VPCs
route_entries = active_connections * 2
return {
'connections': num_connections,
'active_connections': active_connections,
'total_gb': total_gb,
'monthly_cost': data_cost,
'route_entries': route_entries,
'cost_per_gb': 0.01
}
def transit_gateway_cost(topology: NetworkTopology) -> dict:
"""Calculate monthly cost of Transit Gateway."""
# Hourly attachment cost
attachment_cost = topology.num_vpcs * 0.05 * topology.hours_per_month
# Data processing cost
active_pairs = int(
((topology.num_vpcs * (topology.num_vpcs - 1)) // 2)
* topology.active_pairs_ratio
)
total_gb = active_pairs * topology.monthly_gb_per_pair
data_cost = total_gb * 0.02 # $0.02/GB processed
return {
'attachments': topology.num_vpcs,
'attachment_cost': attachment_cost,
'total_gb': total_gb,
'data_cost': data_cost,
'monthly_cost': attachment_cost + data_cost,
'route_entries': topology.num_vpcs, # One route per VPC to TGW
'cost_per_gb': 0.02 + (attachment_cost / max(total_gb, 1))
}
def find_crossover(max_vpcs: int = 50, gb_per_pair: float = 100.0) -> None:
"""Find the VPC count where Transit Gateway becomes cheaper."""
print(f"{'VPCs':<6} {'Peering':<12} {'TGW':<12} {'Winner':<10}")
print("-" * 40)
for num_vpcs in range(2, max_vpcs + 1, 2):
topology = NetworkTopology(
num_vpcs=num_vpcs,
monthly_gb_per_pair=gb_per_pair,
active_pairs_ratio=0.4
)
peer = peering_cost(topology)
tgw = transit_gateway_cost(topology)
winner = "Peering" if peer['monthly_cost'] < tgw['monthly_cost'] else "TGW"
print(f"{num_vpcs:<6} ${peer['monthly_cost']:<10,.0f} ${tgw['monthly_cost']:<10,.0f} {winner}")
# Run analysis
find_crossover(max_vpcs=30, gb_per_pair=50.0)
Real-World Results
Running this model against our actual traffic patterns:
| VPC Count | Active Pairs | Avg GB/Pair | Peering Cost | TGW Cost | Winner |
|---|---|---|---|---|---|
| 5 | 4 | 200 GB | $8 | $219 | Peering |
| 10 | 18 | 150 GB | $27 | $613 | Peering |
| 15 | 42 | 100 GB | $42 | $1,387 | Peering (by cost) |
| 20 | 76 | 80 GB | $61 | $2,581 | Depends on ops |
| 25 | 120 | 60 GB | $72 | $3,877 | TGW (by ops) |
The pure cost crossover almost never favors Transit Gateway for same-region traffic. Transit Gateway wins on operational simplicity, not raw transfer cost.
When Transit Gateway Wins
Transit Gateway becomes the right choice when:
-
You need transitive routing. VPC A talks to VPC B, which talks to VPC C. With peering, you need A-B AND A-C connections. With TGW, A reaches C through B's routes automatically.
-
You have more than 15 VPCs. Managing N*(N-1)/2 peering connections becomes untenable. At 20 VPCs, that's 190 connections.
-
You need centralized network policies. TGW route tables provide centralized control over what can communicate with what.
-
You connect to on-premises. TGW integrates with Direct Connect and VPN, providing a single transit point.
The Hybrid Approach
Our final architecture uses both:
# Transit Gateway for general connectivity
resource "aws_ec2_transit_gateway" "main" {
description = "Central transit gateway"
default_route_table_association = "disable"
default_route_table_propagation = "disable"
tags = {
Name = "central-tgw"
}
}
# TGW attachments for all VPCs
resource "aws_ec2_transit_gateway_vpc_attachment" "vpcs" {
for_each = var.vpc_configs
subnet_ids = each.value.tgw_subnet_ids
transit_gateway_id = aws_ec2_transit_gateway.main.id
vpc_id = each.value.vpc_id
tags = {
Name = "tgw-attach-${each.key}"
}
}
# Direct peering for high-traffic pairs (>500 GB/month)
resource "aws_vpc_peering_connection" "high_traffic" {
for_each = var.high_traffic_pairs
vpc_id = each.value.requester_vpc_id
peer_vpc_id = each.value.accepter_vpc_id
auto_accept = true
tags = {
Name = "peer-${each.value.requester}-${each.value.accepter}"
Reason = "high-traffic-cost-optimization"
MonthlyGB = each.value.estimated_monthly_gb
}
}
Decision Framework
Use this decision tree:
- < 5 VPCs, no on-prem: VPC Peering only
- 5-15 VPCs, no transitive needs: VPC Peering with automation
- 5-15 VPCs, need transitive routing: Transit Gateway
- 15+ VPCs: Transit Gateway + direct peering for top 5 traffic pairs
- Any count with on-premises connectivity: Transit Gateway required
Monitoring the Hybrid
import boto3
from datetime import datetime, timedelta
def compare_path_costs(tgw_id: str, peering_ids: list[str], region: str) -> dict:
"""Compare costs between TGW and peering paths for optimization decisions."""
cloudwatch = boto3.client('cloudwatch', region_name=region)
# Get TGW bytes processed
tgw_metrics = cloudwatch.get_metric_statistics(
Namespace='AWS/TransitGateway',
MetricName='BytesIn',
Dimensions=[{'Name': 'TransitGateway', 'Value': tgw_id}],
StartTime=datetime.utcnow() - timedelta(days=30),
EndTime=datetime.utcnow(),
Period=86400 * 30,
Statistics=['Sum']
)
tgw_bytes = sum(dp['Sum'] for dp in tgw_metrics['Datapoints'])
tgw_gb = tgw_bytes / (1024**3)
tgw_cost = tgw_gb * 0.02 # Data processing only
# Get peering traffic for comparison
peering_traffic = {}
for peer_id in peering_ids:
metrics = cloudwatch.get_metric_statistics(
Namespace='AWS/VPC',
MetricName='BytesTransferred',
Dimensions=[{'Name': 'VpcPeeringConnectionId', 'Value': peer_id}],
StartTime=datetime.utcnow() - timedelta(days=30),
EndTime=datetime.utcnow(),
Period=86400 * 30,
Statistics=['Sum']
)
peer_gb = sum(dp['Sum'] for dp in metrics['Datapoints']) / (1024**3)
peering_traffic[peer_id] = {
'gb': peer_gb,
'cost': peer_gb * 0.01,
'tgw_equivalent_cost': peer_gb * 0.02
}
return {
'tgw': {'gb': tgw_gb, 'cost': tgw_cost},
'peering': peering_traffic,
'savings_from_peering': sum(
p['tgw_equivalent_cost'] - p['cost']
for p in peering_traffic.values()
)
}
Key Takeaways
- Cost alone favors peering. Transit Gateway will almost always cost more per GB for same-region traffic.
- Operational cost is real. Managing 100+ peering connections requires automation that has its own maintenance burden.
- Hybrid is usually optimal. Use TGW for general connectivity, direct peering for your top traffic pairs.
- Monitor and adjust quarterly. Traffic patterns change. A pair that justified peering last quarter might not this quarter.
- Factor in growth. If you're adding 3-5 VPCs per quarter, start with TGW to avoid rearchitecting later.
The right answer is almost never pure peering or pure Transit Gateway. It's a hybrid that optimizes for both cost and operational sanity, reviewed quarterly as traffic patterns evolve. For the broader picture of managing AWS networking costs, see the data transfer cost optimization guide and NAT Gateway cost reduction patterns.
Recommended reading

Per-Team Cost Allocation in Shared Kubernetes Clusters: From Chaos to Clarity
Implementing accurate per-namespace cost allocation in multi-tenant Kubernetes clusters, covering request vs. usage attribution, shared resource amortization, and building showback dashboards that drive accountability.

Measuring and Eliminating Toil: From 40% to 12% of Engineering Time
A systematic approach to identifying, measuring, and automating toil—the repetitive operational work that scales linearly with service growth and prevents engineers from doing creative work.

Serverless Postgres in Production: Branching, Scale-to-Zero, and the End of Database Provisioning
Running Neon serverless Postgres in production for 8 months — covering database branching workflows, scale-to-zero economics, connection pooling, and migration from RDS.

Comments
No comments yet. Be the first to share your thoughts.