Skip to content

NAT (Network Address Translation)

The DHCP lease group closed on a question it deliberately left open: getting one device an address solves half the problem, and the other half is how many devices behind that one address can actually share it. A typical 192.168.1.0/24 DHCP pool can hand out up to 254 addresses, and every one of those devices reaches the internet through a single public IP the ISP gave the router. NAT is the service that makes that sharing possible, and it's worth looking at here from a planning angle — as something you provision and size for, rather than as the packet-rewriting mechanism itself, which the device-level NAT article already covers in full: the NAPT translation table, conntrack, cone types, CGNAT, and hairpinning. If any of those terms are unfamiliar, that article is the one to read first — this one assumes them and asks a different question: how does an organization plan its address space and its NAT deployment so it doesn't run out of room or break under load?

Why a DHCP pool and a NAT table are the same sizing problem

A /24 DHCP pool can lease up to roughly 254 private addresses. None of those addresses are globally routable, so every one of those 254 devices that wants to reach the internet does it through the same NAT table on the same router, translating to the same one or handful of public addresses. The DHCP pool caps how many devices can exist on the network at once; the NAT table caps how many simultaneous connections those devices can have translated at once — and the second number runs out first in practice, because a single device rarely opens just one connection. A browser with a dozen open tabs, each pulling in scripts, images, and ads from different origins, can easily hold fifty or more simultaneous TCP connections open at once.

The table itself isn't infinite. Consumer routers and small business firewalls typically cap concurrent NAT/conntrack entries somewhere in the low tens of thousands, and each entry also consumes one of the roughly 64,512 usable ephemeral ports on the router's public-facing side (RFC 6335 sets the dynamic/private range at 49152–65535, though real routers often use a wider slice of the port space for translation). On a busy household or small office, that's rarely a problem. On a large one — a hotel, a university dorm building, a corporate NAT gateway serving thousands of employees behind one address — it becomes a real capacity constraint, and the fix is the same principle either sizing problem always comes down to: either shrink demand per client, or increase the pool of public addresses doing the translating.

Symptom of an exhausted NAT table

New outbound connections start failing or timing out while existing ones keep working fine — because there's no free entry left in the table to hold a new translation, but nothing evicts the connections already using one. This is a different failure from DHCP pool exhaustion, which stops new devices from joining the network at all; NAT table exhaustion stops existing devices from opening new connections. Distinguishing the two starts with conntrack -C on a Linux gateway, which reports the current entry count against the configured maximum.

Scaling NAT past one router: address pools and load-balanced translation

A large network — a corporate office, a cloud NAT gateway serving an entire VPC — doesn't rely on a single public IP the way a home router does. It's provisioned with a pool of public addresses, and outbound connections get spread across that pool rather than funneled through one address's port range. AWS's managed NAT Gateway, for example, supports up to 55,000 concurrent connections per Elastic IP attached to it, and AWS's own documented fix for hitting that ceiling is adding secondary IP addresses to the same NAT Gateway, which spreads new connections across a larger combined port space automatically. The underlying idea is identical to the single-router case — one public address, minus reserved and system ports, gives you roughly 64,000 usable translated ports — just multiplied across as many addresses as the pool holds.

This matters directly for capacity planning: a service that expects tens of thousands of concurrent outbound connections from behind a NAT boundary — a fleet of application servers all calling out to the same third-party API, for instance — needs to be sized against the port budget of its NAT layer, not just against CPU or bandwidth. Running out of translatable ports produces the exact same symptom as running out of table entries: connections that should succeed time out instead, with nothing in the application logs pointing at NAT as the cause unless someone already knows to look there.

NAT64 and DNS64: bridging an IPv6-only network to an IPv4 internet

Everything covered so far assumes the private side speaks IPv4, which is what Public vs Private IP introduced and what the device-level NAT article's translation table works with. But plenty of newer networks — mobile carrier networks especially, and increasingly some cloud environments — are provisioned as IPv6-only internally, specifically to sidestep IPv4 address planning altogether. The problem that creates is the mirror image of the original one: a client with only an IPv6 address still needs to reach IPv4-only servers, since a meaningful share of the internet hasn't adopted IPv6 yet.

NAT64 solves this the same way ordinary NAT solves address sharing — by translating headers at a gateway — but across address families rather than within one. A NAT64 gateway sits at the edge of the IPv6-only network, and IPv4 addresses it needs to reach are represented to internal clients as specially-constructed IPv6 addresses, most commonly under the well-known prefix 64:ff9b::/96, with the target IPv4 address embedded directly in the low 32 bits.

DNS64 is what makes that translation transparent to an application that only knows how to ask for an IPv6 address. When an IPv6-only client queries DNS for a name that has no AAAA record — meaning the destination is IPv4-only — a DNS64 resolver synthesizes one on the fly, embedding the real A record's IPv4 address inside the 64:ff9b::/96 prefix and returning that as if it were a genuine AAAA record. The client, none the wiser, connects to that synthesized IPv6 address exactly as it would to any other, and the NAT64 gateway does the actual translation back to real IPv4 when the packet reaches it.

IPv6-only client --DNS query: example.com AAAA--> DNS64 resolver
                                                      |
                                     No real AAAA record exists;
                                     resolver fetches the A record
                                     instead and synthesizes
                                     64:ff9b::93.184.216.34

Client <--synthesized AAAA: 64:ff9b::5db8:d822-- DNS64 resolver

Client --packet to 64:ff9b::5db8:d822--> NAT64 gateway --translated IPv4 packet--> 93.184.216.34

This is exactly the same trade-off ordinary NAT makes, one layer up: it lets a network avoid holding a large IPv4 allocation at all, at the cost of a translation gateway that every outbound connection to the legacy internet has to pass through — and, like any NAT, it breaks anything that depends on the original client address being visible end to end, or on protocols that embed IP addresses inside their own application payload rather than just the packet header.

Practical scenario: an office NAT gateway that "randomly" drops outbound calls

A company runs 400 employees behind a single office NAT gateway, and over several months the number of simultaneous SaaS tools, chat clients, and background sync tools each workstation runs has grown considerably. Helpdesk starts getting sporadic reports of video calls dropping and file uploads failing partway through — never for everyone at once, never at a fixed time of day, and restarting the affected application usually fixes it immediately.

sudo conntrack -C
sudo cat /proc/sys/net/netfilter/nf_conntrack_max
65486
65536

The gateway is running at 99.9% of its configured conntrack table capacity. With roughly 400 workstations averaging well over 150 simultaneous connections each during business hours — video calls, chat presence pings, background sync, browser tabs — the table fills up during peak hours, and whichever new connection attempt arrives once it's full simply fails to get an entry, with no clear pattern to which one it is. It looks random because it effectively is: whichever application on whichever machine happens to try opening a new connection at the exact moment the table is full loses.

The fix has the same two shapes any capacity problem does. Raising nf_conntrack_max on the gateway buys headroom immediately, but only up to what the hardware and available memory can hold — each conntrack entry consumes real kernel memory, so this isn't free scaling. The more durable fix is reducing demand or spreading it: shortening idle connection timeouts so completed or abandoned connections free their table entry sooner instead of lingering, or moving to a NAT deployment with multiple public addresses in its pool, splitting the same total connection demand across a larger combined port space the way the cloud NAT gateway example above does.

Sources

A device sharing one public address through NAT still can't be reached from the outside without something explicitly telling the router where to send an unsolicited inbound connection. That's the deliberate gap NAT leaves — and closing it on purpose, safely, is exactly what port forwarding is for.