Why 'both are peered to the hub' sounds like enough
What a peering connection actually checks
- The instance in A sends to 10.2.0.10.
- 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.
- 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.
- 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.
How it shows up when you test it
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.
Checking your own topology
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 tableBASH
# 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=INCOMINGThe production pattern: give the hub a real router
- 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.
