Stop Paying NAT Gateway Fees for S3 Traffic: A Practical Guide to VPC Gateway Endpoints
Private EC2 instances talking to S3 through a NAT Gateway quietly burn money on every gigabyte. Learn how VPC Gateway Endpoints fix this for free, with a hands-on setup, security hardening, and the gotchas.
The problem: a surprise line on your AWS bill
You build a clean architecture: EC2 instances or containers in private subnets, a NAT Gateway so they can reach the outside world. Your app reads and writes files in S3. Everything works, so you move on.
Then the bill arrives, and the NAT Gateway is one of the top line items.
Here is why. When a private instance calls S3 and there is no better route, the traffic goes:
Instance → NAT Gateway → Internet Gateway → S3 public endpointThe NAT Gateway charges per hour and per GB processed. Every byte your app sends to or from S3 gets counted, even though S3 is an AWS service in the same region. You are paying a toll on a road that did not need to exist.
Prices vary by region and change over time, so check the current VPC pricing page. The pattern is what matters: NAT Gateway data processing is billed per GB, and Gateway Endpoints for S3 have no hourly or data processing charge.
The fix: VPC Gateway Endpoints
A Gateway Endpoint gives your VPC a private route to S3 or DynamoDB (these are the only two services that support gateway endpoints).
Think of it like this: instead of leaving your building, walking onto the public street and entering the bank through the front door, you get a private corridor from your office straight into the bank's vault room.
The new path is:
Instance → Gateway Endpoint → S3- No NAT Gateway in the path
- No Internet Gateway involved
- Traffic stays on the AWS network
- No hourly or data processing charge for the endpoint itself
How it works under the hood
A gateway endpoint is not a network interface or a box in your subnet. It works through your route tables.
When you create one, AWS adds a route to the route tables you select:
| Destination | Target |
|---|---|
pl-xxxxxxxx (S3 managed prefix list) | vpce-xxxxxxxx (your endpoint) |
A prefix list is a managed list of the IP ranges S3 uses in that region. AWS keeps it updated, so you never maintain IP ranges by hand.
Routing picks the most specific matching route. The S3 prefix list is more specific than your default
0.0.0.0/0 → NAT route, so S3 traffic takes the endpoint automatically. Your code does not change. You still call s3.amazonaws.com or the regional S3 endpoint as usual.If you have written code with a router or middleware chain, this is the same idea: a more specific rule wins over the catch-all.
Step-by-step setup
Option 1: AWS Console
- Open VPC → Endpoints → Create endpoint
- Service category: AWS services
- Pick the service named
com.amazonaws.<region>.s3with type Gateway - Select your VPC
- Select the route tables used by your private subnets
- Keep the policy as Full access for now (we harden it below)
- Create
Option 2: AWS CLI
1
2
3
4
5
aws ec2 create-vpc-endpoint \
--vpc-id vpc-0abc1234 \
--vpc-endpoint-type Gateway \
--service-name com.amazonaws.ap-south-1.s3 \
--route-table-ids rtb-0abc1234 rtb-0def5678Option 3: Terraform
1
2
3
4
5
6
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.ap-south-1.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [aws_route_table.private.id]
}Verify it works
Check that the route exists:
1
aws ec2 describe-route-tables --route-table-ids rtb-0abc1234Look for a route whose target starts with
vpce-. From a private instance, aws s3 ls should work with no NAT in the path.Security: lock it down
Cost savings are nice, but gateway endpoints also improve security when you add two policies.
1. Endpoint policy
This controls what can be accessed through the endpoint. For example, allow only your own bucket:
1
2
3
4
5
6
7
8
9
10
11
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-app-bucket/*"
}
]
}This helps prevent data exfiltration to someone else's bucket from inside your VPC.
2. Bucket policy with aws:SourceVpce
This works in the other direction: the bucket only accepts requests that come through your endpoint.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowOnlyFromMyEndpoint",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-app-bucket",
"arn:aws:s3:::my-app-bucket/*"
],
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-0abc1234"
}
}
}
]
}Warning: a blanket deny like this also blocks you from the console and from other networks, including your own admin access. Test on a non-critical bucket first and make sure you keep a safe way in (for example, an extra condition for an admin role).
Gateway vs Interface endpoints
This comparison is a classic exam topic.
| Gateway endpoint | Interface endpoint (PrivateLink) | |
|---|---|---|
| Services | S3 and DynamoDB only | Most AWS services, plus partner and your own services |
| How it works | Route table entry | ENI with a private IP in your subnet |
| Cost | No endpoint charge | Hourly per AZ, plus per GB |
| Reachable from on-premises (VPN or Direct Connect) | No | Yes |
| Security groups | Not supported | Supported |
Interface endpoints for S3 also exist. They are useful when you need access from on-premises or from another region's network via peering, but you pay for them. If your traffic originates inside the VPC, the gateway endpoint is usually the better choice.
Gotchas to know before you ship
- Same-region only. A gateway endpoint covers buckets in the same region as the endpoint. Cross-region S3 access will not use it.
- You must update route tables. The endpoint only affects the route tables you associate it with. A forgotten private subnet keeps using the NAT.
- Not reachable from on-premises. Traffic from a VPN or Direct Connect connection cannot use a gateway endpoint. Use an interface endpoint in that case.
- DNS and bucket policies can surprise you. If a bucket policy blocks non-endpoint traffic, anything outside the path (CI runners, laptops) loses access.
- Other services still need NAT. The endpoint only helps S3 and DynamoDB traffic. Calls to third-party APIs or non-supported AWS services still go through NAT or interface endpoints.
How to see how much you will save
Before and after changing anything, compare:
- Cost Explorer, filtered to the NAT Gateway usage type (look for
NatGateway-Bytes) - CloudWatch metrics on the NAT Gateway, such as
BytesOutToDestination - VPC Flow Logs, to find which destinations generate the most NAT traffic
If large S3 or DynamoDB transfers show up, a gateway endpoint will cut that part of the bill. It takes a few minutes to set up.
Key takeaways
- NAT Gateways bill per GB, and S3 traffic through one adds up fast
- Gateway endpoints route S3 and DynamoDB traffic privately through route table entries
- The most specific route wins, which is why no application code changes are needed
- Add endpoint policies and
aws:SourceVpcebucket conditions for real security - Know the gateway vs interface differences, since they come up in the Solutions Architect exam
Have you found a surprising NAT Gateway bill in your own account? Share what you found in the comments.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article