Skip to content

NAT Gateway vs NAT instance — the egress-cost cliff and when VPC endpoints flip the question

NAT Gateway is managed and multi-Gbps. NAT instance is cheaper at low scale. Neither is the answer if endpoints can take the traffic. Here is the decision frame and the numbers.

A scrappy startup runs a single t3.micro NAT instance for the development VPC and spends almost nothing on egress. The CTO reads about NAT Gateway, switches production to it for the HA story, and the next month’s bill arrives with a per-GB data processing line that is bigger than the EC2 fleet. The instinct is to switch back to NAT instances. That is the wrong reaction — the right one is to look at what the traffic actually is. Half of the per-GB charge is for S3 PUT/GET calls that should be going through a VPC gateway endpoint at zero data-processing cost. The choice between NAT Gateway and NAT instance is real, but the choice that flips the bill is whether a chunk of the traffic should be hitting an endpoint instead.

What’s happening

NAT Gateway is the managed AWS service. AWS runs it, scales it (up to 45 Gbps per gateway, or 100 Gbps in some regions), and gives you a 99.95% SLA per AZ. It is deployed per AZ, has a public Elastic IP, and forwards outbound IPv4 traffic from private subnets to the internet. You pay an hourly rate per NAT Gateway (constant, regardless of traffic) plus a per-GB charge on every byte that flows through it. The per-GB charge is what kills bills on egress-heavy workloads. For a workload pulling many tens of TB per month through NAT — apt-get update on every boot, container image pulls from public registries, S3 traffic that is not routed through a gateway endpoint — the per-GB line becomes the dominant cost line for the VPC.

NAT instance is a customer-managed EC2 instance running a NAT software stack (the AL2023 NAT AMI, or iptables masquerade on a plain Linux box). You pay the EC2 instance price (hourly) and the data transfer out of the VPC to the internet — there is no per-GB data-processing surcharge. A t3.small NAT instance saturates around 5 Gbps under good conditions. At low traffic volumes the NAT instance can be 10× cheaper than NAT Gateway in absolute dollars. At high traffic volumes the NAT instance saturates before the per-GB savings catch up.

The single point of failure on a NAT instance is real and well-known. A single t3.small running NAT is one stop/start away from a 5-minute VPC blackout. The mitigation is an Auto Scaling Group of size 1, behind a per-AZ NAT instance with a Route 53 failover or a script that flips the route table on health-check failure. The mitigation is operational work — somebody owns the AMI, the patching, the kernel updates, the security group, the source/destination check disabled. NAT Gateway has none of that work, which is why most teams pick it once the bill is not the top concern.

The third option is VPC endpoints. A gateway endpoint for S3 or DynamoDB is free — no hourly charge, no per-GB charge — and a route table edit sends traffic for those services directly to AWS without ever touching the NAT path. An interface endpoint for other AWS services has an hourly per-AZ charge and a per-GB charge that is lower than NAT Gateway’s. For workloads that talk a lot to AWS services from private subnets, the right move is endpoints first, NAT for whatever is left.

How to verify

The first measurement is “what is actually going through NAT?” Enable VPC Flow Logs on the private subnet for a representative period (24 hours is enough for a workload with a daily cycle) and look at the top destinations by byte count.

# In CloudWatch Logs Insights, on the flow logs log group:
fields @timestamp, srcAddr, dstAddr, bytes
| filter action = "ACCEPT" and srcAddr like /^10\.20\./ and not dstAddr like /^10\./
| stats sum(bytes) as total_bytes by dstAddr
| sort total_bytes desc
| limit 50

Resolve the top destination IPs with dig -x <ip> or AWS’s IP range data. A typical small-SaaS workload will show:

  • S3 bucket IPs (range s3.us-east-1.amazonaws.com) at the top, often 60% of traffic. These should be on a gateway endpoint at zero cost.
  • ECR image registry IPs (range api.ecr.us-east-1.amazonaws.com and pkg-containers.githubusercontent.com for GHCR pulls). ECR has an interface endpoint with a lower per-GB rate; GHCR can be put behind a registry mirror in your own ECR.
  • DynamoDB IPs at lower volume. Gateway endpoint, free.
  • Package manager IPs (deb.debian.org, packagecloud.io, npm, PyPI). These are real internet egress — they go through NAT no matter what. Mitigate with a private package mirror if the volume is high enough.
  • Third-party API IPs (Stripe, SendGrid, Twilio). Real internet egress, no endpoint exists.

The second measurement is the cost itself:

# Cost Explorer call for the last full month, scoped to NAT
aws ce get-cost-and-usage \
  --time-period Start=2026-04-01,End=2026-05-01 \
  --granularity MONTHLY \
  --metrics UnblendedCost UsageQuantity \
  --filter '{"Dimensions":{"Key":"USAGE_TYPE_GROUP","Values":["EC2: NAT Gateway"]}}'

Compare to the workload’s NAT-instance equivalent. The break-even point is roughly:

  • Below ~1 TB/month per VPC: NAT instance wins on cost, the single-AZ failure risk is the price.
  • 1–10 TB/month: NAT Gateway is the right default; engineering effort to maintain a NAT-instance ASG is not worth the savings.
  • Above 10 TB/month: NAT Gateway hourly is negligible against the data-processing cost. The leverage is on endpoint deployment, not on the NAT type.

The fix

The procedure for an existing VPC with a high NAT bill:

  1. Enable Flow Logs first. Without flow logs you are guessing. With them you have the bytes-by-destination histogram you need to decide.

  2. Add gateway endpoints for S3 and DynamoDB. Free, instant, and a one-line route table change. Edit the private route table to add a route pl-XXX → vpce-XXX (the prefix list is the S3 service prefix list ID). The route is more specific than 0.0.0.0/0, so it takes precedence. Verify with aws s3 cp from a private-subnet instance and check the flow log — the source IP should now be the instance’s private IP routed through the endpoint, not the NAT Gateway’s public IP.

  3. Add interface endpoints for ECR and the other high-volume AWS services. Interface endpoints are not free — there is an hourly per-AZ charge plus a per-GB charge — but for workloads pulling many GB of container images per day, the math works out. The pattern is: deploy three endpoints (one per AZ), enable private DNS, and the SDK keeps using the same *.amazonaws.com hostnames but resolves them to the endpoint IPs.

  4. Decide NAT Gateway vs NAT instance for what is left. If the remaining NAT traffic is below 1 TB per month per VPC and you have the operational appetite to run a NAT instance ASG, the NAT instance wins on cost. The setup is:

    # Launch a NAT instance from the AL2023 NAT AMI
    aws ec2 run-instances \
      --image-id $(aws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 --query Parameter.Value --output text) \
      --instance-type t3.small \
      --subnet-id subnet-public-1a \
      --security-group-ids sg-nat-instance \
      --user-data file://nat-init.sh
    
    # Disable source/destination check on the NAT instance ENI
    aws ec2 modify-instance-attribute \
      --instance-id i-xxx \
      --no-source-dest-check
    
    # Update the private route table to point default at the NAT instance
    aws ec2 replace-route \
      --route-table-id rtb-private-1a \
      --destination-cidr-block 0.0.0.0/0 \
      --instance-id i-xxx
  5. For production, default to NAT Gateway. The hours an engineer spends maintaining a NAT-instance ASG are worth more than the savings. The exception is development VPCs where the traffic is light and the cost matters.

The one mistake to not make: deploying NAT Gateway in one AZ and routing all private subnets to it. That is a single AZ failure away from a VPC-wide outage, and it adds cross-AZ data transfer charges on every byte from the other AZs. Per-AZ NAT Gateways are the correct default — the hourly cost of two extra NAT Gateways is small against the cross-AZ data transfer they save.

The other rule we hold: NAT cost is a symptom, not a cause. A VPC pulling 50 TB/month through NAT has a workload that is doing too much over the public path. The fix is to look at what is generating that traffic — usually container image pulls and package manager updates that should be cached in a private mirror — and reduce the volume, not to switch NAT types and call it a day.

For the endpoint deployment in detail, see VPC endpoints for S3, DynamoDB, and interface. For the subnet design that puts NAT in the right place, see Multi-AZ subnet design.


Stack Harbor designs and audits production VPC topologies as part of Managed AWS and ongoing Managed Cloud Ops. If your egress bill has gotten away from your VPC plan, start with an infrastructure consult.