Homeβ€Ί Blogβ€Ί Azure Architecture Series #70 β€” NAT Gateway and SNAT Port Exhaustion…
Azure Architecture Azure Architecture Series

Azure Architecture Series #70 β€” NAT Gateway and SNAT Port Exhaustion

Verified against current vendor documentation on 22 October 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.

Business Challenge

#68 established that new virtual networks no longer get implicit internet egress, and that you must pick an explicit method instead. Microsoft names a favourite without hedging:

“Azure NAT Gateway is the method that we recommend for outbound connectivity.”

The pitch is that it removes the problem it is named after — it “dynamically allocates source NAT (SNAT) ports to automatically scale outbound connectivity and minimize the risk of SNAT port exhaustion”, and “doesn’t have the same limitations of SNAT port exhaustion as default outbound access and outbound rules of a load balancer.”

Minimise, not eliminate. So the useful question is what the remaining limits are, and the arithmetic is worth doing properly because the headline number and the binding number are different by a factor of twenty.

FigureValueWhat it governs
SNAT ports per public IP64,512The port inventory
Public IPs per NAT gatewayup to 16How far the inventory scales
Total SNAT ports“more than 1 million”16 × 64,512 = 1,032,192
Concurrent connections to one destination, per IP50,000The limit you will actually hit
Active connections per gateway2 millionBeyond it, new connections fail

The fourth row is the post. A million ports is not a pool you draw from freely, because ports are not interchangeable — what a port identifies is a flow, and flows to the same place must differ:

The two sentences that explain everything

“The same SNAT port can be used to connect to different destination endpoints at the same time.”

“Different SNAT ports are used to make connections to the same destination endpoint in order to distinguish connection flows from one another.”

So the inventory is effectively per-destination. Spread across a thousand destinations, a million ports is enormous. Pointed at one — one payment gateway, one API, one database endpoint — the ceiling is 50,000 per public IP, and the way to raise it is to add public IPs rather than to add machines.

Architecture

A subnet-level service with no routes to write, a port inventory shared on demand, and a set of timers that decide how fast ports come back.

Diagram: Azure NAT Gateway and SNAT port exhaustion in five layers. The first layer gives the arithmetic: each public IP attached to a NAT gateway provides 64,512 SNAT ports, up to 16 public IPs can be attached, and 16 times 64,512 equals 1,032,192, which is the more-than-one-million figure. The second layer explains why that number is not a free pool: the same SNAT port can serve different destination endpoints simultaneously, but different SNAT ports are required for connections to the same destination endpoint, so the inventory is effectively per-destination. The binding limit is therefore 50,000 concurrent connections per public IP address to the same destination endpoint, and the remedy is to add public IP addresses rather than machines. A separate ceiling applies per gateway: 2 million active simultaneous connections counted on the five-tuple of source address, source port, destination address, destination port and protocol, beyond which the data path's availability declines and new connections fail. TCP and UDP hold separate port inventories. The third layer covers the timers that govern how quickly ports return. TCP idle timeout defaults to 4 minutes and is configurable up to 120 minutes, but Microsoft does not recommend raising it because the gateway holds the port until the connection times out and longer timers increase exhaustion risk; UDP idle timeout is fixed at 4 minutes. Port reuse hold-downs to the same destination are 65 seconds after a TCP FIN, 16 seconds after a TCP RST, 30 seconds for a half-open connection, and 65 seconds for UDP. Microsoft warns these timer values are subject to change and should not be depended on. TCP keepalives appear as duplicate acknowledgements and are invisible to the application; UDP keepalives are one-directional and must be enabled on both sides. The fourth layer is the precedence ladder for outbound traffic, highest first: a user-defined route to a virtual appliance or virtual network gateway, then the NAT gateway, then an instance-level public IP on the virtual machine, then load balancer outbound rules, then the default system route to the internet. So attaching a NAT gateway silently overrides a load balancer, an instance public IP and Azure Firewall for new connections, while a user-defined route overrides the NAT gateway. The fifth layer lists the constraints: one NAT gateway per subnet, cannot span virtual networks, not allowed in a gateway subnet, no Basic SKU resources anywhere in the subnet, no public IPs using Internet routing preference or with DDoS protection enabled, no IP fragmentation, not supported in a secured virtual WAN hub, the zone cannot be changed after deployment, Standard cannot be upgraded to StandardV2 and StandardV2 requires StandardV2 public IPs, and existing flows are not preserved when a virtual machine live-migrates. Throughput is 50 Gbps split 25 outbound and 25 inbound on Standard, and 100 Gbps on StandardV2.
A million ports, but the binding limit is 50,000 per IP to one destination — and the fix is more addresses, not more machines.

Where the ports come from, and who shares them

There is no per-machine allocation, which is the structural difference from load balancer outbound rules: “SNAT port inventory is provided by the public IP addresses, public IP prefixes, or both attached to a NAT gateway. SNAT port inventory is available on demand to all instances within a subnet attached to the NAT gateway. No preallocation of SNAT ports per instance is required.”

That on-demand model is why a NAT gateway tolerates uneven load: one busy VM can draw on the whole inventory rather than being capped at its share. The flip side is that the inventory is a shared resource with a shared fate — “When multiple subnets within a virtual network are attached to the same NAT gateway resource, the SNAT port inventory… is shared across all subnets.” One noisy subnet can starve another.

One mitigation that costs nothing to know: “TCP and UDP are separate SNAT port inventories”, so a UDP-heavy workload cannot exhaust TCP capacity or vice versa.

The timers, which are the real exhaustion mechanism

Ports are not returned the moment a connection ends. Two kinds of delay apply, and together they set the recycling rate:

TimerValueEffect
TCP idle timeout4 minutes default, up to 120 minutesPort held for the whole idle period
UDP idle timeout4 minutes, not configurableSame, with no control
Reuse after TCP FIN65 secondsHold-down to the same destination
Reuse after TCP RST16 secondsA reset frees the port four times faster
TCP half open30 secondsUnacknowledged setup closes, then frees
Reuse after UDP close65 secondsHold-down to the same destination

The idle timeout is the one worth not touching, and the documentation says so explicitly: “When a connection goes idle, the NAT gateway holds onto the SNAT port until the connection idle times out. Because long idle timeout timers can unnecessarily increase the likelihood of SNAT port exhaustion, we don’t recommend that you increase the TCP idle timeout duration to longer than the default time of 4 minutes.”

Which inverts the instinct. Faced with connections dropping, the obvious move is to raise the idle timeout — and that makes exhaustion more likely, because every idle connection is now squatting on a port for up to two hours. The documented alternative is keepalives: “TCP keepalives appear as duplicate acknowledgements (ACKs) to the endpoints, are low overhead, and are invisible to the application layer” — they refresh the flow without holding a port idle.

UDP keepalives come with a catch worth reading twice: “Unlike TCP connections, a UDP keepalive enabled on one side of the connection applies only to traffic flow in one direction. You must enable UDP keepalives on both sides.”

And a caveat on the whole table, which is unusually candid: “These timer settings are subject to change. The provided values can help with troubleshooting. You shouldn’t take a dependency on specific timers at this time.” Useful for diagnosis, not for capacity planning.

The precedence ladder

Attaching a NAT gateway changes outbound behaviour for things you did not touch, because it outranks them: “Azure NAT Gateway takes precedence over other outbound connectivity methods, including a load balancer, instance-level public IP addresses, and Azure Firewall.”

The full order, highest first:

PriorityMethod
1UDR to a virtual appliance or virtual network gateway
2NAT gateway
3Instance-level public IP on the VM
4Load balancer outbound rules
5Default system route to the internet

Two consequences. A machine with its own public IP stops using it for egress the moment a NAT gateway appears on the subnet — and the change is not retroactive, since “There are no drops in traffic flow for existing connections that use other explicit methods”, so old flows persist on the old path while new ones move. And a 0.0.0.0/0 UDR to a firewall beats the NAT gateway entirely, which is how a hub-and-spoke design keeps inspection in place.

Why This Architecture Holds Up

Because the number that constrains you is not the number that gets quoted

“Over a million SNAT ports” is true and nearly useless for capacity planning. The figure to size against is “up to 50,000 concurrent connections per public IP address to the same destination endpoint”.

So the question is not how many connections the workload opens, but how many it opens to each place. A fleet making a million connections spread over thousands of endpoints is comfortable. A fleet making 60,000 simultaneous calls to one API endpoint is over the limit on a single public IP, regardless of how much inventory is sitting unused.

Which makes the remedy counterintuitive and cheap: attach more public IP addresses. Each adds 64,512 ports and another 50,000-connection allowance to that destination. Scaling out the VMs does nothing for this limit; scaling out the addresses is the whole fix.

The second ceiling is absolute and worth knowing because its failure mode is ugly: “Each can support up to 2 million active connections simultaneously… If a NAT gateway exceeds 2 million connections, the data path’s availability declines and new connections fail.” Counted on the 5-tuple, so a connection is a flow and not a socket.

Because the fix for dropped connections makes exhaustion worse

This is the trap I would most expect an on-call engineer to walk into. Connections are dying; the idle timeout is right there and goes up to two hours; raising it looks like the fix. It is the opposite of the fix, because the port stays held for the whole idle window.

And the symptom that sends people there is itself informative. A NAT gateway “sends a TCP Reset (RST) packet for any connection flow that it doesn’t recognize as an existing connection… When the sender… receives the… TCP RST packet, the connection is no longer usable.” So an application that has been idle past the timeout does not hang — it gets a reset on its next write, which reads like the remote end hung up.

There is one small mercy in the timer table: a reset frees its port after 16 seconds versus 65 for a graceful FIN. Connections that die badly recycle four times faster than connections that close politely, which is a genuinely strange property to hold in mind.

Because it silently takes over egress from things you configured

The precedence ladder means adding a NAT gateway is not an additive change. A VM with an instance-level public IP keeps the address — and stops using it for outbound. A load balancer with outbound rules keeps the rules — and stops being the egress path. Azure Firewall likewise, unless a UDR puts it above.

For most estates that is what you want, and it is why the migration is simple. But it makes the NAT gateway a thing that changes the meaning of existing configuration without changing the configuration — the same shape of problem as AllowVNetInBound in #65, arriving from the opposite direction. Anyone auditing egress addresses needs to read the ladder, not the resource list.

Because the SKU choice is permanent and drags the public IPs with it

Two SKUs, and the difference is resilience: Standard is “zonal (deployed to a single availability zone)” while StandardV2 is “zone redundant” with higher throughput, IPv6 and flow logs. The consequence of choosing Standard is explicit — “If the zone that’s associated with a Standard NAT gateway goes down, the outage affects outbound connectivity for the subnets associated with the NAT gateway.”

And there is no path between them: “You can’t upgrade the Standard SKU to the StandardV2 SKU. You must create a StandardV2 NAT gateway to replace the Standard NAT gateway on your subnet.” Nor can you carry the addresses across — “The StandardV2 SKU requires StandardV2 public IP addresses or prefixes. Standard public IPs aren’t supported with StandardV2.”

Which lands directly on #69: the Standard-v2 public IP that could only be used with a Standard v2 NAT Gateway turns out to be the matching half of this decision. Choosing zone redundancy here means re-IPing, and re-IPing means every firewall rule and A record that referenced the old addresses. The zone cannot be changed later either — “After you deploy a NAT gateway, you can’t change the zone selection.”

Because the Basic SKU exclusion is subnet-wide

A constraint that turns #69's migration into a prerequisite rather than a parallel task: “You can’t use Azure NAT Gateway with subnets that have Basic resources. Resources for the Basic SKU, such as a Basic load balancer or Basic public IPs, don’t work with Azure NAT Gateway.”

So one leftover Basic public IP anywhere in the subnet blocks the recommended outbound method for everything in it. Given #68 pushes new subnets towards requiring an explicit method and #69 established that Basic is retired and unsupported, the three posts compose into a single ordering: clear the Basic resources, then attach the NAT gateway, then make the subnet private.

Because a handful of exclusions will decide the design before the numbers do

ExclusionWhy it matters
“IP fragmentation isn’t available”The same gap default outbound access had — migrating does not fix it
No public IPs with Internet routing preferenceRules out the cheaper egress routing from #69
No public IPs with DDoS protection enabledA security control and this service are mutually exclusive
Not in a gateway subnetThe VPN gateway's own subnet cannot use it
Not in a secured vWAN hubRules it out for that whole topology
One per subnet, never across VNetsHub-and-spoke needs the Firewall-plus-UDR pattern instead

The fragmentation one is the quiet repeat. #68 noted that default outbound access IPs “don’t support fragmented packets”, and the recommended replacement does not either. A workload failing on large UDP payloads will fail identically after the migration, which is worth knowing before anyone proposes the migration as the cure.

Because live migration breaks flows and nothing warns you

One operational detail with no configuration to fix it: “NAT Gateway doesn’t support connection continuity for existing flows when a virtual machine undergoes live migration. Existing connections through the NAT gateway might be disrupted and need to be reestablished.”

Live migration is a platform action you do not schedule. So long-lived outbound connections through a NAT gateway need application-level reconnection logic, not because of anything in your design, but because the host underneath can move. That belongs in a resilience review rather than a network one.

Key Architecture Decisions

Decision point Choose Because
Explicit outbound method NAT gateway It is Microsoft's recommended method.
Sizing against “a million ports” Don’t — size per destination Ports are only distinct per destination endpoint.
Heavy traffic to one endpoint Add public IP addresses 50,000 concurrent per IP, per destination.
Scaling out VMs to fix exhaustion Won’t help The limit is per address, not per instance.
Approaching 2 million active flows Split across gateways Beyond it, new connections fail.
Connections dropping when idle Keepalives, not a longer timeout A longer timeout holds ports and worsens exhaustion.
Raising the TCP idle timeout Microsoft advises against it Default 4 minutes; 120 is available but risky.
UDP keepalives Enable on both sides One side only covers one direction.
Planning capacity from the timer table Don’t Microsoft says not to depend on the values.
An app getting resets after inactivity Suspect the idle timeout The gateway resets unrecognised flows.
Attaching a gateway to a subnet with public IPs Expect egress to move to it It outranks instance IPs and load balancers.
Keeping firewall inspection on egress A 0.0.0.0/0 UDR A UDR outranks the NAT gateway.
Needing zone resilience StandardV2, from the start Standard is zonal and can't be upgraded.
Moving Standard to StandardV2 Plan a re-IP StandardV2 needs StandardV2 public IPs.
Any Basic resource in the subnet Upgrade it first Basic blocks NAT gateway for the whole subnet.
A workload sending fragmented packets NAT gateway won’t fix it IP fragmentation isn't available.
Public IPs with DDoS protection Incompatible Not supported with NAT gateway.
Outbound from a gateway subnet Not possible here NAT gateway can't be used in one.
Secured vWAN hub topology Use another method NAT gateway isn't supported there.
Long-lived outbound connections Add reconnection logic Live migration breaks existing flows.
Backends behind an internal load balancer NAT gateway on the backend subnet It gives SNAT without public IPs per VM.

Closing Thought

The honest summary is that a NAT gateway makes SNAT port exhaustion much harder to hit and changes what hitting it looks like. The old failure was a per-instance allocation running dry while the pool had capacity. The new failure is a per-destination ceiling: 50,000 concurrent connections from one public IP to one endpoint, with a million ports sitting idle because they are not the kind of port you need.

That is why the capacity question is about shape rather than volume. Tell me how many connections your fleet opens and I cannot size this. Tell me how many it opens to its busiest single endpoint and the answer is arithmetic: divide by 50,000, round up, attach that many public IP addresses. Everything else — instance count, subnet count, VM size — is irrelevant to that number.

And the thing I would put on the wall is the idle timeout inversion. Connections are resetting, the timeout is adjustable up to two hours, and raising it is exactly wrong because every idle connection holds its port for the whole window. Microsoft says so plainly and the control is still there, set to four minutes, inviting the mistake. Keepalives refresh a flow without holding a port; a longer timeout holds a port without refreshing anything. One of those scales and the other is a slow way to run out.

Next in this series

#71 returns to the topology that #62 set up and prices it: VNet peering, global peering, and what the data transfer actually costs when every spoke talks through a hub.

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent