Symptoms
- Intermittent
SocketException,connection timed out,An attempt was made to access a socket in a way forbidden by its access permissionsor HTTP 5xx from calls to external APIs, Azure SQL, Redis or storage. - It gets worse at peak and clears when traffic drops.
- CPU and memory look normal; the downstream service says it saw nothing.
Why it happens
Each outbound connection to the same destination IP and port uses a SNAT port on the shared front end. App Service gives each instance a limited pre-allocation (it varies; check current docs). Code that opens a new connection per request, or never closes them, burns through the pool. New connections then wait and time out.
Step 1: confirm (5 minutes)
Portal: App Service > Diagnose and solve problems > SNAT Port Exhaustion detector. It shows allocated and failed SNAT ports per instance.
Metrics and connections:
APP_ID=$(az webapp show -g rg-web -n web-prod --query id -o tsv)
az monitor metrics list --resource $APP_ID --metric "Connections" "Http5xx" \
--interval PT1M --aggregation Maximum --offset 2h -o table
From the Kudu console (Windows: Debug console; Linux: SSH), count connections by remote endpoint:
# Linux container
netstat -ant | awk '$6=="ESTABLISHED" || $6=="TIME_WAIT"{print $5}' | sort | uniq -c | sort -rn | head
# Windows (Kudu PowerShell)
Get-NetTCPConnection | Group-Object RemoteAddress, RemotePort | Sort-Object Count -Descending | Select-Object -First 10 Count, Name
Hundreds of connections in TIME_WAIT to one endpoint is the fingerprint.
Step 2: stop the bleeding (minutes)
Scale out: each new instance brings its own SNAT allocation.
az appservice plan update -g rg-web -n plan-prod --number-of-workers 6
If one dependency is the hot spot and it supports private endpoints (Azure SQL, Storage, Redis, Key Vault), traffic through VNet integration to a private endpoint does not use App Service SNAT at all. That change takes longer than scaling but is often doable in the same day.
Tip
Restarting the app clears connections and looks like a fix. It is not; the pool fills again at the next peak. Use the time it buys you to do step 3.
Step 3: fix connection reuse (the real fix)
| Stack | Wrong | Right |
|---|---|---|
| .NET | new HttpClient() per request | One HttpClient via IHttpClientFactory, or a static instance with SocketsHttpHandler.PooledConnectionLifetime |
| Node.js | default agent without keep-alive (older versions) | new https.Agent({ keepAlive: true, maxSockets: 50 }) |
| Python | requests.get() each time | one requests.Session() reused |
| Java | new client per call | shared pooled client (Apache HttpClient pool, OkHttp) |
| Database | open/close without pooling | driver pooling on, sensible max pool size |
.NET example:
builder.Services.AddHttpClient("payments", c => c.BaseAddress = new Uri("https://api.payments.example"))
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler {
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
MaxConnectionsPerServer = 100
});
Python:
import requests
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(pool_connections=20, pool_maxsize=50)
session.mount("https://", adapter)
Step 4: move egress off the shared SNAT
With regional VNet integration, route all outbound traffic through the VNet and attach a NAT gateway to the integration subnet. A NAT gateway gives 64,512 SNAT ports per public IP and you can attach several IPs.
az network public-ip create -g rg-web -n pip-nat-web --sku Standard
az network nat gateway create -g rg-web -n nat-web --public-ip-addresses pip-nat-web --idle-timeout 4
az network vnet subnet update -g rg-web --vnet-name vnet-web -n snet-appsvc-integration --nat-gateway nat-web
az webapp config set -g rg-web -n web-prod --generic-configurations '{"vnetRouteAllEnabled": true}'
On newer platform versions the setting is exposed as outbound VNet routing; check the current CLI help. Remember that the outbound IP changes to the NAT gateway's IP, so update partner allow-lists first.
Step 5: confirm
- SNAT detector shows no failed allocations at the next peak.
- Connection counts per endpoint stay stable instead of climbing.
- Timeouts in Application Insights dependency telemetry drop to baseline:
dependencies
| where timestamp > ago(6h)
| summarize failures = countif(success == false), calls = count() by target, bin(timestamp, 5m)
Prevent it next time
- Code review rule: no per-request HTTP or DB clients.
- Private endpoints for Azure PaaS dependencies; NAT gateway for internet egress.
- Load test at 2x peak and watch the SNAT detector before big launches.