EKS Networking Model: VPC CNI, ENI Limits, and Pod IP Planning
In EKS, every pod gets a real VPC IP address, not a virtual overlay address. This is the Amazon VPC CNI model, and it changes how you plan IP space, choose instance types, control network access, and troubleshoot connectivity. Understanding it before you scale is the difference between a smooth cluster and one that breaks in confusing ways.
What this means in plain terms
In most Kubernetes setups, pods get private addresses that only exist inside the cluster. To talk to something outside, traffic must be translated or tunnelled. In EKS, there is no private overlay. Every pod gets an IP from your VPC subnet, the same subnet your EC2 instances and RDS databases use. An RDS database receiving a connection from a pod sees the pod’s actual IP, not the node’s IP. This simplifies security group rules, makes VPC Flow Logs useful, and removes the need for NAT within the VPC. The trade-off is that pods consume real IP addresses — potentially thousands of them in a large cluster.
How EKS networking works
When a pod is scheduled onto a node, the Amazon VPC CNI plugin runs on that node and assigns the pod an IP address from the VPC subnet. That IP comes from one of the secondary IP slots on an Elastic Network Interface (ENI) attached to the EC2 instance.
The CNI plugin includes a daemon called ipamd (IP Address Management Daemon), which runs as part of the aws-node DaemonSet on every node. Rather than allocating IPs on demand when pods start, ipamd maintains a warm pool of pre-allocated IPs ready to hand to new pods immediately. It watches for available ENI slots and fills them proactively, which keeps pod startup times fast.
Think of ipamd like a bank teller who keeps a drawer of cash on hand rather than fetching bills from the vault for every transaction. When a pod starts, ipamd hands over a pre-allocated IP from the drawer. When the drawer runs low, it quietly refills from the ENI in the background. This is why pods start quickly even though IP allocation involves AWS API calls.
When a pod is deleted, its IP is returned to the warm pool and reused by the next pod on that node.
Primary ENI vs secondary ENIs#
Each EC2 instance has one primary ENI that holds the node’s main IP address — the IP Kubernetes uses to identify the node. The VPC CNI attaches secondary ENIs to the instance and uses the secondary IP slots on those ENIs for pod IPs. The primary IP of each ENI is reserved, which is why the pod capacity formula subtracts one IP per ENI.
Pod networking is pure VPC routing. There is no encapsulation layer, no VXLAN tunnel, and no virtual bridge to manage or debug.
Why EKS uses VPC CNI instead of an overlay network
Most self-managed Kubernetes distributions use an overlay network (Flannel, Weave, Calico in encapsulation mode). An overlay gives pods private addresses invisible to the underlying network. Traffic between pods on different nodes is wrapped in a packet the physical network can route, then unwrapped at the destination.
An overlay network is like a company’s internal phone extension system: you dial extension 42 and the switchboard routes it, but anyone outside the building cannot dial extension 42 directly. VPC CNI is like giving every person their own direct phone number. Anyone in the same network can call them directly — your RDS database, your ElastiCache cluster, your security team’s flow logs — with no switchboard in the middle.
EKS chose the native VPC approach deliberately:
- Pods can reach RDS, ElastiCache, MSK, and other services using security group rules, exactly like EC2 instances — no proxy needed.
- VPC Flow Logs show the pod’s actual IP, making security investigations and compliance audits straightforward.
- No encapsulation means lower latency and less CPU overhead per packet.
- AWS security groups can be applied at the pod level with Security Groups for Pods (covered below).
The cost is IP address consumption. Every pod uses a real VPC IP, which means large clusters need large subnets.
| Characteristic | VPC CNI (EKS default) | Overlay networking |
|---|---|---|
| Pod IP visibility | Real VPC IPs, visible to all VPC resources | Internal cluster IPs only, invisible outside the cluster |
| Operational complexity | Lower — no overlay layer to manage | Higher — must understand both overlay and physical network |
| Packet latency | Native VPC performance, no encapsulation | Small encapsulation overhead per packet |
| IP address consumption | High — one real VPC IP per pod | Low — only node IPs use VPC addresses |
| AWS security group integration | Direct — pods can be security group members | Indirect — requires additional tooling or proxies |
| Troubleshooting | VPC Flow Logs show pod IPs directly | Must correlate tunnel or node IPs to identify pods |
ENI limits and pod density
Pod count on a node is not only limited by CPU and memory. It is also hard-capped by the instance’s ENI and IP limits. The VPC CNI uses this formula:
Max pods = (Number of ENIs x (IPs per ENI - 1)) + 2The -1 per ENI reserves one IP for the ENI itself. The +2 accounts for the kubelet and the node networking daemon.
The following figures are examples based on current AWS documentation. Verify against the EC2 ENI documentation for your instance type before making capacity decisions, as new instance types are added regularly.
| Instance type | Max ENIs | IPs per ENI | Max pods (secondary IP mode) |
|---|---|---|---|
| t3.small | 3 | 4 | 11 |
| t3.medium | 3 | 6 | 17 |
| m5.large | 3 | 10 | 29 |
| m5.xlarge | 4 | 15 | 58 |
| m5.2xlarge | 4 | 15 | 58 |
| m5.4xlarge | 8 | 30 | 234 |
Two things are worth paying close attention to:
- Doubling CPU does not double pod capacity. An
m5.xlargeand anm5.2xlargeboth cap at 58 pods because they share the same ENI configuration. If you need more pods per node, choose an instance with more ENIs, not just more vCPU. - Small instances have surprisingly low pod limits. A
t3.mediumcan run at most 17 pods regardless of how much CPU and memory sits unused.
Pods stuck in Pending with the error failed to assign an IP address to container usually means the node has exhausted its IP pool, not that the node is out of CPU or memory. This catches teams off guard. Check the node’s ENI-based pod limit when sizing your node groups.
IP address planning and subnet sizing
Because every pod consumes a VPC IP, the subnets for your EKS cluster need to accommodate both nodes and pods. Each node uses one IP, each pod uses one more, and ipamd holds a warm buffer above current demand.
A cluster with 50 nodes running 30 pods each needs at minimum 1,550 IPs before the warm pool overhead. Size your subnets for at least 2x the addresses you expect at peak.
| Cluster size | Approximate nodes | Recommended subnet per AZ | IPs available |
|---|---|---|---|
| Small | Up to 20 | /24 | 254 |
| Medium | 20 to 100 | /22 | 1,022 |
| Large | 100+ | /21 | 2,046 |
VPC CIDR ranges and subnet CIDRs cannot be changed after creation without significant disruption. Design your IP address space before running eksctl create cluster or applying Terraform. See subnets in AWS for sizing guidance and VPC networks explained for how to structure CIDR blocks across availability zones.
Capacity planning checklist#
Before provisioning a cluster, work through these questions:
- How many nodes will the cluster run at peak? What is the 2x growth target?
- What is the target pod density per node? Check the ENI limits for your instance type.
- Are you deploying across three AZs? Each AZ needs its own subnet sized for the full peak node count.
- Does ipamd’s warm pool fit in the subnet? By default, ipamd pre-allocates a buffer of IPs above current demand.
- Do other workloads share these subnets? Pod IPs and EC2 instance IPs come from the same pool.
- Are you using secondary CIDR blocks to extend address space? These must be attached to the VPC before cluster creation.
Prefix delegation and higher pod density
When secondary IP mode is too constrained, the VPC CNI supports prefix delegation on Nitro-based instance types. Instead of assigning individual IP addresses to ENI slots, prefix delegation assigns entire /28 CIDR blocks (16 IPs each) to every slot. A single ENI slot that previously held one pod IP can now hold 16, substantially raising how many pods can run per node.
Enable it by setting an environment variable on the aws-node DaemonSet:
kubectl set env daemonset aws-node -n kube-system ENABLE_PREFIX_DELEGATION=trueAfter enabling prefix delegation, update the --max-pods kubelet argument for new nodes. The default kubelet limit does not update automatically; new nodes will still cap at the old secondary IP mode value unless the bootstrap configuration is updated through the node group launch template or managed node group configuration.
Prefix delegation increases pod capacity but reserves more IP space per node upfront. A node running only a few pods still pre-allocates a full /28 block per warm ENI slot. Make sure your subnets have enough IPs to support this reserved space before enabling prefix delegation across a large node group.
Security Groups for Pods
By default, security group rules apply at the node level. All pods on a node share the node’s security group. If any pod on a node needs to reach an RDS instance on port 5432, the entire node has that rule open — which applies to every pod on that node.
Security Groups for Pods (SGP) lets you assign AWS security groups to individual pods rather than to the node. This makes pod-level network segmentation possible without running separate node groups for each pod type.
How SGP works#
SGP relies on trunk and branch ENIs. The VPC Resource Controller (part of EKS) reserves one ENI on each supporting node as a trunk ENI. When a pod needs SGP, the controller attaches a dedicated branch ENI to the trunk, assigns the pod an IP from that branch ENI, and applies the security groups defined in the pod’s SecurityGroupPolicy resource to that branch ENI.
Think of the trunk ENI as a power strip plugged into the wall. Each branch ENI is a device plugged into the strip. Each device has its own power controls (security groups), completely separate from the others. The trunk handles the physical connection; the branches handle the individual rules.
This is distinct from standard VPC CNI secondary IP mode. SGP pods use a separate ENI chain managed by the resource controller, not the main ipamd warm pool.
Enabling SGP#
SGP requires Nitro-based instance types and ENABLE_POD_ENI=true on the aws-node DaemonSet:
kubectl set env daemonset aws-node -n kube-system ENABLE_POD_ENI=trueAssigning security groups with SecurityGroupPolicy#
The correct way to assign security groups to pods is the SecurityGroupPolicy Custom Resource Definition (CRD), installed automatically by the VPC Resource Controller on supported clusters:
apiVersion: vpcresources.k8s.aws/v1beta1
kind: SecurityGroupPolicy
metadata:
name: allow-db-access
namespace: production
spec:
podSelector:
matchLabels:
app: payment-processor
securityGroups:
groupIds:
- sg-0123456789abcdef0Any pod in the production namespace with the label app: payment-processor will be assigned security group sg-0123456789abcdef0. You can also use serviceAccountSelector instead of podSelector, which is useful in IRSA-based architectures where pod identity is already tied to service accounts.
Important caveats#
SGP changes how the VPC CNI assigns IPs to some pods and introduces additional ENI overhead per node. Read these caveats before enabling it on a live cluster.
Supported instances only. SGP requires Nitro-based instances. Verify all node groups in your cluster use compatible instance types before setting ENABLE_POD_ENI=true. Mixed node groups with unsupported instances can cause scheduling errors.
Pod startup latency. Attaching a branch ENI to a trunk takes a few seconds. Pods using SGP start slightly slower than standard pods. For workloads with aggressive scale-out requirements, factor this overhead into your design.
Private subnet behaviour. SGP works correctly in private subnets. For pods in public subnets needing outbound internet access, the branch ENI also needs a route to the internet, typically handled by a NAT gateway.
NodePort and external load balancer traffic. Pods using SGP receive inbound traffic through their branch ENI, which can cause asymmetric routing issues with NodePort services and some LoadBalancer configurations. Test this carefully if your SGP pods receive external traffic through NodePort.
Kubelet health probes. Kubernetes liveness and readiness probes originate from the node, not from inside the pod. Make sure the node’s security group allows the kubelet to reach the pod’s health check port, independent of any SGP rules restricting the branch ENI.
Reserved trunk ENI capacity. Each node reserves one ENI slot for the trunk ENI, slightly reducing total secondary IP capacity available to standard pods on that node.
Security Groups for Pods vs Kubernetes NetworkPolicy
These two features address related but distinct problems. Using one does not replace the other.
| Dimension | Security Groups for Pods | Kubernetes NetworkPolicy |
|---|---|---|
| Enforcement layer | AWS VPC networking (ENI-level) | In-cluster CNI (Amazon Network Policy Controller, Calico, etc.) |
| Controls access to AWS services | Yes — works natively with RDS, ElastiCache, MSK | No — requires additional tooling |
| Pod-to-pod within cluster | Yes, using SG-to-SG rules | Yes, using namespace and pod label selectors |
| Default deny capability | No — SG rules are additive | Yes — empty podSelector creates an implicit deny |
| Requires specific instance types | Yes — Nitro instances only | No — any node type |
Use Security Groups for Pods to control access between pods and AWS services. Use Kubernetes NetworkPolicy to enforce east-west traffic controls within the cluster. The securing EKS clusters guide covers how these fit together into a full security posture.
Pod-to-pod communication and cross-AZ traffic
Because pods have real VPC IPs, pod-to-pod traffic is standard VPC routing with no overlay to traverse:
- Same node: Traffic goes through the local Linux network namespace. No physical network involved.
- Same AZ, different nodes: Standard VPC routing within the AZ. Fast and free.
- Different AZs: Standard VPC routing across AZs. Each GB of data transfer between AZs costs approximately $0.01. For high-throughput workloads like streaming pipelines or real-time analytics, this adds up.
Use Kubernetes topology spread constraints to colocate related pods within the same AZ when latency or cost matters. For Kubernetes services that should prefer local traffic, configure topology-aware routing on the service.
When this model works well, and when it becomes limiting
Good fit#
AWS-native workloads. If pods need to reach RDS, ElastiCache, MSK, or other VPC resources using security groups, the VPC CNI model is a natural match. Clean security group rules, no translation layer.
Teams that need VPC-level visibility. VPC Flow Logs capture real pod IPs. This makes it straightforward to investigate access patterns, connectivity issues, and compliance audit requirements.
Organisations already familiar with AWS networking. If your team understands VPC subnets, ENIs, and security groups, operating the VPC CNI model requires no new mental model.
Well-planned clusters with adequate IP space. If you pre-sized your subnets correctly, the model is operationally simple. No overlay to debug, no encapsulation to reason about.
When it becomes painful#
IP-constrained VPCs. If your organisation has used a large portion of its RFC 1918 address space, carving out large subnets for pods can be difficult. You may need secondary CIDRs or custom networking mode, both of which add complexity.
Very high pod density on small instances. Small and medium instance types have ENI ceilings that prefix delegation can raise but not eliminate entirely. See the managed node groups guide for instance type selection guidance.
Multi-tenant clusters without careful design. The default shared-node model means tenants share a security group. Using SGP for per-tenant isolation requires SecurityGroupPolicy resources per tenant and careful testing of the caveats above.
Environments limited to non-Nitro instances. Prefix delegation and Security Groups for Pods both require Nitro. Non-Nitro instances lock you out of both features, limiting pod density and granular security controls.
If you are hitting IP exhaustion, the quickest short-term fix is enabling prefix delegation on Nitro instances. The long-term fix is adding a secondary VPC CIDR and creating new subnets for your node groups. Both changes require planning; neither can be applied retroactively to running nodes without rolling the node group.
Common mistakes
- Undersizing VPC subnets before creating the cluster. A /24 subnet has 256 IPs. With 20 nodes running 10 pods each, plus ipamd’s warm pool overhead, that fills up quickly. Size subnets at least 2x your expected peak. See the subnet sizing guidance for how to calculate this.
- Choosing small instance types for pod-dense workloads. A
t3.mediumcaps at 17 pods regardless of CPU and memory availability. Check the ENI limits for your instance type before committing to it for a workload with many small pods. - Assuming pod-level security groups without enabling SGP. By default, security group rules apply to the node. Individual pods only get their own security group enforcement after SGP is enabled and a
SecurityGroupPolicyresource is created. - Enabling SGP without checking instance compatibility. Setting
ENABLE_POD_ENI=trueon a node group that includes non-Nitro instances causes errors and prevents pod scheduling on those nodes. - Ignoring cross-AZ traffic costs. Spreading pods across three AZs without considering which pods communicate frequently can result in unexpected data transfer costs. Use topology spread constraints to colocate pods that communicate at high volume.
- Not monitoring subnet IP exhaustion. EKS does not alert you when subnets run low on IPs. Monitor the
AvailableIpAddressCountmetric in CloudWatch for your EKS subnets and set an alert at 20% remaining capacity, not 0%.
Troubleshooting checklist
Pods stuck in Pending
- Run
kubectl describe pod <name>and look forfailed to assign an IP address to container - Check available IPs in the node’s subnet via CloudWatch (
AvailableIpAddressCount) or the VPC console - Compare the node’s current pod count against its ENI-based max pods limit
- Check if ipamd is healthy:
kubectl logs -n kube-system -l k8s-app=aws-node
Wrong max-pods value after enabling prefix delegation
- The kubelet
--max-podsargument must be updated for new nodes; it does not change automatically - Check the current allocatable pods:
kubectl get node <name> -o jsonpath='{.status.allocatable.pods}'
Subnet CIDR exhausted
- Check
AvailableIpAddressCountacross all AZ subnets in CloudWatch - Consider adding a secondary VPC CIDR and new subnets, then migrating node groups to the new subnets
Security Groups for Pods not applying
- Verify
ENABLE_POD_ENI=trueis set on theaws-nodeDaemonSet - Confirm the node instance type is Nitro-based
- Check that a
SecurityGroupPolicyresource exists in the correct namespace with labels matching the pod - Check VPC Resource Controller logs:
kubectl logs -n kube-system -l app=vpc-resource-controller
Cross-AZ traffic surprises
- Run
kubectl get pods -o wideto confirm which nodes pods are running on and which AZ those nodes are in - Enable VPC Flow Logs and filter by AZ to confirm where traffic is actually flowing
Pod-to-AWS-service traffic not working
- Confirm the pod’s IP belongs to the correct subnet CIDR so security group rules apply as expected
- Check that the target service’s security group allows inbound traffic from the pod’s security group or IP range
- For SGP pods, verify the branch ENI’s security group has the required egress rule
For general VPC connectivity issues, see the troubleshooting network issues guide. For cluster-level monitoring and alerting, see monitoring EKS clusters.
Summary
- EKS uses the Amazon VPC CNI plugin. Every pod gets a real VPC IP address, with no overlay network.
- ipamd maintains a warm pool of pre-allocated IPs on each node using secondary ENIs attached to the EC2 instance.
- Pod density per node is hard-capped by ENI limits, not just CPU and memory. Check instance types before provisioning.
- Large clusters consume thousands of VPC IPs. Size subnets at 2x expected peak before creating the cluster.
- Prefix delegation assigns /28 blocks instead of single IPs, dramatically increasing pod capacity on Nitro instances.
- Security Groups for Pods uses trunk and branch ENIs to enable per-pod AWS security group assignment, but has important caveats around instance types, startup latency, and external traffic.
- Security Groups for Pods and Kubernetes NetworkPolicy solve different problems and can be used together.
Frequently asked questions
Does EKS use overlay networking?
No. EKS uses the Amazon VPC CNI plugin by default, which assigns real VPC IP addresses directly to pods. There is no overlay network — pod traffic travels over standard VPC routing, the same as traffic between EC2 instances.
Why does every pod in EKS need its own VPC IP address?
The VPC CNI design makes pods first-class VPC citizens. This lets pods communicate directly with RDS, ElastiCache, and other services using security group rules, and makes VPC Flow Logs show actual pod IPs. The trade-off is that large clusters consume many IP addresses from your subnets.
What limits the number of pods on an EKS node?
Pod count is hard-capped by the instance type ENI and IP limits, not only CPU or memory. The formula is (ENIs x (IPs per ENI - 1)) + 2. A t3.medium caps at 17 pods; an m5.xlarge caps at 58. If you need higher density, use prefix delegation on Nitro instances or choose instance types with more ENIs.
When should I use Security Groups for Pods?
Use Security Groups for Pods when you need AWS-native network controls at the pod level — for example, allowing only specific pods to reach an RDS instance, without opening that access to every pod on the node. Evaluate the caveats (startup latency, Nitro-only requirement, NodePort behaviour) before enabling it in production.
Is Security Groups for Pods the same as Kubernetes NetworkPolicy?
No. Security Groups for Pods attaches AWS security groups to pod ENIs at the VPC layer. Kubernetes NetworkPolicy controls pod-to-pod traffic inside the cluster using pod and namespace selectors, enforced by the CNI. Both can be active at the same time and address different parts of your security model.