Elastic Network Interfaces (ENIs)
Every EC2 instance (a virtual server in AWS) talks to the network through a virtual network card called an Elastic Network Interface, or ENI. Just like a physical server has a real network card plugged into it, your instance has one or more ENIs that carry its IP addresses, its hardware address, and the firewall rules that protect it. The clever part is that an ENI is a separate, movable object: you can detach it from one instance and attach it to another, and everything it carries — the IPs, the MAC, the security groups — moves with it. Understanding ENIs is the key to understanding how AWS networking actually works under the hood.
What an ENI actually holds
An ENI lives inside one subnet (a slice of your VPC’s IP range) in one Availability Zone (AZ — a physically separate data center). It bundles together several things:
- A primary private IP address — a fixed, internal-only address from the subnet’s range that never changes for the life of the ENI.
- Optionally, one or more secondary private IP addresses from the same subnet.
- An optional public IPv4 address and/or one or more Elastic IPs (an Elastic IP is a permanent public IP address you own and can move around).
- A MAC address (Media Access Control address — the unique hardware-style identifier for the card). This stays the same even when you move the ENI, which is useful for software licensing that is locked to a MAC.
- One or more security groups (virtual firewalls that allow or deny traffic).
- A source/destination check flag (when turned off, the ENI can forward traffic for other addresses — needed for NAT or firewall appliances).
Every instance gets a primary ENI (called eth0) that is created with it and cannot be detached. Any extra ENIs you add are secondary ENIs, and those you can attach and detach freely.
When to use a secondary ENI (and when not to)
Most everyday instances need nothing more than their primary ENI. Reach for a secondary ENI only when you have a specific reason:
| Use case | Why an ENI helps |
|---|---|
| Management / admin network | Put a second ENI in a separate, locked-down subnet so admin SSH (Secure Shell) traffic is isolated from app traffic. |
| Failover / high availability | Keep a “floating” ENI with a fixed IP. If the active instance dies, attach that ENI to a standby instance and traffic follows the IP — no DNS change needed. |
| MAC-locked licensing | Some commercial software is licensed to a MAC address. Move the ENI and the license moves with it. |
| Network appliances | Firewalls, NAT instances, and routers need multiple interfaces in different subnets with source/destination check disabled. |
| Multiple secondary IPs | Host several TLS websites or containers on one instance, each on its own private IP. |
When NOT to. Do not use ENIs to “load balance” or to scale throughput across two interfaces on a normal app server — that adds routing complexity and rarely helps. Use an Elastic Load Balancer instead. Reserve multi-ENI designs for failover, isolation, and appliances.
Creating and attaching an ENI
Using the Console
- Open the EC2 console and choose Network Interfaces in the left menu (under Network & Security).
- Click Create network interface.
- Pick the Subnet (this fixes the AZ and the IP range).
- Leave the private IPv4 address on Auto-assign, or type one in the subnet’s range.
- Choose one or more Security groups.
- Click Create network interface.
- Select the new ENI, choose Actions -> Attach, and pick the target instance (it must be in the same AZ).
Using the AWS CLI
The same flow with AWS CLI v2: create the interface, then attach it to a running instance.
aws ec2 create-network-interface \
--subnet-id subnet-0a1b2c3d \
--groups sg-0a1b2c3d \
--description "app-secondary-eni"
Output:
{
"NetworkInterface": {
"NetworkInterfaceId": "eni-0a1b2c3d4e5f",
"SubnetId": "subnet-0a1b2c3d",
"VpcId": "vpc-0a1b2c3d",
"AvailabilityZone": "us-east-1a",
"MacAddress": "0a:1b:2c:3d:4e:5f",
"PrivateIpAddress": "10.0.1.42",
"Status": "available",
"Groups": [{ "GroupId": "sg-0a1b2c3d", "GroupName": "app-sg" }]
}
}
Now attach it to an instance as device index 1 (the primary is index 0):
aws ec2 attach-network-interface \
--network-interface-id eni-0a1b2c3d4e5f \
--instance-id i-0a1b2c3d4e5f \
--device-index 1
Output:
{
"AttachmentId": "eni-attach-0a1b2c3d4e5f"
}
To move the ENI to a standby instance, detach it and attach it again elsewhere:
aws ec2 detach-network-interface --attachment-id eni-attach-0a1b2c3d4e5f
You can also declare an ENI in infrastructure-as-code so it is repeatable:
Resources:
AppEni:
Type: AWS::EC2::NetworkInterface
Properties:
SubnetId: subnet-0a1b2c3d
GroupSet:
- sg-0a1b2c3d
Description: app-secondary-eni
The gotchas that bite people
Instance type caps the number of ENIs and IPs. AWS limits how many ENIs an instance can have and how many private IPs each ENI can hold, based on the instance type. A small t3.micro allows only 2 ENIs with 2 IPs each, while a large m6i.8xlarge allows 8 ENIs with many IPs each. If you plan to host many IPs, size the instance accordingly. Check the limit per type:
aws ec2 describe-instance-types \
--instance-types t3.micro \
--query "InstanceTypes[0].NetworkInfo.{ENIs:MaximumNetworkInterfaces,IPsPerENI:Ipv4AddressesPerInterface}"
Output:
{
"ENIs": 2,
"IPsPerENI": 2
}
ENIs are AZ-locked. An ENI belongs to one subnet, which sits in one AZ. You can only attach it to an instance in that same AZ. Your failover standby must therefore live in the same AZ as the floating ENI — for cross-AZ failover you need a different pattern (such as moving an Elastic IP or updating DNS).
Cost and IP-exhaustion gotcha. Many managed services silently create ENIs in your subnets and consume IP addresses. Each RDS database, each interface VPC endpoint, each Lambda function running inside your VPC, and each NAT gateway grabs one or more IPs. A
/28subnet has only 16 addresses, and AWS reserves 5 of them, leaving 11. A handful of Lambda functions plus an endpoint can exhaust that and cause new launches to fail with “insufficient free addresses.” Size subnets generously (a/24gives 251 usable IPs) and watch usage. ENIs themselves are free; you only pay for any attached Elastic IPs and the data transfer.
You can list every ENI in use, including the hidden ones created by services, to audit IP consumption:
aws ec2 describe-network-interfaces \
--filters "Name=subnet-id,Values=subnet-0a1b2c3d" \
--query "NetworkInterfaces[].{ID:NetworkInterfaceId,IP:PrivateIpAddress,Type:InterfaceType,Desc:Description}" \
--output table
Output:
-------------------------------------------------------------------------------
| DescribeNetworkInterfaces |
+-------------------+--------------+--------------+----------------------------+
| ID | IP | Type | Desc |
+-------------------+--------------+--------------+----------------------------+
| eni-0a1b2c3d4e5f | 10.0.1.42 | interface | app-secondary-eni |
| eni-1a2b3c4d5e6f | 10.0.1.18 | vpc_endpoint| VPC Endpoint Interface |
| eni-2a3b4c5d6e7f | 10.0.1.9 | lambda | AWS Lambda VPC ENI |
| eni-3a4b5c6d7e8f | 10.0.1.5 | interface | RDS network interface |
+-------------------+--------------+--------------+----------------------------+
Best practices
- Leave the primary ENI (
eth0) alone for normal instances and only add secondary ENIs when you have a concrete failover, isolation, or appliance need. - Size subnets with room to spare (
/24or larger for shared subnets) so service-created ENIs do not exhaust your address space. - Audit ENI usage regularly with
describe-network-interfacesto see how many IPs RDS, Lambda, and endpoints are quietly consuming. - Match instance type to the number of ENIs and IPs you actually need before launching, since you cannot exceed the per-type cap.
- For NAT or firewall appliances, remember to disable the source/destination check (
--source-dest-check '{"Value": false}') or traffic will be dropped. - Tag ENIs with a clear
Nameand owner so the floating/failover interfaces are easy to identify during an incident.