Why this matters
Since February 2024, AWS charges an hourly fee for every public IPv4 address: Elastic IPs (attached or not), auto-assigned public IPs on instances, and the public IPs behind internet-facing load balancers and NAT gateways. Check the current Amazon VPC pricing page for the rate in your region.
Idle Elastic IPs are pure waste. Auto-assigned public IPs on instances that sit behind a load balancer or never talk to the internet are usually waste too. This sheet finds both and stops new ones.
All commands run against one region. Add --region <code> or loop over regions (last section).
See what you are paying
In Cost Explorer, filter Usage type for PublicIPv4. The two usage types are PublicIPv4:InUseAddress and PublicIPv4:IdleAddress, prefixed by a region code (for example USE1-). From the CLI:
aws ce get-cost-and-usage \
--time-period Start=2026-10-01,End=2026-11-01 \
--granularity MONTHLY \
--metrics UnblendedCost UsageQuantity \
--group-by Type=DIMENSION,Key=USAGE_TYPE \
--query "ResultsByTime[].Groups[?contains(Keys[0], 'PublicIPv4')].[Keys[0],Metrics.UsageQuantity.Amount,Metrics.UnblendedCost.Amount]" \
--output table
UsageQuantity is address-hours. Divide by the hours in the month to get the average number of addresses. Cost Explorer runs from us-east-1 regardless of your default region.
For a per-resource view across regions, the console's VPC IP Address Manager > Public IP insights page breaks addresses down by type (EIP, EC2 auto-assigned, service-managed). It works on the IPAM Free Tier.
Elastic IPs
List all Elastic IPs and what they are attached to
aws ec2 describe-addresses \
--query "Addresses[].{IP:PublicIp,Alloc:AllocationId,Instance:InstanceId,ENI:NetworkInterfaceId,Name:Tags[?Key=='Name']|[0].Value}" \
--output table
Find idle (unassociated) Elastic IPs
aws ec2 describe-addresses \
--query "Addresses[?AssociationId==null].[PublicIp,AllocationId,Tags[?Key=='Name']|[0].Value]" \
--output text
Elastic IPs on stopped instances
A stopped instance still holds its Elastic IP and you are still charged for it.
for i in $(aws ec2 describe-addresses --query "Addresses[?InstanceId!=null].InstanceId" --output text); do
aws ec2 describe-instances --instance-ids "$i" \
--query "Reservations[].Instances[?State.Name=='stopped'].[InstanceId,Tags[?Key=='Name']|[0].Value]" \
--output text
done
Release an Elastic IP
Disassociate first if it is attached, then release:
aws ec2 disassociate-address --association-id eipassoc-0123456789abcdef0
aws ec2 release-address --allocation-id eipalloc-0123456789abcdef0
Warning
Before releasing, search for the address in DNS records, partner firewall allowlists, VPN configs and SaaS IP allowlists. A released IP goes back to the AWS pool. Anything that whitelisted it breaks, and you may not get it back.
Try to recover a released Elastic IP
If nobody else has allocated it yet, you can request the same address:
aws ec2 allocate-address --domain vpc --address 203.0.113.25
This fails if the address is now in use by another account. Treat it as a best-effort undo, not a safety net.
Tag Elastic IPs with an owner
Untagged EIPs are the ones nobody dares release. Tag them when you allocate:
aws ec2 create-tags --resources eipalloc-0123456789abcdef0 \
--tags Key=Owner,Value=payments-team Key=Purpose,Value=partner-allowlist
Auto-assigned public IPs on instances
Instances with a public IP that is not an Elastic IP
aws ec2 describe-instances \
--filters Name=instance-state-name,Values=running,stopped \
--query "Reservations[].Instances[?PublicIpAddress!=null].[InstanceId,PublicIpAddress,SubnetId,Tags[?Key=='Name']|[0].Value]" \
--output table
Compare the IPs against describe-addresses. Anything not in that list is auto-assigned.
All ENIs with a public IP, and who owns the IP
This catches load balancers, NAT gateways and other service-managed interfaces too:
aws ec2 describe-network-interfaces \
--query "NetworkInterfaces[?Association.PublicIp!=null].[NetworkInterfaceId,InterfaceType,Association.PublicIp,Association.IpOwnerId,Description]" \
--output table
IpOwnerId | InterfaceType / Description | Meaning | Action |
|---|---|---|---|
| your account ID | any | Elastic IP | Covered above |
amazon | interface, instance attached | Auto-assigned public IP | Remove if not needed |
amazon | Description starts ELB app/ or ELB net/ | Internet-facing ALB/NLB | Consolidate idle load balancers |
amazon or account | nat_gateway | NAT gateway EIP | Remove unused NAT gateways |
Remove an auto-assigned public IP from a running instance
You no longer need to stop or replace the instance. This needs a recent AWS CLI v2 version:
aws ec2 modify-network-interface-attribute \
--network-interface-id eni-0123456789abcdef0 \
--no-associate-public-ip-address
Caution
This cuts the instance off from the internet immediately if it has no NAT gateway route, and drops any SSH sessions that came in over the public IP. Make sure you can still reach it through Session Manager or EC2 Instance Connect Endpoint first.
Stop new public IPs appearing
Subnets that auto-assign public IPs
aws ec2 describe-subnets \
--filters Name=map-public-ip-on-launch,Values=true \
--query "Subnets[].[SubnetId,VpcId,CidrBlock,Tags[?Key=='Name']|[0].Value]" \
--output table
Turn it off on subnets where instances do not need a public address (instances behind an ALB, for example):
aws ec2 modify-subnet-attribute --subnet-id subnet-0123456789abcdef0 --no-map-public-ip-on-launch
Existing instances keep their addresses. Only new launches change.
Launch templates that force a public IP
aws ec2 describe-launch-template-versions \
--launch-template-id lt-0123456789abcdef0 --versions '$Latest' \
--query "LaunchTemplateVersions[].LaunchTemplateData.NetworkInterfaces[].AssociatePublicIpAddress"
true overrides the subnet setting. Create a new version with AssociatePublicIpAddress set to false and roll it through your Auto Scaling group.
Run it across all regions
for r in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do
n=$(aws ec2 describe-addresses --region "$r" \
--query "length(Addresses[?AssociationId==null])" --output text)
[ "$n" != "0" ] && echo "$r idle EIPs: $n"
done
Prevent it next time
- Make "no public IP" the default: subnets with
MapPublicIpOnLaunch=false, launch templates withAssociatePublicIpAddress=false, and public access only through load balancers. - Require an
Ownertag on Elastic IPs, enforced with a tag policy or AWS Config rule, so clean-up has someone to ask. - Enable the AWS Config managed rule
eip-attachedto flag unattached Elastic IPs continuously. - Reach instances through Session Manager or EC2 Instance Connect Endpoint instead of public IPs.
- Where clients support it, use IPv6 or dual-stack for outbound traffic and check whether an egress-only internet gateway fits your design.