
Identify and Optimize NAT Gateway Traffic Using VPC Flow Logs and DNS Query Logs
As NAT Gateway traffic grows, a portion of it is destined for AWS services or SaaS providers that could be reached more cost-effectively — and privately — through VPC Endpoints or PrivateLink, but flow logs alone only show raw destination IPs, making it hard to know what's worth moving. This guide shows a serverless approach: enable VPC Flow Logs and Route 53 DNS query logs for a few days, then use Amazon Athena to produce a ranked list of your NAT destinations resolved to real domain names.
NAT Gateways provide a managed, highly available path for private subnet workloads to reach the internet and AWS service endpoints. As environments grow, so does the traffic flowing through them, and with it the opportunity to route more efficiently. Traffic destined for AWS services like S3, CloudWatch, or ECR (and even third-party SaaS providers) can often be routed more efficiently through VPC Endpoints or private connectivity options, improving both cost and security posture.
The practical challenge is visibility: VPC Flow Logs include a
pkt_dst_aws_service field, but in practice it only distinguishes a few services (S3, DynamoDB, EC2). The majority of AWS service traffic is labeled generically as AMAZON, which doesn't help you decide between a CloudWatch endpoint, an STS endpoint, or a KMS endpoint. Internet-bound traffic shows only a raw destination IP. Traditionally, teams perform manual reverse DNS lookups to identify those IPs, a tedious process that doesn't scale. By correlating flow logs with Route 53 DNS query logs, you get specific domain-level attribution automatically.This article walks through a serverless approach to solve that problem. You'll enable VPC Flow Logs and Route 53 DNS Query Logs for a few days, then use Amazon Athena to produce a ranked list of NAT Gateway destinations, resolved to domain names and classified by AWS service. The setup takes about 30 minutes of hands-on time spread across a week.
Before investing the collection time, confirm there's a prize worth chasing: check your NAT Gateway's
BytesOutToDestination metric in CloudWatch, or the NatGateway-Bytes usage type in Cost Explorer. If processing volume is already low, the optimization upside is limited and you can skip the rest.What you get at the end: A table showing where your NAT Gateway traffic is going, ordered by volume, with enough detail to make routing decisions. Treat the results as directional: they rank destinations by approximate volume to guide where to act, not as an exact, flow-by-flow accounting.
The Approach
Two data sources, joined in Athena:
- VPC Flow Logs in Parquet format, where every flow record includes byte counts and, critically, the
pkt_dst_aws_servicefield that tags traffic destined for AWS services - Route 53 Resolver DNS Query Logs, capturing every DNS lookup made from the VPC, which lets us resolve raw destination IPs back to domain names like
monitoring.us-east-1.amazonaws.comorapi.datadoghq.com
The detection technique relies on a simple property of NAT translation: when a flow log record shows
dstaddr ≠ pkt_dstaddr, that flow traversed a NAT Gateway. No need to hardcode any NAT Gateway IPs; the mismatch is the signature.
A minimum of 3 days captures steady-state traffic patterns. Extend to 7 days if your environment includes weekly batch jobs, scheduled tasks, or CI/CD pipelines that run on specific days.
Prerequisites
- An existing NAT Gateway carrying traffic you want to analyze. Private subnets in the VPC should already send their internet-bound traffic (
0.0.0.0/0) to the NAT Gateway. The workloads generating that traffic are what you're profiling. - Amazon-provided DNS in the VPC. The analysis relies on Route 53 Resolver query logs, which only capture lookups that go through the VPC's default resolver. This is on by default; it won't work if your VPC's DHCP option set points
domain-name-serversat a custom or non-AWS resolver. - IAM permissions to create S3 buckets and bucket policies, VPC Flow Logs, Route 53 Resolver query log configurations, and Athena tables.
Expected spend. Plan for a few dollars total across the 3–7 day collection window: CloudWatch Vended Logs delivery charges for both flow logs and DNS logs, S3 storage, and Athena query cost (~$0.05–$0.10 per analysis run). See CloudWatch Vended Logs pricing and Athena pricing for current rates.
Step 1: Create an S3 Bucket
Create a bucket in the same region as your VPC to receive both log types. A naming convention like
nat-traffic-analysis-<ACCOUNT_ID>-<REGION> keeps things identifiable. Enable "Block all public access" and SSE-S3 encryption. See Creating a bucket if you need a walkthrough.You'll create the two log sources next, then apply a single bucket policy that authorizes both to write (Step 4). Applying the policy after the log sources exist avoids a sequencing pitfall: creating a flow log in the Console replaces any existing bucket policy, which would otherwise wipe out the Route 53 statement.
Step 2: Enable VPC Flow Logs
Create a flow log on the target VPC with the following settings. The field order matters: it must match the Athena DDL in Step 6 exactly.
| Setting | Value |
|---|---|
| Filter | All |
| Aggregation interval | 10 minutes |
| Destination | S3 bucket with /flow-logs/ prefix |
| Log record format | Custom, v2 through v5 fields in the order below |
| File format | Parquet |
| Hive-compatible S3 prefix | Enabled |
| Partition by time | Every 1 hour |
Field order (29 fields, versions 2–5):
1
version account-id interface-id srcaddr dstaddr srcport dstport protocol packets bytes start end action log-status vpc-id subnet-id instance-id tcp-flags type pkt-srcaddr pkt-dstaddr region az-id sublocation-type sublocation-id pkt-src-aws-service pkt-dst-aws-service flow-direction traffic-path
Important: The flow log must capture the NAT Gateway's elastic network interface. The simplest way is to scope the flow log at the VPC level (as below), which captures all ENIs including the NAT GW in the public subnet. If you scope the flow log only to a private subnet or a specific instance ENI, thedstaddr ≠ pkt_dstaddrdetection will return zero rows, because the NAT translation is only visible from the NAT GW ENI's perspective.
For Console setup, see Publish flow logs to Amazon S3 . For CLI:
1
2
3
4
5
6
7
8
9
10
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids "${VPC_ID}" \
--traffic-type ALL \
--log-destination-type s3 \
--log-destination "arn:aws:s3:::${BUCKET_NAME}/flow-logs/" \
--max-aggregation-interval 600 \
--log-format '${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}' \
--destination-options "FileFormat=parquet,HiveCompatiblePartitions=true,PerHourPartition=true" \
--tag-specifications 'ResourceType=vpc-flow-log,Tags=[{Key=Name,Value=nat-traffic-analysis}]'
Why Parquet? It's columnar, compresses ~5× better than text, and Athena charges by data scanned, so smaller files mean cheaper queries. See the VPC Flow Logs in Parquet blog post for details.
Step 3: Enable Route 53 Resolver DNS Query Logging
This captures every DNS query made by resources in the VPC. We need it to map the raw destination IPs from flow logs back to human-readable domain names.
Point it at the same S3 bucket under a
/dns-logs/ prefix, and associate it with your target VPC. See Resolver query logging for the Console walkthrough, or use the CLI:1
2
3
4
5
6
7
8
CONFIG_ID=$(aws route53resolver create-resolver-query-log-config \
--name "nat-traffic-dns-logs" \
--destination-arn "arn:aws:s3:::${BUCKET_NAME}/dns-logs/" \
--query 'ResolverQueryLogConfig.Id' --output text)
aws route53resolver associate-resolver-query-log-config \
--resolver-query-log-config-id "${CONFIG_ID}" \
--resource-id "${VPC_ID}"
Once the bucket policy is in place (Step 4), DNS logs start appearing in S3 within a few minutes of the first query. Note that CloudWatch Vended Logs delivery charges apply to Resolver query logs even when published directly to S3.
Step 4: Apply the Bucket Policy
Both the VPC Flow Logs delivery service and Route 53 Resolver need
s3:PutObject permission on the bucket. The combined policy below covers both; apply it now that both log sources exist.Applying it at this point, rather than at bucket creation, sidesteps a sequencing pitfall: creating a flow log in the Console replaces any existing bucket policy (the CLI appends to it instead), so a policy applied earlier could be silently overwritten. AWS needs the policy in place at delivery time, not at creation time, so the log sources begin delivering as soon as you apply it.
In the S3 Console, go to your bucket → Permissions → Bucket policy, and apply all three statements below. For background on the required permissions, see flow log S3 permissions and Resolver logging targets .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AWSLogDeliveryWrite",
"Effect": "Allow",
"Principal": {"Service": "delivery.logs.amazonaws.com"},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::BUCKET_NAME/flow-logs/AWSLogs/aws-account-id=ACCOUNT_ID/*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "bucket-owner-full-control",
"aws:SourceAccount": "ACCOUNT_ID"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:logs:REGION:ACCOUNT_ID:*"
}
}
},
{
"Sid": "AWSLogDeliveryAclCheck",
"Effect": "Allow",
"Principal": {"Service": "delivery.logs.amazonaws.com"},
"Action": "s3:GetBucketAcl",
"Resource": "arn:aws:s3:::BUCKET_NAME",
"Condition": {
"StringEquals": {"aws:SourceAccount": "ACCOUNT_ID"},
"ArnLike": {"aws:SourceArn": "arn:aws:logs:REGION:ACCOUNT_ID:*"}
}
},
{
"Sid": "Route53ResolverLoggingWrite",
"Effect": "Allow",
"Principal": {"Service": "delivery.logs.amazonaws.com"},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::BUCKET_NAME/dns-logs/AWSLogs/ACCOUNT_ID/*",
"Condition": {
"StringEquals": {"s3:x-amz-acl": "bucket-owner-full-control"}
}
}
]
}
Replace
BUCKET_NAME, ACCOUNT_ID, and REGION with your values. The flow-logs Resource uses the aws-account-id= prefix to match the Hive-partitioned delivery path enabled in Step 2.The Route 53 statement intentionally omits
aws:SourceAccount because the Resolver delivery service doesn't populate that context variable on PutObject requests.Step 5: Collect Data (3–7 Days)
Both log sources begin delivering within about 10–15 minutes of the policy being applied. Verify by listing objects under
s3://BUCKET_NAME/flow-logs/ and s3://BUCKET_NAME/dns-logs/.Let the logs accumulate:
- 3 days minimum, sufficient for steady-state workloads (web apps, APIs, microservices) where traffic patterns repeat daily
- 7 days recommended if your environment includes weekly batch jobs, scheduled ETL, CI/CD pipelines, or cron tasks that only run on certain days
Step 6: Create Athena Tables
With logs in S3, create two external tables in Athena. Both use partition projection , so new hourly partitions are discovered automatically, with no
MSCK REPAIR TABLE needed.Flow Logs Table
See Querying VPC flow logs in Parquet for background.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
CREATE EXTERNAL TABLE IF NOT EXISTS nat_flow_logs (
version int,
account_id string,
interface_id string,
srcaddr string,
dstaddr string,
srcport int,
dstport int,
protocol bigint,
packets bigint,
bytes bigint,
start bigint,
`end` bigint,
action string,
log_status string,
vpc_id string,
subnet_id string,
instance_id string,
tcp_flags int,
type string,
pkt_srcaddr string,
pkt_dstaddr string,
region string,
az_id string,
sublocation_type string,
sublocation_id string,
pkt_src_aws_service string,
pkt_dst_aws_service string,
flow_direction string,
traffic_path int
)
PARTITIONED BY (
`aws-account-id` string,
`aws-service` string,
`aws-region` string,
`year` string,
`month` string,
`day` string,
`hour` string
)
ROW FORMAT SERDE 'org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe'
STORED AS INPUTFORMAT 'org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat'
LOCATION 's3://BUCKET_NAME/flow-logs/AWSLogs/'
TBLPROPERTIES (
'EXTERNAL'='true',
'projection.enabled'='true',
'projection.aws-account-id.type'='enum',
'projection.aws-account-id.values'='ACCOUNT_ID',
'projection.aws-service.type'='enum',
'projection.aws-service.values'='vpcflowlogs',
'projection.aws-region.type'='enum',
'projection.aws-region.values'='REGION',
'projection.year.type'='integer',
'projection.year.range'='2024,2030',
'projection.month.type'='integer',
'projection.month.range'='1,12',
'projection.month.digits'='2',
'projection.day.type'='integer',
'projection.day.range'='1,31',
'projection.day.digits'='2',
'projection.hour.type'='integer',
'projection.hour.range'='0,23',
'projection.hour.digits'='2',
'storage.location.template'=
's3://BUCKET_NAME/flow-logs/AWSLogs/aws-account-id=${aws-account-id}/aws-service=${aws-service}/aws-region=${aws-region}/year=${year}/month=${month}/day=${day}/hour=${hour}'
);
DNS Query Logs Table
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE EXTERNAL TABLE IF NOT EXISTS dns_query_logs (
version string,
account_id string,
region string,
vpc_id string,
query_timestamp string,
query_name string,
query_type string,
query_class string,
rcode string,
answers array<struct<Rdata:string, Type:string, Class:string>>,
srcaddr string,
srcport int,
transport string,
srcids struct<instance:string, resolver_endpoint:string>
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
STORED AS INPUTFORMAT 'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://BUCKET_NAME/dns-logs/AWSLogs/ACCOUNT_ID/vpcdnsquerylogs/VPC_ID/'
TBLPROPERTIES ('classification'='json');
Replace
BUCKET_NAME, ACCOUNT_ID, REGION, and VPC_ID in both DDL statements.Verify the DNS table parses correctly. Run this after creating the table:
1
2
3
SELECT query_name, rcode, srcaddr, answers
FROM dns_query_logs
LIMIT 5;
All four columns should populate. If
query_name/rcode/srcaddr show data but answers is NULL on every row, the JSON structure doesn't match the DDL's answers array<struct<Rdata, Type, Class>> schema. (An empty answers: [] array on some rows is normal: it means the response had no answer records, such as an NXDOMAIN or a name with no record of the queried type.)To inspect the raw JSON and compare field names:
1
2
aws s3 cp s3://BUCKET_NAME/dns-logs/AWSLogs/ACCOUNT_ID/vpcdnsquerylogs/VPC_ID/YYYY/MM/DD/<file>.gz - \
| gunzip | head -1 | jq .
Step 7: Run the Analysis Query
This query identifies all traffic that traversed the NAT Gateway in both directions (uploads and downloads), groups by destination, joins DNS logs to resolve IPs to domain names, and ranks by total bytes transferred.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
WITH nat_flows AS (
SELECT
pkt_dstaddr AS destination_ip,
dstport,
protocol,
bytes AS total_bytes,
pkt_dst_aws_service,
'outbound' AS direction
FROM nat_flow_logs
WHERE year = 'YYYY' AND month = 'MM' AND day BETWEEN 'DD' AND 'DD'
AND dstaddr <> pkt_dstaddr
AND log_status = 'OK'
UNION ALL
SELECT
pkt_srcaddr AS destination_ip,
srcport AS dstport,
protocol,
bytes AS total_bytes,
pkt_src_aws_service AS pkt_dst_aws_service,
'inbound' AS direction
FROM nat_flow_logs
WHERE year = 'YYYY' AND month = 'MM' AND day BETWEEN 'DD' AND 'DD'
AND srcaddr <> pkt_srcaddr
AND log_status = 'OK'
),
dns_map AS (
SELECT ip, MAX(domain) AS domain
FROM (
SELECT DISTINCT
ans.Rdata AS ip,
query_name AS domain
FROM dns_query_logs
CROSS JOIN UNNEST(answers) AS t(ans)
WHERE ans.Type = 'A'
)
GROUP BY ip
)
SELECT
COALESCE(dm.domain, NULLIF(nf.pkt_dst_aws_service, '-'), nf.destination_ip) AS destination_name,
arbitrary(NULLIF(nf.pkt_dst_aws_service, '-')) AS aws_service,
COUNT(DISTINCT nf.destination_ip) AS distinct_ips,
COUNT(*) AS flow_count,
SUM(nf.total_bytes) AS total_bytes,
ROUND(SUM(nf.total_bytes) / 1073741824.0, 3) AS total_gb
FROM nat_flows nf
LEFT JOIN dns_map dm ON nf.destination_ip = dm.ip
GROUP BY 1
ORDER BY total_bytes DESC
LIMIT 50;
Update the date filters (
YYYY, MM, DD) to match your collection window. Always include them. Without partition filters, Athena scans all historical data and the query will be slow and expensive.Performance: A 7-day dataset from a medium VPC (~5 GB Parquet) typically completes in 15–30 seconds at a cost of $0.05–$0.10. For repeated exploration, consider materializing with
CREATE TABLE AS SELECT.Results are grouped by resolved destination, with
distinct_ips showing how many addresses each destination was served from. To break a destination back out by IP, add destination_ip to the SELECT and GROUP BY.Note on DNS attribution: Domain resolution is best-effort: it covers only the lookups that went through the VPC's Route 53 Resolver during the collection window, so traffic to hardcoded IPs or cached records won't resolve. When a destination resolved,destination_nameshows one of the domains seen for that IP (indicative, not a guaranteed 1:1 mapping per flow); when it didn't, the column falls back to the AWS service tag or the raw IP.
Note on private NAT Gateways: If your VPC uses a private NAT Gateway for inter-VPC routing (e.g., overlapping CIDRs), that traffic will also appear in the results, identifiable by private destination IPs (10.x, 172.16–31.x, 192.168.x). This doesn't affect the accuracy of the report; simply focus on public IP and AWS service destinations when evaluating VPC Endpoint candidates.
Step 8: Reading the Results
The output gives you two categories of destinations worth acting on. Sort them by reading the
destination_name (domain) column; the aws_service tag is too coarse to rely on for this.AWS Service Traffic: VPC Endpoint Candidates
Look for
destination_name values ending in amazonaws.com. These are AWS service endpoints currently reached through NAT that could go through a VPC Endpoint instead. The subdomain identifies the service (monitoring.*, sts.*, kms.*, and so on).AWS PrivateLink now supports 200+ AWS services . For almost all of them, a domain of
<service>.*.amazonaws.com maps to the endpoint com.amazonaws.<REGION>.<service>, so a monitoring.* domain means the com.amazonaws.us-east-1.monitoring endpoint, and so on.The services that most commonly top the results:
s3, dynamodb, monitoring, logs, ssm, ssmmessages, ec2messages, sts, kms, secretsmanager, sqs, sns, execute-api, lambda, elasticfilesystem, bedrock-runtime, and ec2. A few endpoint names don't match their domain exactly (for example, Kinesis and ECR), so confirm the precise name in the PrivateLink service catalog when you create the endpoint.Gateway endpoints (
s3 and dynamodb) are free with no hourly or per-GB charges, so deploy these regardless of volume. Note that S3 also offers a (priced) Interface endpoint for cross-region or on-premises (Direct Connect) access; the free Gateway endpoint is the right choice for same-region VPC traffic.Every other service above uses an Interface endpoint, which carries a fixed hourly cost per AZ plus $0.01/GB processed. For high-volume destinations, they're significantly cheaper than NAT's $0.045/GB processing charge. The Appendix at the end of this article includes the break-even math, but as a quick rule of thumb: if a single destination is moving more than ~400 GB/month through your 2-AZ NAT Gateway, an Interface endpoint will likely cost less.
Note: While PrivateLink coverage is extensive, not every service is available in every region. Verify availability for your specific region at AWS services that integrate with AWS PrivateLink .
Third-Party SaaS Traffic: Private Connectivity Candidates
Look for
destination_name values that are recognizable vendor domains rather than amazonaws.com. These are third-party services reached over NAT.A growing number of these providers support private connectivity via AWS PrivateLink. Some common examples:
| Provider | Typical domains in results | Documentation |
|---|---|---|
| Datadog | *.datadoghq.com | Connect to Datadog over AWS PrivateLink |
| MongoDB Atlas | *.mongodb.net | Atlas Private Endpoints |
| Snowflake | *.snowflakecomputing.com | AWS PrivateLink & Snowflake |
| Confluent Cloud | *.confluent.cloud | Set up AWS PrivateLink |
| Elastic Cloud | *.elastic-cloud.com | Private connectivity with AWS PrivateLink |
| HashiCorp Vault (HCP) | *.hashicorp.cloud | Configure AWS PrivateLink |
| Splunk Cloud | *.splunkcloud.com | About private connectivity |
Switching these flows to PrivateLink removes NAT Gateway data processing charges for that traffic and keeps it on the AWS network rather than traversing the public internet.
The list above is not exhaustive; many other SaaS providers support PrivateLink. If you see significant traffic to any third-party domain in your results, check the vendor's networking or security documentation for PrivateLink support, or reach out to them directly.
Next Steps: Deploying VPC Endpoints
Once you've identified your top destinations, the following resources walk through the implementation:
- Migrate your workloads to use VPC endpoints with minimum downtime : step-by-step migration from NAT to VPC Endpoints without disrupting existing traffic flows (Sep 2024)
- Streamline DNS management for AWS PrivateLink deployment with Route 53 Profiles : managing private DNS across multiple VPCs without per-VPC hosted zone associations (Feb 2025)
- How to use interface VPC endpoints to meet your security objectives : endpoint policies, private DNS configuration, and restricting access (Oct 2024)
Core documentation:
- Create an interface endpoint : the step-by-step for creating your first endpoint and enabling private DNS
- Manage DNS names for VPC endpoint services : how private DNS resolution works with endpoints
Step 9: Cleanup
Once you have your results:
- Delete the VPC Flow Log (VPC Console → Flow Logs tab, or
aws ec2 delete-flow-logs) - Disassociate the VPC from the Route 53 DNS Query Log config, then delete the config (Route 53 Console → Resolver → Query logging, or
aws route53resolver disassociate-resolver-query-log-configfollowed byaws route53resolver delete-resolver-query-log-config). The config can't be deleted while a VPC is still associated. - Empty and delete the S3 bucket, or add a lifecycle rule to auto-expire objects
- Drop the Athena tables:
1
2
DROP TABLE IF EXISTS nat_flow_logs;
DROP TABLE IF EXISTS dns_query_logs;
Appendix: Interface Endpoint Break-Even
Interface endpoints are cheaper than NAT for high-volume destinations, but carry a fixed hourly cost. Here's how to decide.
The economics: NAT charges $0.045/GB. An Interface endpoint charges $0.01/GB plus $0.01/hour per Availability Zone. The net savings when you move a flow to an endpoint is $0.035/GB, but you need enough volume to cover the endpoint's fixed hourly cost.
Solving for the break-even:
1
2
3
Total_GB × $0.045 = (AZs × 730 hrs × $0.01) + (Total_GB × $0.01)
Total_GB × $0.035 = AZs × $7.30
Total_GB = AZs × 208.6
| Endpoint AZ count | Fixed cost/month | Break-even traffic to one destination |
|---|---|---|
| 1 AZ | $7.30 | ~209 GB/month |
| 2 AZs | $14.60 | ~417 GB/month |
| 3 AZs | $21.90 | ~626 GB/month |
Reading this against your query output: The
total_gb column is the combined traffic to that destination across all AZs. If a row exceeds the break-even for your planned endpoint AZ count, the endpoint will be cheaper than continuing through NAT.Quick check, at exactly 417 GB with 2 AZs:
- NAT: 417 × $0.045 = $18.77
- Endpoint: $14.60 + (417 × $0.01) = $18.77 ✓
Above that, every additional GB saves $0.035. (Note: the $0.01/GB endpoint data processing rate is tiered, dropping to $0.006/GB above 1 PB/month and $0.004/GB above 5 PB/month per region. For most environments this first tier applies.) Below it, NAT remains the more economical path for that specific destination.
These rates apply to standard commercial regions (us-east-1, eu-west-1, etc.). Pricing varies by region; confirm at NAT Gateway pricing and PrivateLink pricing .
Further Reading
- VPC Flow Logs documentation
- Flow log record fields (versions 2–5)
- Route 53 Resolver query logging
- AWS services that integrate with AWS PrivateLink
- Optimize performance and reduce costs for network analytics with VPC Flow Logs in Parquet format (AWS Blog)
- Reduce unexpected AWS costs: Tracing billing charges with log correlation techniques (AWS Blog)
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article