Skip to content
Professional6 min read

Transit Gateway Route Tables: Association, Propagation, and Segmentation

A Transit Gateway is not a router with one routing table — it is a routing domain per attachment. Understanding which table governs which direction is what separates a working hub-and-spoke design from one that fails asymmetrically.

Published Feb 17, 2026

Most Transit Gateway confusion traces back to a single wrong assumption: that the Transit Gateway has a route table. It does not. It has as many as you create, each attachment consults exactly one of them, and which one it consults depends on the direction traffic is moving. Get that model right and hub-and-spoke design becomes mechanical. Get it wrong and you produce topologies that work in one direction and fail silently in the other.

The two operations, and why you need both

Every attachment participates in routing through two independent relationships. The AWS console presents them as adjacent tabs with no hint that one without the other is useless.

| | Association | Propagation | | --- | --- | --- | | Question it answers | Which route table does traffic arriving from this attachment consult? | Which route tables learn the CIDRs behind this attachment? | | Direction | Ingress — how this attachment sends | Advertisement — how others reach this attachment | | Cardinality | Exactly one route table per attachment | Any number of route tables per attachment | | Failure symptom | This attachment cannot send anywhere | Others cannot reach this attachment |

Read that table again with the cardinality in mind, because it is the asymmetry that causes trouble. An attachment has one association and can have many propagations. A checklist that verifies "attachment is associated" is verifying the operation that is hard to get wrong.

Route lookup happens per direction

This is the part worth internalizing, because it is what makes Transit Gateway failures direction-asymmetric.

When a packet enters the Transit Gateway, the route table consulted is the one associated with the attachment it arrived on. A request and its reply therefore enter on different attachments and may be evaluated against different route tables with different contents.

Consider a spoke talking to shared services:

Request and reply are governed by two different route tables
  1. EC2

    Request leaves spoke-b

    10.2.1.20 → 10.0.1.20

  2. RTB

    Enters on the spoke-b attachment

    consults tgw-rt-spokes — needs 10.0.0.0/16 propagated here

  3. DEST

    Arrives at shared services

    service accepts and answers

  4. RTB

    Reply enters on the shared attachment

    consults tgw-rt-shared — needs 10.2.0.0/16 propagated here

  5. DEST

    Reply reaches spoke-b

    both lookups succeeded

Two lookups, two route tables. A working path requires the destination CIDR to be present in both — and nothing in either VPC's configuration reveals whether it is.

So a complete path needs four things, not two: the sender associated somewhere, the receiver's CIDR propagated into that table, the receiver associated somewhere, and the sender's CIDR propagated into that table. Onboarding runbooks routinely capture the first three.

Segmentation is built from absence

Once you see the Transit Gateway as one routing domain per attachment, segmentation stops needing firewalls or NACLs. You express it by controlling what gets propagated where.

Flat. One route table, every attachment associated with it, every attachment propagated into it. Everything reaches everything. Fine for a small estate; the thing you are migrating away from.

Isolated spokes. Two route tables:

tgw-rt-spokesassoc: every spoke attachment
DestinationTargetState
10.0.0.0/16attach-sharedactiveOnly shared services is propagated here, so a spoke can reach shared and nothing else.

Spoke-to-spoke isolation is enforced by the routes this table does NOT contain. There is no deny rule anywhere.

tgw-rt-sharedassoc: shared services attachment
DestinationTargetState
10.1.0.0/16attach-spoke-aactive
10.2.0.0/16attach-spoke-bactiveEvery spoke must be propagated here, or shared services cannot answer it.

Shared services needs a route back to every spoke. This is the table onboarding forgets.

The consequence for code review is significant: what is missing from a Transit Gateway route table is as load-bearing as what is in it. A reviewer reading only added lines cannot evaluate a segmentation policy, because the policy lives in the absences. Say so explicitly in the module's documentation, or someone will "fix" the missing spoke routes in tgw-rt-spokes and quietly flatten your segmentation while resolving a ticket.

Centralized inspection. Spokes associate with a table whose default route points at an inspection VPC attachment; the inspection VPC associates with a table carrying routes to all spokes. Traffic is forced through the appliance because it is the only next hop the spoke table offers.

Appliance mode

If you build centralized inspection, this setting is not optional.

A Transit Gateway attachment spans multiple availability zones, and by default it keeps traffic in the AZ it arrived in. For a stateful appliance that is fatal: the request may be handled by the appliance ENI in us-east-1a while the reply arrives at the ENI in us-east-1b. The second appliance has no state for that flow and drops it.

resource "aws_ec2_transit_gateway_vpc_attachment" "inspection" {
  transit_gateway_id = aws_ec2_transit_gateway.hub.id
  vpc_id             = aws_vpc.inspection.id
  subnet_ids         = values(aws_subnet.inspection)[*].id
 
  # Pins both directions of a flow to the same AZ so a stateful appliance sees
  # the whole conversation. Set this on the INSPECTION attachment, not the
  # spokes.
  appliance_mode_support = "enable"
}

The symptom without it is memorable and easily misdiagnosed: connections work intermittently, and the pattern correlates with which AZ the client happens to be in rather than with load or time. It looks like a flaky appliance. It is a routing decision.

Reading a route table when something fails

Two habits worth building.

Search for the destination rather than reading the table. A route table with thirty entries is not something you scan reliably. Ask the specific question:

aws ec2 search-transit-gateway-routes \
  --transit-gateway-route-table-id tgw-rtb-0a1b2c3d \
  --filters "Name=route-search.longest-prefix-match,Values=10.2.1.20" \
  --query 'Routes[].[DestinationCidrBlock,Type,State]' --output table

An empty result is a finding, not an absence of information.

Distinguish a missing route from a blackhole. They produce identical symptoms and require different fixes:

| | Missing route | Blackhole route | | --- | --- | --- | | Appears in the table | No | Yes, with state blackhole | | Usual cause | Propagation never configured | Static route whose attachment was deleted, or a deliberate isolation | | How you find it | Search returns empty | Visible when listing routes | | Fix | Add the propagation | Remove the static route or restore the attachment |

A blackhole records intent. A missing propagation leaves nothing behind, which is why it is the harder of the two and why the search-returns-empty habit matters.

What to instrument

Transit Gateway publishes a CloudWatch metric that fires for exactly the failure described above:

  • PacketDropCountNoRoute — a packet arrived and no route matched. Non-zero means some path in your estate is broken right now. Alarm on it.
  • PacketDropCountBlackhole — matched a blackhole route. Expected if you use blackholes deliberately; otherwise a signal that an attachment went away and left a static route behind.
  • BytesIn / BytesOut per attachment — for cost attribution, since Transit Gateway bills per attachment-hour plus per gigabyte processed.

The no-route metric is the one people wish they had had. It is populated from the first dropped packet, well before anyone files a ticket.

Cost, briefly

Transit Gateway is one of the easier services to overspend on because the hourly component scales with attachment count rather than traffic:

  • Roughly $0.05 per attachment-hour. Ten attachments is about $360/month before a single byte moves.
  • Roughly $0.02 per gigabyte processed.
  • Traffic that crosses the Transit Gateway twice — spoke to inspection, inspection to spoke — is processed twice and billed twice. Centralized inspection designs should model this deliberately rather than discovering it.

For an estate of two or three VPCs, peering is cheaper and simpler. Transit Gateway earns its cost at the point where the number of peering relationships becomes unmanageable, or where you need segmentation and centralized inspection.

Where to practice

Lab 03 deploys a hub-and-spoke topology where one spoke is correctly associated and never propagated into the shared services route table. Both VPCs come from the same module, every attachment reports available, and one spoke silently cannot be answered.