Find idle NAT gateways before they bill $33 a month

An idle AWS NAT gateway billing about 33 dollars a month while no traffic flows through it

Your AWS bill has a networking line item that charges the same whether traffic is pouring through it or your VPC has been asleep for a month. It is the NAT gateway. Each one costs about $33 a month just to exist, before a single byte moves. The public IP address it is required to carry takes that past $36. The ones quietly draining money are the idle gateways: the dev VPC nobody tore down, the second gateway you spun up for high availability in an Availability Zone that no longer runs anything. Here is how to find them by hand and shut them off without breaking a route.

Why an idle NAT gateway still costs $33 a month

A NAT gateway has three charges, and only one of them depends on traffic.

The first is the hourly charge. In us-east-1 a NAT gateway lists at $0.045 per hour. AWS bills every partial hour as a full one, so a gateway that exists for a month costs about $32.85 (730 hours times $0.045) no matter what it does. That charge runs while the gateway sits idle, exactly as it does while the gateway is busy.

The second is data processing, at $0.045 per gigabyte that passes through the gateway. Any traffic bound for the open internet also carries the standard $0.09 per GB egress rate, so a byte leaving your VPC through a NAT gateway costs about $0.135 all in. Data processing only bills when traffic actually flows, so an idle gateway dodges it. The hourly charge is the one that bleeds on a gateway nobody uses.

The third is the public IPv4 address, and it is easy to miss because it lands on a different line of the bill. A public NAT gateway must have an Elastic IP attached at creation, so there is no version of this you can opt out of. Since February 1, 2024 AWS charges $0.005 per hour for every public IPv4 address, whether it is attached to a service or sitting idle in your account. That is another $3.65 a month (730 hours times $0.005) riding along with every gateway you own.

Add the two hourly charges and a public NAT gateway costs about $36.50 a month to exist. The $33 figure is the gateway on its own. AWS's own NAT gateway pricing page names the hourly and data processing charges and stops there, so the address is easy to leave out of the math.

Now multiply. A common high-availability pattern puts one NAT gateway in each Availability Zone a workload spans. Three AZs mean three gateways, about $98.55 a month in gateway hours plus $10.95 in address charges. Call it $110 a month before any data moves. When the workload behind two of those zones gets torn down but the gateways stay, you are paying for standby capacity that routes nothing.

The trap: a busy gateway can waste as much as an idle one

A busy NAT gateway can waste as much as an idle one when it routes S3 and DynamoDB traffic that a free gateway VPC endpoint would carry. Hunting idle gateways catches the obvious waste. The subtler drain sits on the gateways that look busy.

A lot of NAT gateway traffic is not headed for the open internet at all. It is your instances talking to Amazon S3 and DynamoDB, two services that live inside AWS. Route that traffic through a NAT gateway and you pay the $0.045 per GB processing charge on bytes that never leave the Region.

The $0.09 egress rate does not apply to those bytes. S3 lists data transferred from a bucket to any AWS service in the same Region among the transfers it charges nothing for. DynamoDB says the same: it "does not charge for data transferred between DynamoDB and other AWS services within the same AWS Region". Cross-Region traffic is a different bill. So on same-Region S3 and DynamoDB traffic the gateway's own processing charge is the entire per-GB cost, which makes it the entire saving.

S3 and DynamoDB each support a gateway VPC endpoint, and the gateway endpoint is free. It adds an entry to your route table that sends S3 and DynamoDB traffic straight to the service over the AWS network, skipping the NAT gateway entirely. It costs nothing to run and nothing per gigabyte.

Put a number on it. An application pulling 1 TB of objects from S3 each month through a NAT gateway runs about $46 in data processing, which is 1024 GB times $0.045. The same traffic over a gateway endpoint costs nothing, because AWS states there are "no data processing or hourly charges for using Gateway Type VPC endpoints". Only S3 and DynamoDB offer gateway endpoints, so this is the first thing to check on any gateway carrying real volume before you assume that traffic has to be there.

To measure how much of a busy gateway's traffic is S3 or DynamoDB, turn on VPC Flow Logs for the subnet and group the destinations against the S3 and DynamoDB prefix lists from aws ec2 describe-managed-prefix-lists. Traffic that lands in those prefixes is what a free gateway endpoint takes off the gateway.

Find your idle gateways by hand

To spot an idle NAT gateway, you sum its CloudWatch BytesOutToDestination over 30 days and confirm it sent almost nothing. Start with the inventory. List every available NAT gateway in the region, with its VPC, subnet, and public IP.

aws ec2 describe-nat-gateways \
  --filter Name=state,Values=available \
  --query "NatGateways[].{id:NatGatewayId,vpc:VpcId,subnet:SubnetId,ip:NatGatewayAddresses[0].PublicIp}" \
  --output table

That is the list of gateways you pay the hourly charge on. NAT gateways are regional, so run this in every region you have ever used, since a forgotten gateway in a region you rarely open still bills every hour. Pass --region explicitly, or loop over the output of aws ec2 describe-regions --query "Regions[].RegionName" --output text.

Now ask each one whether it is actually moving traffic. The metric that answers this is BytesOutToDestination in the AWS/NATGateway CloudWatch namespace. AWS defines a value greater than zero as traffic going to the internet from clients behind the gateway. Sum it over a full 30 days.

aws cloudwatch get-metric-statistics \
  --namespace AWS/NATGateway \
  --metric-name BytesOutToDestination \
  --dimensions Name=NatGatewayId,Value=nat-0123456789abcdef0 \
  --start-time "$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
  --end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --period 86400 \
  --statistics Sum \
  --query "Datapoints[].Sum" \
  --output json | jq 'add // 0'

That prints the total bytes the gateway sent to the internet over the window. A NAT gateway that sends under 1 GB to the internet across 30 days is idle. Divide the byte total by 1073741824 to read it in gigabytes. The date -d form is GNU coreutils. On macOS, use date -u -v-30d +%Y-%m-%dT%H:%M:%SZ for the start time instead.

Confirm it with a second metric before you act. ActiveConnectionCount reports concurrent TCP connections through the gateway, and AWS defines a value of zero as no active connections. Read its maximum over the same window.

aws cloudwatch get-metric-statistics \
  --namespace AWS/NATGateway \
  --metric-name ActiveConnectionCount \
  --dimensions Name=NatGatewayId,Value=nat-0123456789abcdef0 \
  --start-time "$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
  --end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --period 86400 \
  --statistics Maximum \
  --query "Datapoints[].Maximum" \
  --output json | jq 'max // 0'

Here is the part that trips people. Measure a full billing cycle, not a quiet afternoon. A gateway can read as dead all week and then light up on the first of the month when a backup job or a batch export runs. Thirty days of near-zero BytesOutToDestination and a peak ActiveConnectionCount of zero is a gateway you can retire. A single low day is not enough to call it.

Delete an idle gateway without breaking routes

Deleting a NAT gateway is one call. The care goes into the route it serves and the Elastic IP attached to it.

First, find the route tables that send traffic to the gateway. Any subnet whose default route points at a gateway you are about to delete loses its path to the internet the moment it goes.

aws ec2 describe-route-tables \
  --filters Name=route.nat-gateway-id,Values=nat-0123456789abcdef0 \
  --query "RouteTables[].{rt:RouteTableId,subnets:Associations[].SubnetId}" \
  --output json

If that returns nothing, no subnet routes through the gateway, and it is safe to remove. If it returns a route table, decide what those subnets should do instead: route through a different gateway, or nothing at all when the workload is gone.

Note the Elastic IP before you delete, because you will want it for rollback. Grab the allocation ID.

aws ec2 describe-nat-gateways \
  --nat-gateway-ids nat-0123456789abcdef0 \
  --query "NatGateways[0].NatGatewayAddresses[0].AllocationId" \
  --output text

Now delete the gateway.

aws ec2 delete-nat-gateway --nat-gateway-id nat-0123456789abcdef0

The gateway moves to deleting, and the hourly charge stops once it reaches deleted. Deleting the gateway does not remove the route that pointed at it. That entry stays in the route table and flips to a blackhole state, so the subnet quietly drops internet-bound packets until you fix it. Repoint it with aws ec2 replace-route, or delete it with aws ec2 delete-route.

The Elastic IP is not removed with the gateway. It returns to your account as an unassociated address, which matters for two reasons. A NAT gateway takes a fresh public IP every time you create one, so keeping the allocation lets you rebuild at the same address. The second reason is that deleting the gateway does not stop the address charge. That IP was billing $0.005 an hour while it was attached, and it bills the same $0.005 an hour sitting unused in your account. Deleting the gateway saves you the $32.85. Releasing the address saves the other $3.65, so release it once you are sure you will not rebuild.

Rollback is a rebuild. If you cut the wrong gateway, create a new one in the same subnet with the allocation ID you saved, then repoint the route table at it.

aws ec2 create-nat-gateway \
  --subnet-id subnet-0123456789abcdef0 \
  --allocation-id eipalloc-0123456789abcdef0

aws ec2 create-route \
  --route-table-id rtb-0123456789abcdef0 \
  --destination-cidr-block 0.0.0.0/0 \
  --nat-gateway-id nat-0987654321fedcba0

Because you kept the Elastic IP, the rebuilt gateway comes back on the same public address, so anything that allowlisted that IP keeps working.

A quick decision tree

graph TD
  A[NAT gateway on the bill] --> B{Sum of BytesOutToDestination over 30 days}
  B -- Under ~1 GB --> C{Peak ActiveConnectionCount = 0?}
  B -- Well above 1 GB --> D{Traffic mostly S3 or DynamoDB?}
  C -- Yes --> E[Idle: delete it. Release the Elastic IP unless you plan to rebuild]
  C -- No --> F[A monthly job may use it. Recheck over a full cycle]
  D -- Yes --> G[Add a free gateway endpoint and cut the processing charge]
  D -- No --> H[Real internet egress: keep the gateway]

The same shape of waste, paying for capacity that sits idle, shows up all over a cloud bill.

Common questions

How much does an idle NAT gateway cost? About $36.50 a month in us-east-1, with no traffic at all. The gateway lists at $0.045 an hour, which works out to roughly $32.85 over a 730-hour month, and the public IPv4 address it is required to carry adds $0.005 an hour, or another $3.65. Both are hourly charges, so they bill for as long as the gateway exists.

Does a NAT gateway cost money if no traffic flows through it? Yes. A NAT gateway carries three charges, and only data processing depends on traffic. An idle gateway dodges the $0.045 per GB processing charge and still pays the hourly gateway rate and the hourly public IPv4 rate, which is where the roughly $36.50 a month comes from.

How do I know whether a NAT gateway is idle? Sum the CloudWatch BytesOutToDestination metric in the AWS/NATGateway namespace over 30 days. A gateway that sent under 1 GB to the internet across that window is idle. Confirm it against ActiveConnectionCount, where a peak of zero over the same window means nothing held a connection through it. Measure a full billing cycle, because a gateway can read as dead all week and then light up when a monthly backup or batch export runs.

What happens to my routes when I delete a NAT gateway? Deleting the gateway leaves the route that pointed at it in place. That entry stays in the route table and flips to a blackhole state, so the subnet quietly drops internet-bound packets until you repoint it with aws ec2 replace-route or remove it with aws ec2 delete-route. Find the affected tables first by filtering aws ec2 describe-route-tables on route.nat-gateway-id.

Does deleting a NAT gateway release its Elastic IP? No. The address returns to your account as an unassociated Elastic IP and keeps billing $0.005 an hour, the same rate it cost while attached. Deleting the gateway saves the $32.85, and releasing the address saves the remaining $3.65. Keep the allocation if you might rebuild, because a gateway recreated with the same allocation ID comes back on the same public address.

Can I cut NAT gateway costs without deleting the gateway? Yes, when the traffic is bound for Amazon S3 or DynamoDB. Both support a gateway VPC endpoint, which costs nothing to run and nothing per gigabyte, and it sends that traffic straight to the service over the AWS network instead of through the NAT gateway. An application pulling 1 TB from S3 each month runs about $46 in data processing through a gateway, against $0 over an endpoint. Same-Region S3 and DynamoDB traffic carries no internet egress charge on either path, so that processing charge is the whole difference. Only S3 and DynamoDB offer gateway endpoints.

Do I need to check every AWS region for idle gateways? Yes. NAT gateways are regional, so a forgotten gateway in a region you rarely open still bills every hour and never appears in the console view you usually look at. Run the describe-nat-gateways inventory with an explicit --region, or loop it over aws ec2 describe-regions --query "Regions[].RegionName" --output text.

How OhChimp approaches this

The idle check above finds the obvious gateways and says nothing about the busy ones carrying S3 and DynamoDB traffic that a free gateway endpoint would take off your bill.

OhChimp scans your AWS accounts, sums BytesOutToDestination over the window, and flags the gateways under the idle rate, alongside unattached Elastic IPs and load balancers with no healthy targets. Each one becomes a reviewable plan with a confidence score, a risk level, and rollback steps, and because the Elastic IP survives a delete, a flagged gateway can be rebuilt at the same address. You click apply, nothing moves until you do, and the saving is measured against your real AWS bill.

See how the AWS connection works on the AWS integration page.

All OhChimp posts