EC2 to S3 data transfer within the same AWS region is free — AWS does not charge per-GB for moving data between EC2 and S3 in the same region. But most AWS accounts still pay for this traffic every month. The reason is a single architectural detail: the NAT Gateway in between.
The Exact Pricing
| Traffic path | S3 transfer charge | NAT Gateway charge | Total per GB |
|---|---|---|---|
| EC2 → S3, same region, via VPC Gateway Endpoint | $0.00 | $0.00 | $0.00 |
| EC2 → S3, same region, via NAT Gateway (private subnet, no endpoint) | $0.00 | $0.045/GB | $0.045/GB |
| EC2 → S3, cross-region | $0.09/GB | $0.00 | $0.09/GB |
| S3 → EC2, same region (download) | $0.00 | $0.00 | $0.00 |
| S3 → internet (download by end users) | $0.09/GB | N/A | $0.09/GB |
Source: AWS S3 pricing, AWS VPC pricing. NAT Gateway also charges $0.045/hour per AZ regardless of traffic volume — that's $32.40/month just for the gateway being on.
Why Private Subnet Traffic Goes Through NAT
When you create a private subnet with a NAT Gateway for outbound internet access, that NAT Gateway becomes the default route for all outbound traffic — including traffic to S3. S3 has public endpoints (s3.amazonaws.com), so the default route sends EC2-to-S3 traffic out through NAT, into the internet, and back to AWS S3. The traffic is charged as NAT data processing even though the source and destination are both inside AWS.
What This Costs at Scale
A data pipeline writing 10 TB to S3 per month through NAT:
An ML training job checkpointing 50 TB of model weights to S3 per month: $2,304 in NAT charges alone. The S3 transfer itself costs nothing — all of that bill is the NAT Gateway in the path.
The Three Scenarios
EC2 instances in a public subnet (with a public IP or Elastic IP) connect directly to S3 via the Internet Gateway. No NAT Gateway in the path. Transfer charge: $0.00 within the same region.
A VPC Gateway Endpoint routes S3 traffic privately inside AWS, bypassing the NAT Gateway. AWS does not charge for S3 or DynamoDB Gateway Endpoints. This is the correct architecture for private subnets that access S3.
The default setup for most private subnets. All outbound traffic — including S3 — routes through NAT. Every GB uploaded or downloaded is charged at $0.045. This is the most common avoidable cost pattern in AWS accounts.
How to Check If You Are Affected
Check whether your VPCs have an S3 Gateway Endpoint configured:
aws ec2 describe-vpc-endpoints \
--filters "Name=service-name,Values=com.amazonaws.us-east-1.s3" \
"Name=vpc-endpoint-type,Values=Gateway" \
--query 'VpcEndpoints[*].[VpcId,State,RouteTableIds]' \
--output table
If this returns no rows for a VPC that has EC2 instances in private subnets, that VPC is routing S3 traffic through NAT. Confirm by checking VPC Flow Logs for traffic from your EC2 private IPs to the S3 public IP range — or use Netway, which detects this pattern automatically across all VPCs in a single scan.
To see how much this is costing you, check NAT Gateway data processing in Cost Explorer:
aws ce get-cost-and-usage \
--time-period Start=2026-06-01,End=2026-06-26 \
--granularity MONTHLY \
--filter '{"Dimensions":{"Key":"USAGE_TYPE","Values":["USE1-NatGateway-Bytes"]}}' \
--metrics UnblendedCost BlendedCost UsageQuantity \
--output table
The Fix: Add an S3 VPC Gateway Endpoint
First, find your VPC ID and route table ID:
# Get VPC IDs
aws ec2 describe-vpcs \
--query 'Vpcs[*].[VpcId,Tags[?Key==`Name`].Value|[0],CidrBlock]' \
--output table
# Get route table for your private subnet
aws ec2 describe-route-tables \
--filters "Name=vpc-id,Values=vpc-xxxxxxxx" \
--query 'RouteTables[*].[RouteTableId,Tags[?Key==`Name`].Value|[0]]' \
--output table
Then create the endpoint:
aws ec2 create-vpc-endpoint \
--vpc-id vpc-xxxxxxxx \
--service-name com.amazonaws.us-east-1.s3 \
--route-table-ids rtb-xxxxxxxx
Once the endpoint is in place, EC2 instances in that subnet will route S3 traffic through the endpoint automatically — no code changes, no application restart. The route table entry added by the endpoint takes precedence over the NAT Gateway default route for S3 destinations.
Verify Traffic Is Now Using the Endpoint
# Describe the endpoint to confirm it's active and route tables are associated
aws ec2 describe-vpc-endpoints \
--filters "Name=service-name,Values=com.amazonaws.us-east-1.s3" \
--query 'VpcEndpoints[*].[VpcEndpointId,State,RouteTableIds]' \
--output table
# In VPC Flow Logs, S3 traffic via endpoint shows as destined to
# prefix list pl-xxxxxxxx (not a public IP range) — confirming private routing
Other Data Transfer Charges That Catch Teams Off Guard
| Scenario | Cost | Notes |
|---|---|---|
| EC2 cross-AZ (private subnets) | $0.01/GB each way | Applies to EC2, RDS, ElastiCache across AZs |
| S3 cross-region replication | $0.09/GB source region | Plus $0.01–$0.02/GB between most US regions |
| EC2 to internet (data transfer out) | $0.09/GB (first 10TB) | First 100 GB/month free across all services |
| Transit Gateway data processing | $0.02/GB | Per GB sent through TGW attachment |
| S3 → CloudFront (same region) | $0.00 | Free — use CloudFront to avoid S3 egress |
Detect This Automatically Across All Your VPCs
If you manage multiple VPCs or AWS accounts, checking each one manually doesn't scale. Netway deploys as a single Lambda function and scans your VPC Flow Logs to detect S3-via-NAT traffic automatically — showing you which instance, which VPC, the monthly cost, and the exact CLI command to fix it.