← Back to blog
9 min readApril 28, 2026

Transit Gateway Flow Logs: monitor and audit inter-VPC traffic on AWS

Mohamed Aït El KamelBy Mohamed Aït El Kamel, Founder & AWS Solutions Architect
Transit Gateway Flow Logs: monitor and audit inter-VPC traffic on AWS

What are Transit Gateway Flow Logs?

Transit Gateway Flow Logs capture metadata about IP traffic flowing through an AWS Transit Gateway. Introduced in 2023, they work like VPC Flow Logs but operate at the TGW level — recording traffic as it crosses between attachments (VPC-to-VPC, VPN-to-VPC, Direct Connect-to-VPC) rather than at individual network interfaces inside a VPC.

This distinction matters in practice. VPC Flow Logs tell you what entered or left a VPC. TGW Flow Logs tell you what crossed your network hub — including inter-VPC traffic that may not appear in either VPC's flow logs with enough attachment context to be useful for routing audits.

How TGW Flow Logs differ from VPC Flow Logs

Both log IP traffic metadata. The differences are in scope and available fields.

VPC Flow Logs capture traffic at the ENI level inside a VPC. They include the action field (ACCEPT/REJECT) which reflects whether a security group or NACL allowed the traffic. They are the right tool for: debugging security group rules, identifying traffic to/from specific instances, detecting anomalous connections inside a VPC.

TGW Flow Logs capture traffic at the Transit Gateway level — as packets traverse the TGW between attachments. There is no ACCEPT/REJECT field (TGW doesn't have security groups). Instead, TGW-specific fields tell you which attachment sent the traffic and which attachment received it. They are the right tool for: auditing inter-VPC traffic flows, verifying routing isolation between environments, detecting unexpected lateral movement between accounts.

The two are complementary. VPC Flow Logs answer "what happened inside this VPC". TGW Flow Logs answer "what crossed the hub between which environments".

TGW-specific log fields

In addition to the standard 5-tuple fields (source/destination IP, ports, protocol, bytes, packets, start/end), TGW Flow Logs add:

FieldDescription
tgw-idThe Transit Gateway ID handling the traffic
tgw-attachment-idThe attachment where traffic entered the TGW
tgw-src-vpc-account-idAWS account ID of the source VPC
tgw-dst-vpc-account-idAWS account ID of the destination VPC
tgw-pair-attachment-idThe attachment where traffic exited the TGW
tgw-src-vpc-idSource VPC ID
tgw-dst-vpc-idDestination VPC ID
tgw-src-subnet-idSource subnet ID
tgw-dst-subnet-idDestination subnet ID
tgw-src-eniSource ENI
tgw-dst-eniDestination ENI
tgw-src-az-idSource Availability Zone ID
tgw-dst-az-idDestination Availability Zone ID
tgw-pair-attachment-idThe egress attachment ID
log-statusOK, NODATA, or SKIPDATA

The tgw-src-vpc-account-id and tgw-dst-vpc-account-id fields are especially useful in multi-account environments — they let you identify cross-account traffic flows without correlating across multiple VPC Flow Log groups.

Enabling TGW Flow Logs

Flow logs can be enabled at the Transit Gateway level (captures all attachment traffic) or at the attachment level (captures traffic for a specific VPC, VPN, or Direct Connect attachment).

Enable on a Transit Gateway (AWS CLI):

aws ec2 create-flow-logs \
  --resource-type TransitGateway \
  --resource-ids tgw-xxxxxxxx \
  --traffic-type ALL \
  --log-destination-type s3 \
  --log-destination arn:aws:s3:::my-tgw-flow-logs/tgw/ \
  --log-format '${version} ${resource-type} ${account-id} ${tgw-id} ${tgw-attachment-id} ${tgw-src-vpc-account-id} ${tgw-dst-vpc-account-id} ${tgw-src-vpc-id} ${tgw-dst-vpc-id} ${tgw-src-subnet-id} ${tgw-dst-subnet-id} ${tgw-src-eni} ${tgw-dst-eni} ${tgw-src-az-id} ${tgw-dst-az-id} ${tgw-pair-attachment-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${log-status}'

Enable on a specific attachment:

aws ec2 create-flow-logs \
  --resource-type TransitGatewayAttachment \
  --resource-ids tgw-attach-xxxxxxxx \
  --traffic-type ALL \
  --log-destination-type s3 \
  --log-destination arn:aws:s3:::my-tgw-flow-logs/attachments/

Terraform:

resource "aws_flow_log" "tgw" {
  resource_type        = "TransitGateway"
  traffic_type         = "ALL"
  transit_gateway_id   = aws_ec2_transit_gateway.main.id
  log_destination_type = "s3"
  log_destination      = "${aws_s3_bucket.flow_logs.arn}/tgw/"

  log_format = join(" ", [
    "${version}", "${resource-type}", "${account-id}",
    "${tgw-id}", "${tgw-attachment-id}",
    "${tgw-src-vpc-account-id}", "${tgw-dst-vpc-account-id}",
    "${tgw-src-vpc-id}", "${tgw-dst-vpc-id}",
    "${srcaddr}", "${dstaddr}", "${srcport}", "${dstport}",
    "${protocol}", "${bytes}", "${start}", "${end}", "${log-status}"
  ])
}

Note the ${...} escaping in Terraform — this prevents Terraform from interpreting the ${} as interpolation syntax.

Querying with Athena

For S3-stored logs, Athena is the most practical query tool. Create the table with partition projection to avoid scanning the full bucket.

CREATE EXTERNAL TABLE tgw_flow_logs (
  version        int,
  resource_type  string,
  account_id     string,
  tgw_id         string,
  tgw_attach_id  string,
  src_account    string,
  dst_account    string,
  src_vpc        string,
  dst_vpc        string,
  src_subnet     string,
  dst_subnet     string,
  src_eni        string,
  dst_eni        string,
  src_az         string,
  dst_az         string,
  pair_attach_id string,
  srcaddr        string,
  dstaddr        string,
  srcport        int,
  dstport        int,
  protocol       int,
  packets        bigint,
  bytes          bigint,
  start          bigint,
  end            bigint,
  log_status     string
)
PARTITIONED BY (dt string)
ROW FORMAT DELIMITED FIELDS TERMINATED BY ' '
STORED AS TEXTFILE
LOCATION 's3://my-tgw-flow-logs/tgw/'
TBLPROPERTIES (
  "projection.enabled"    = "true",
  "projection.dt.type"    = "date",
  "projection.dt.format"  = "yyyy/MM/dd",
  "projection.dt.range"   = "2024/01/01,NOW",
  "projection.dt.interval"= "1",
  "projection.dt.interval.unit" = "DAYS",
  "storage.location.template" = "s3://my-tgw-flow-logs/tgw/${dt}"
);

Useful queries

All inter-VPC traffic in the last 24 hours, grouped by VPC pair:

SELECT
  src_vpc,
  dst_vpc,
  src_account,
  dst_account,
  SUM(bytes)   AS total_bytes,
  SUM(packets) AS total_packets
FROM tgw_flow_logs
WHERE dt >= date_format(current_date - interval '1' day, '%Y/%m/%d')
  AND src_vpc  != '-'
  AND dst_vpc  != '-'
  AND src_vpc  != dst_vpc
GROUP BY src_vpc, dst_vpc, src_account, dst_account
ORDER BY total_bytes DESC;

Detect unexpected cross-account traffic (prod → non-prod):

SELECT
  src_account, dst_account,
  srcaddr, dstaddr, dstport,
  SUM(bytes) AS bytes
FROM tgw_flow_logs
WHERE dt >= date_format(current_date - interval '7' day, '%Y/%m/%d')
  AND src_account = '111111111111'   -- prod account
  AND dst_account = '222222222222'   -- nonprod account
GROUP BY src_account, dst_account, srcaddr, dstaddr, dstport
ORDER BY bytes DESC;

Top talkers through the TGW (bytes by source IP):

SELECT
  srcaddr,
  src_vpc,
  SUM(bytes) AS total_bytes
FROM tgw_flow_logs
WHERE dt = date_format(current_date, '%Y/%m/%d')
  AND log_status = 'OK'
GROUP BY srcaddr, src_vpc
ORDER BY total_bytes DESC
LIMIT 20;

Traffic from VPN attachment to VPCs (on-prem reach audit):

SELECT
  tgw_attach_id AS vpn_attachment,
  dst_vpc,
  dst_account,
  dstaddr,
  dstport,
  SUM(bytes) AS bytes
FROM tgw_flow_logs
WHERE dt >= date_format(current_date - interval '7' day, '%Y/%m/%d')
  AND tgw_attach_id = 'tgw-attach-xxxxxxxx'  -- your VPN attachment ID
GROUP BY tgw_attach_id, dst_vpc, dst_account, dstaddr, dstport
ORDER BY bytes DESC;

TGW Flow Logs vs VPC Flow Logs: when to use which

QuestionUse
Why was traffic from IP X rejected in VPC A?VPC Flow Logs (has ACCEPT/REJECT)
Is prod VPC communicating with dev VPC?TGW Flow Logs (cross-attachment context)
Which instance inside VPC A is the top bandwidth user?VPC Flow Logs
Which AWS accounts are exchanging traffic through the TGW?TGW Flow Logs
Is VPN/on-prem reaching VPCs it shouldn't?TGW Flow Logs
Is a Lambda function making unexpected outbound calls?VPC Flow Logs
Are two business units' accounts isolating traffic correctly?TGW Flow Logs

For comprehensive network observability, enable both. VPC Flow Logs at the VPC level for per-resource troubleshooting; TGW Flow Logs at the TGW level for routing policy validation and cross-account audits.

Cost

TGW Flow Logs follow the same pricing model as VPC Flow Logs:

  • Log delivery to S3: ~$0.02–$0.09/GB depending on region (S3 PUT costs negligible)
  • Log delivery to CloudWatch Logs: $0.50/GB ingested + $0.03/GB/month storage
  • Athena queries: $5/TB scanned — partition projection is essential to keep costs down

Traffic volume on a TGW is typically lower than the sum of individual VPC flow log volumes, since TGW logs capture flows between attachments rather than every ENI-level packet. A 10-VPC environment with moderate inter-VPC traffic might generate 10–50 GB/month of TGW flow logs — S3 delivery cost under $5/month.

Recommendation: always use S3 as the destination. CloudWatch Logs ingestion costs add up quickly at volume, and Athena ad-hoc querying against S3 is more powerful for the batch analysis that TGW logs are typically used for.

Practical setup checklist

  • Enable at the TGW level (not attachment level) unless you have a specific reason to scope it
  • Use a custom log format — the default format omits TGW-specific fields, making the logs much less useful
  • Send to a centralized S3 bucket in your network/security account with appropriate bucket policy
  • Create the Athena table with partition projection immediately — don't wait until you need to query
  • Set S3 lifecycle policy: move to Intelligent-Tiering after 30 days, expire after 90 days (or longer for compliance requirements)
  • If using AWS Organizations, consider aggregating logs from all TGWs into a single S3 bucket using prefix per TGW ID

VizCon + TGW Flow Logs

TGW Flow Logs tell you what traffic is crossing your hub. VizCon shows you how your hub is wired — which VPCs attach to which TGW, what route tables govern which attachments, and whether your routing policy (prod isolated from nonprod, on-prem limited to shared services) is actually implemented. When a TGW flow log query surfaces unexpected cross-account traffic, you can open VizCon to immediately understand the route it took: which TGW route table allowed it, which attachment it exited through, and which VPC subnets were involved.

See how VizCon works in 10 minutes

Book a personalized demo and discover how VizCon visualizes your live AWS infrastructure.

Book a demo