Bali, Indonesia
Insights

Peered Both VPCs to the Hub After Migration, Yet They Still Can't Reach Each Other

October 28, 2026
0:00
A common belief after a migration goes like this: if VPC A is peered to the hub and VPC B is also peered to the hub, then A and B can reach each other through the hub. The diagram supports it, because both spokes connect to the middle. Then the first test from an instance in A to an instance in B times out, or occasionally returns Destination Unreachable, and nothing in the console shows an error. The belief is wrong. Native VPC peering is non-transitive, so a peering connection never acts as a stop on a longer path.
The assumption comes from how people picture a physical network. In a data center, a core switch or router sits in the middle, and anything plugged into it can reach anything else plugged into it, subject to VLANs and ACLs. A hub VPC looks like that core in a diagram, so it is natural to assume it behaves like one. Cloud peering doesn't put a router in the middle. AWS VPC Peering and GCP VPC Network Peering each create a direct, private adjacency between exactly two networks. Azure VNet peering behaves the same way for spoke-to-spoke traffic. The hub is just a third network that happens to have two separate adjacencies. It doesn't forward packets from one adjacency to the other unless you deliberately put something there that does. This confuses many teams during migrations. On-premises, the old setup often had a firewall or core router doing exactly that forwarding job. The migration replaced it with peerings, and the forwarding role disappeared without anyone noticing.
Think of a peering as an agreement between two networks: 'packets from my address range to your address range, and back, may cross here.' The provider's software-defined network enforces that agreement at the fabric level. When a packet arrives at the peering, the fabric checks whether its destination belongs to the network on the other side. If it does, the packet is delivered. If it doesn't, the packet is dropped. Follow a packet from A (illustrative example: 10.1.0.0/16) to B (10.2.0.0/16), with the hub at 10.0.0.0/16:
  1. The instance in A sends to 10.2.0.10.
  2. VPC A's route table needs a route for 10.2.0.0/16. If its only peering route covers the hub's 10.0.0.0/16, there is no match, and the packet goes nowhere.
  3. Suppose someone adds a route for 10.2.0.0/16 pointing at the A-to-hub peering. Depending on the platform, the route is either rejected or accepted, but the peering still won't carry traffic whose destination isn't inside the hub's ranges.
  4. Even if the packet reached the hub, the hub's route to B is a separate peering. Native peering doesn't chain one adjacency into another.
The same rule explains a related surprise. A spoke usually can't use the hub's NAT gateway, VPN connection, or internet gateway through peering either. These are often called edge-to-edge routing limits, and they come from the same design: a peering connects two networks and nothing beyond them. Some platforms let you put a virtual appliance (a VM running routing or firewall software) in the hub and steer spoke traffic to it with custom routes, and that becomes your router. Whether this works over plain peering, or only when traffic is tunneled so that the outer destination is the appliance itself, depends on the provider. Check that in your provider's documentation before you design around it.
The usual symptom is a silent timeout. ping gets no reply, curl hangs until it gives up, and traceroute shows the first hop or nothing at all. The fabric drops packets without telling the sender, so the client only knows that no answer came back. Destination Unreachable can appear too, but it usually comes from a guest OS or an appliance along the path that has no route for the destination, not from the peering itself. Teams sometimes read an ICMP unreachable as proof that a firewall is blocking them and spend time on security groups. Security groups and network ACLs do matter, but they come after routing. If no valid path exists, firewall rules never get evaluated. Two details make this harder to spot. Connectivity between each spoke and the hub works perfectly, so any test against hub resources passes. Health checks from a monitoring box in the hub also stay green. The only broken path is spoke to spoke, which is usually exactly the path the migrated application needs, such as an app tier in one VPC calling a database in another. If the symptom is intermittent rather than a consistent failure, you are probably dealing with something else, such as asymmetric routing or a path that only fails for some flows. That case is covered in Deciding If Inter-VPC Timeouts Are Asymmetric Drops or a Dead Path.
You don't need packet captures to confirm this. Compare which peerings exist with which routes each spoke actually has. On AWS, these read-only commands list peerings with the CIDRs on each side, then show the routes in one VPC:
BASH
# List peering connections with the CIDR on each side (read-only)
aws ec2 describe-vpc-peering-connections \
  --query 'VpcPeeringConnections[].{id:VpcPeeringConnectionId,requester:RequesterVpcInfo.CidrBlock,accepter:AccepterVpcInfo.CidrBlock,status:Status.Code}' \
  --output table

# Show every route in VPC A's route tables (replace the VPC ID)
aws ec2 describe-route-tables \
  --filters 'Name=vpc-id,Values=vpc-0aaaaaaaaaaaaaaaa' \
  --query 'RouteTables[].Routes[]' \
  --output table
Check each peering. If no peering has VPC A on one side and VPC B on the other, then no native path between them exists, regardless of how the route tables look. Also check whether the subnet your instance uses is associated with the route table you are reading. An unassociated subnet falls back to the main route table. On GCP, list the peerings for the spoke network and the routes it imports through them:
BASH
# Peerings attached to the spoke network (read-only)
gcloud compute networks peerings list --network='spoke-a'

# Routes imported from the hub over a specific peering
gcloud compute networks peerings list-routes 'spoke-a-to-hub' \
  --network='spoke-a' --region='us-central1' --direction=INCOMING
If the imported routes cover only the hub's ranges, spoke B is not reachable from spoke A, and adding more peerings to the hub won't change that. Most providers also offer a path analysis tool that simulates a flow between two endpoints and reports where it stops. It is worth running before and after any change. When a support engineer gets a ticket like 'the app can't reach the database since the move,' the quickest first step is to ask for the source and destination VPCs and look at the peering list. Thirty seconds of that usually rules routing in or out before anyone touches firewall rules.
If spokes need to talk to each other, the hub needs a component that actually routes. Major providers offer a managed option for this: AWS Transit Gateway, Azure Virtual WAN, and GCP Network Connectivity Center. The terminology differs, but the model is similar. Each network attaches to the central router, the router keeps its own route tables, and you decide which attachments learn which routes. That last point is the real benefit, not only that the connection works. With a transit router you can let the app spokes reach the shared services spoke without letting them reach each other, or keep production and staging fully apart while both use the same VPN to the office. On AWS this is controlled through route table association and propagation on the transit gateway. Other platforms use their own equivalents. A few things still need care after you switch:
  • Each spoke's own route table must send the other spokes' CIDRs to the transit attachment. Attaching the VPC alone doesn't update its routes.
  • Address ranges must not overlap. Overlapping CIDRs block peering, and they also make transit routing ambiguous. Fix this during migration planning, because it is painful to fix later.
  • Security groups, network ACLs, and OS firewalls still apply on both ends.
  • MTU (the largest packet size allowed on a link) can change between paths, especially when VPN or other encapsulation is involved. If small requests work but large transfers stall after the change, see When Keepalives Vanish: Debugging IPsec MTU Black Holes Across Cloud Transit Gateways.
Managed transit is usually billed per attachment and per amount of data processed, while peering often costs less for the same traffic. Check current pricing for your provider and region rather than assuming either option is cheaper for your traffic pattern.
Non-transitive peering isn't a flaw to work around. It is a deliberate design that keeps peering simple and predictable. If only two or three VPCs need to talk, a direct peering between each pair that needs connectivity is often the simplest and cheapest option, and it adds no extra hop. The trouble starts when the number of pairs grows. Each new VPC needs a peering to every network it talks to, plus routes on both sides, and the mesh quickly becomes hard to track and easy to get wrong. A practical rule of thumb: use direct peering when the set of connections is small and stable, and move to a transit router when you have a real hub, a shared services network, hybrid links, or a need for routing policy between groups of spokes. Either way, decide on purpose. Don't let the diagram imply connectivity the platform never provides. If two spokes can't reach each other after a migration, don't start with security groups. Start with the peering list. If the two VPCs aren't peered directly and nothing in the hub actually routes, the drop is the expected behavior, and the fix is a direct peering or a transit router, not another rule.