Troubleshooting Nginx Upstream Ephemeral Port Exhaustion: Resolving '99: Cannot assign requested address' & TIME_WAIT Floods

A deep systems engineering guide to diagnosing and fixing Nginx upstream socket starvation, Linux TCP TIME_WAIT floods, and 99: Cannot assign requested address errors under high concurrency.

Troubleshooting Nginx Upstream Ephemeral Port Exhaustion: Resolving '99: Cannot assign requested address' & TIME_WAIT Floods

Troubleshooting Nginx Upstream Ephemeral Port Exhaustion: Resolving “99: Cannot assign requested address” & TIME_WAIT Floods

In high-concurrency production environments—such as high-traffic WooCommerce flash sales, SaaS API gateways, or containerized reverse proxy clusters—systems engineers frequently encounter a sudden and baffling failure. Without any spike in CPU utilization, memory pressure, or disk I/O bottlenecks, Nginx abruptly begins returning 502 Bad Gateway errors to hundreds of visitors per second.

When inspecting /var/log/nginx/error.log, the underlying failure reveals a critical socket-level refusal:

2026/09/25 04:12:30 [crit] 28140#28140: *4819213 connect() to 127.0.0.1:9000 failed (99: Cannot assign requested address) while connecting to upstream, client: 198.51.100.24, server: example.com, request: "POST /wp-admin/admin-ajax.php HTTP/2.0", upstream: "fastcgi://127.0.0.1:9000", host: "example.com"

Or on HTTP microservice reverse proxies routing traffic to backend applications:

2026/09/25 04:15:12 [crit] 31042#31042: *1528190 connect() to 10.0.0.15:8080 failed (99: Cannot assign requested address) while connecting to upstream, client: 203.0.113.88, server: api.example.com, request: "GET /v1/checkout/session HTTP/2.0", upstream: "http://10.0.0.15:8080/v1/checkout/session", host: "api.example.com"

This error is not caused by file descriptor limits (worker_rlimit_nofile), nor is it caused by worker process limits (worker_connections).

Instead, this failure represents Linux Kernel Ephemeral Port Starvation driven by runaway TCP TIME_WAIT socket accumulation in the Linux network stack.

In this deep-dive guide, we dissect the mathematical constraints of the TCP 4-tuple, explain the kernel lifecycle of TIME_WAIT, inspect real diagnostic telemetry via ss and bpftrace, and deploy production-grade kernel and Nginx configurations for high-throughput nodes, Dedicated Servers, and mission-critical Dedicated Servers in Pakistan.


1. The Anatomy of the TCP 4-Tuple Bottleneck

To understand why error 99: Cannot assign requested address (EADDRNOTAVAIL) occurs, we must examine how the Linux kernel identifies and allocates network connections.

Every TCP connection across an IP network is uniquely defined by a 4-tuple:

$$\text{Connection ID} = (\text{Source IP}, \text{Source Port}, \text{Destination IP}, \text{Destination Port})$$

+-----------------------------------------------------------------------------------------+
|                                    CLIENT BROWSER                                       |
+-----------------------------------------------------------------------------------------+
                                             |
                                             | HTTPS (Port 443)
                                             v
+-----------------------------------------------------------------------------------------+
|                                  NGINX REVERSE PROXY                                    |
|                                                                                         |
|  Inbound:  [ Client IP : Random Client Port ] -> [ Nginx IP : 443 ]                     |
|  Outbound: [ Local Source IP : EPHEMERAL PORT ] -> [ Upstream IP : Upstream Port ]      |
+-----------------------------------------------------------------------------------------+
                                             |
                   TCP Loopback (127.0.0.1:9000) or Private LAN (10.0.0.15:8080)
                                             v
+-----------------------------------------------------------------------------------------+
|                                UPSTREAM BACKEND SERVICE                                 |
|                         (PHP-FPM, Node.js, Go, Python Gunicorn)                         |
+-----------------------------------------------------------------------------------------+

When Nginx acts as a reverse proxy forwarding requests to a backend service (e.g., PHP-FPM listening on 127.0.0.1:9000 or an API server listening on 10.0.0.15:8080):

  1. Destination IP is fixed (127.0.0.1 or 10.0.0.15).
  2. Destination Port is fixed (9000 or 8080).
  3. Source IP is fixed (the outbound interface IP of the Nginx server).

Because three parameters of the 4-tuple are immutable constants, Source Port is the single variable available to identify distinct connections.

The Linux kernel draws source ports for outbound connections from an internal pool known as the ephemeral port range, governed by net.ipv4.ip_local_port_range.

Check the default ephemeral port range on your server:

sysctl net.ipv4.ip_local_port_range

Default standard output:

net.ipv4.ip_local_port_range = 32768 60999

This default range provides exactly:

$$60999 - 32768 + 1 = 28,232 \text{ usable outbound ports}$$


2. The Physics of the TIME_WAIT State

When an HTTP transaction finishes between Nginx and the upstream server, one of the two endpoints must initiate the TCP teardown sequence by transmitting a FIN packet.

The endpoint that sends the active FIN enters the TIME_WAIT state after receiving the final ACK.

  Nginx (Active Closer)                      Upstream (Passive Closer)
        |                                                |
        |  --- FIN --->                                  |  (Nginx initiates close)
        |  <-- ACK ---                                   |
        |                                                |
        |  <-- FIN ---                                   |
        |  --- ACK --->                                  |
        |                                                |
   [TIME_WAIT]                                        [CLOSED]
        |
        | (Locked for 2 * MSL = 60 seconds)
        v
     [CLOSED]

Why does TIME_WAIT exist?

  1. Preventing Data Corruption: It ensures that late, delayed, or duplicated packets from the old connection in transit across the Internet or virtual network fabrics do not collide with a newly opened connection reusing the exact same 4-tuple.
  2. Graceful Teardown Guarantee: It ensures the remote peer receives the final ACK. If the final ACK is dropped in transit, the remote peer will retransmit its FIN, which the host in TIME_WAIT can re-acknowledge.

The 60-Second Hardcoded Kernel Constant

In the Linux kernel source code (include/net/tcp.h), the duration of TIME_WAIT is defined by $2 \times \text{MSL}$ (Maximum Segment Lifetime):

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds */

This value cannot be adjusted via a standard sysctl parameter; it is hardcoded to 60 seconds.

The Mathematical Saturation Threshold

If Nginx opens a new TCP connection for every incoming upstream request without connection reuse:

$$\text{Max Sustainable Throughput} = \frac{\text{Total Usable Ephemeral Ports}}{\text{TIME_WAIT Duration}} = \frac{28,232 \text{ ports}}{60 \text{ seconds}} \approx 470.53 \text{ req/sec}$$

If your application traffic exceeds 471 requests per second to a single upstream destination, the rate of new connection requests exceeds the rate at which expired sockets exit TIME_WAIT.

Within minutes:

  1. Every ephemeral port in the kernel table is locked in TIME_WAIT.
  2. When Nginx calls the kernel connect() syscall, the kernel routine __inet_hash_connect() searches the port hash table and finds zero unallocated 4-tuples.
  3. The kernel returns error -EADDRNOTAVAIL.
  4. Nginx logs connect() failed (99: Cannot assign requested address) and aborts with 502 Bad Gateway.

3. Real-Time Diagnostics & Telemetry

When investigating sudden 502 errors, do not guess. Use the following commands to confirm ephemeral port exhaustion and socket table saturation.

Step 1: Inspect Global Socket Summary with ss

Run ss -s to inspect the kernel’s live socket allocation:

ss -s

Sample output during an active port exhaustion event:

Total: 31410
TCP:   29142 (estab 782, closed 28240, orphaned 12, timewait 28212)

Transport Total     IP        IPv6
RAW	  2         1         1        
UDP	  8         5         3        
TCP	  902       882       20       
INET	  912       888       24       
FRAG	  0         0         0        

Notice timewait 28212—this matches the exact boundary of the default 32768-60999 range.

Step 2: Analyze Socket States by Destination Port

Verify which upstream service is holding the sockets:

ss -tan state time-wait '( dport = :9000 or dport = :8080 )' | wc -l

Or view a breakdown of socket states across all active ports:

ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn

Output:

  28231 TIME-WAIT
    740 ESTAB
     65 LISTEN
     12 CLOSE-WAIT
      4 SYN-SENT

Step 3: Inspect Kernel Memory Allocation via /proc/net/sockstat

cat /proc/net/sockstat

Output:

sockets: used 30412
TCP: inuse 815 orphan 12 tw 28231 alloc 30120 mem 42
UDP: inuse 8 mem 2
UDPLITE: inuse 0
RAW: inuse 2
FRAG: inuse 0 memory 0

tw 28231 confirms that thousands of socket control blocks are lingering in memory, monopolizing the local port allocator.

Step 4: Trace Kernel Allocation Failures with eBPF

To observe the kernel failing to assign ports in real time, run this bpftrace one-liner (requires bpftrace and kernel headers installed):

bpftrace -e 'kretprobe:__inet_hash_connect /retval < 0/ { @failures[retval] = count(); }'

Output:

Attaching 1 probe...
^C
@failures[-99]: 14829

-99 corresponds directly to EADDRNOTAVAIL (Cannot assign requested address).


4. The Dangerous Fallacy: tcp_tw_recycle

When searching online forums for TIME_WAIT fixes, older articles frequently recommend enabling:

# DO NOT RUN THIS - DANGEROUS AND DEPRECATED
sysctl -w net.ipv4.tcp_tw_recycle=1

[!WARNING] tcp_tw_recycle was completely removed in Linux Kernel 4.12+ (2017). If you are on an older legacy distribution where this parameter still exists, enabling it will cause catastrophic, intermittent connection drops for users behind NAT gateways (offices, mobile carriers, home routers).

Under RFC 1323, tcp_tw_recycle tracked per-IP TCP timestamps. When multiple users share a single public IP address behind NAT, their individual packet timestamps are not synchronized. The Linux kernel discarded out-of-order timestamps as spoofed packets, silently dropping valid connections. Never attempt to re-enable or rely on this parameter.


5. Architectural Resolution: Step-by-Step Production Fix

Eliminating ephemeral port exhaustion requires a multi-layered approach:

  1. Expanding the Kernel Ephemeral Range (immediate headroom).
  2. Enabling Safe Outbound Socket Reuse (tcp_tw_reuse).
  3. Configuring Upstream Persistent Keepalive Pools (the root-cause architectural solution).
  4. Migrating to Unix Domain Sockets where applicable.

Step 1: Expand Kernel Ephemeral Port Range

Expand the usable port pool from 28,232 ports to 55,295 ports by lowering the minimum port boundary to 10240:

Create /etc/sysctl.d/99-network-throughput.conf:

# /etc/sysctl.d/99-network-throughput.conf

# Expand ephemeral port range (Default: 32768 60999 -> 10240 65535)
net.ipv4.ip_local_port_range = 10240 65535

# Enable safe reuse of TIME_WAIT sockets for outbound connections
net.ipv4.tcp_tw_reuse = 1

# Ensure TCP timestamps are enabled (MANDATORY for tcp_tw_reuse under RFC 1323)
net.ipv4.tcp_timestamps = 1

# Lower FIN timeout to reclaim closed sockets faster (Default: 60s -> 15s)
net.ipv4.tcp_fin_timeout = 15

# Increase maximum socket backlog queues for high burst volume
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.core.netdev_max_backlog = 10000

# Increase maximum TIME_WAIT socket bucket size
net.ipv4.tcp_max_tw_buckets = 262144

Apply the changes immediately without rebooting:

sysctl --system

Verify the active values:

sysctl net.ipv4.ip_local_port_range net.ipv4.tcp_tw_reuse net.ipv4.tcp_timestamps

Deep Dive: How net.ipv4.tcp_tw_reuse = 1 Works Safely

Unlike the deprecated tcp_tw_recycle, tcp_tw_reuse = 1 is completely safe for both NAT and non-NAT environments.

It applies only to outbound client/proxy connections initiated by Nginx to upstream servers. When Nginx attempts to open a new connection to an upstream IP:port, if the chosen local port is currently in TIME_WAIT, the kernel compares the timestamp of the new outgoing SYN packet with the timestamp of the last packet received on the previous connection.

If the new timestamp is strictly greater than the recorded timestamp (guaranteed by monotonic system clocks under net.ipv4.tcp_timestamps = 1), the kernel recycles the TIME_WAIT socket immediately without waiting for the 60-second timer to expire.


Step 2: Implement Persistent HTTP Upstream Keepalives in Nginx

While tuning sysctl expands port capacity, opening and tearing down a TCP handshake for every single HTTP request is inherently inefficient.

By default, Nginx connects to upstreams using HTTP/1.0 without keepalives, closing the connection after every single request!

To keep persistent connections open, configure an explicit upstream block with the keepalive directive, and set proxy_http_version 1.1; along with clearing the Connection header.

Hardened Nginx Microservice / Reverse Proxy Configuration:

Edit your site configuration (e.g., /etc/nginx/conf.d/api.conf):

# Define the upstream with a persistent connection pool
upstream api_backend_cluster {
    server 10.0.0.15:8080 max_fails=3 fail_timeout=10s;
    server 10.0.0.16:8080 max_fails=3 fail_timeout=10s;

    # Maximum number of idle keepalive connections per worker process
    keepalive 128;
    
    # Maximum number of requests served through one keepalive connection
    keepalive_requests 10000;
    
    # Idle timeout before closing an unused keepalive connection
    keepalive_timeout 60s;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/ssl/certs/api.example.com.crt;
    ssl_certificate_key /etc/ssl/private/api.example.com.key;

    location / {
        proxy_pass http://api_backend_cluster;

        # MANDATORY: Nginx defaults to HTTP/1.0 for upstreams. HTTP/1.1 is required for keepalive.
        proxy_http_version 1.1;

        # MANDATORY: Clear the Connection header to prevent "Connection: close" transmission
        proxy_set_header Connection "";

        # Standard proxy headers
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Buffer tuning for high throughput
        proxy_buffers 16 32k;
        proxy_buffer_size 64k;
        proxy_busy_buffers_size 128k;

        # Upstream timeouts
        proxy_connect_timeout 5s;
        proxy_read_timeout 60s;
        proxy_send_timeout 60s;
    }
}

[!IMPORTANT] The two directives proxy_http_version 1.1; and proxy_set_header Connection ""; are strictly mandatory. If you configure keepalive 128; in the upstream block but forget to clear the Connection header, Nginx will continue sending Connection: close to the upstream backend, completely nullifying the connection pool!


Step 3: Implement FastCGI Keepalives for PHP-FPM / WordPress

If your server runs WordPress, WooCommerce, or Magento with PHP-FPM listening on TCP socket 127.0.0.1:9000, Nginx will rapidly burn through all ephemeral loopback ports under traffic spikes unless FastCGI keepalive is activated.

Step A: Configure Upstream in Nginx

Edit /etc/nginx/conf.d/php-upstream.conf:

upstream php_fpm_pool {
    server 127.0.0.1:9000;
    
    # Maintain a pool of up to 64 idle FastCGI connections per Nginx worker
    keepalive 64;
}

Step B: Enable fastcgi_keep_conn in Location Block

In your virtual host configuration (/etc/nginx/sites-available/wordpress.conf):

location ~ \.php$ {
    try_files $uri =404;
    
    # Route through the upstream block, NOT directly to 127.0.0.1:9000
    fastcgi_pass php_fpm_pool;
    
    # MANDATORY: Instruct Nginx to keep the FastCGI connection open to PHP-FPM
    fastcgi_keep_conn on;

    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    # Buffer configuration
    fastcgi_buffers 16 16k;
    fastcgi_buffer_size 32k;
    fastcgi_busy_buffers_size 64k;
    
    fastcgi_connect_timeout 5s;
    fastcgi_send_timeout 60s;
    fastcgi_read_timeout 60s;
}

Step 4: The Ultimate Local Performance Fix: Unix Domain Sockets

If Nginx and PHP-FPM reside on the same physical or virtual server, routing through TCP loopback (127.0.0.1:9000) is an unnecessary architectural tax.

TCP loopback requires:

  • TCP three-way handshake (SYN, SYN-ACK, ACK).
  • TCP four-way teardown (FIN, ACK, FIN, ACK).
  • IP checksum computation and network stack packet traversal.
  • Ephemeral port tracking and TIME_WAIT state tables.

In contrast, Unix Domain Sockets (UDS) operate entirely in kernel VFS memory via memory buffers, bypassing IP routing, TCP states, and ephemeral port allocations completely.

Benchmark Comparison: TCP Loopback vs. Unix Domain Socket

Architectural Metric TCP Loopback (127.0.0.1:9000) Unix Domain Socket (unix:/run/php-fpm.sock)
Protocol Overhead Full TCP/IP Stack, Framing, Checksums Direct Kernel Memory Buffer Copy
Port Consumption 1 Ephemeral Port per Connection 0 Ports (Immune to Port Exhaustion)
TIME_WAIT Exposure Up to 28,000+ Sockets Trapped None
System Latency ~65–110 microseconds ~18–35 microseconds (~60% faster)
Throughput Ceiling Bound by net.ipv4.ip_local_port_range Bound only by CPU memory bandwidth

Migration to Unix Domain Sockets:

  1. Edit your PHP-FPM pool configuration (/etc/php/8.3/fpm/pool.d/www.conf):
; Replace: listen = 127.0.0.1:9000
listen = /run/php/php8.3-fpm.sock

; Configure socket ownership and permissions for the Nginx user
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

; Increase socket backlog for burst traffic
listen.backlog = 65535
  1. Restart PHP-FPM:
systemctl restart php8.3-fpm
  1. Update your Nginx configuration:
location ~ \.php$ {
    try_files $uri =404;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
  1. Test and reload Nginx:
nginx -t && systemctl reload nginx

6. Advanced Architecture: Multi-IP proxy_bind for Ultra-Scale Gateways

What if your architecture consists of a dedicated API load balancer forwarding 80,000+ requests per second across high-throughput backend clusters, where backend servers reside on remote hosts and Unix domain sockets cannot be used?

Even with keepalives and expanded port ranges, a single outbound IP address connecting to a single remote backend IP:port will eventually hit the 55,295-port boundary.

To shatter this limit, configure IP Aliasing on the Nginx proxy network interface and utilize Nginx’s proxy_bind directive with a split IP pool.

                                 NGINX REVERSE PROXY
                         (Interface eth0 with 4 Aliased IPs)
                                   10.0.0.100
                                   10.0.0.101
                                   10.0.0.102
                                   10.0.0.103
                                        |
              +-------------------------+-------------------------+
              |                         |                         |
       55,295 Ports              55,295 Ports              55,295 Ports
              |                         |                         |
              v                         v                         v
     [ Backend Server 1 ]      [ Backend Server 2 ]      [ Backend Server 3 ]
        10.0.0.15:8080            10.0.0.16:8080            10.0.0.17:8080

By binding 4 local IP addresses, your available 4-tuple capacity increases fourfold:

$$\text{Capacity} = 4 \text{ Source IPs} \times 55,295 \text{ Ports} = 221,180 \text{ Concurrent Sockets}$$

Nginx Split-IP Configuration:

# Map client connection IDs to distribute outbound source IPs across the pool
split_clients "$connection" $outbound_proxy_ip {
    25.0%   10.0.0.100;
    25.0%   10.0.0.101;
    25.0%   10.0.0.102;
    *       10.0.0.103;
}

upstream backend_cluster {
    server 10.0.0.15:8080;
    server 10.0.0.16:8080;
    keepalive 256;
}

server {
    listen 443 ssl http2;
    server_name api.enterprise.com;

    location / {
        proxy_pass http://backend_cluster;
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # Bind dynamically to one of the 4 interface IPs
        proxy_bind $outbound_proxy_ip transparent;
        
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

7. Verification and Load-Test Validation

After applying the configuration changes, validate that the system scales under heavy concurrency without leaking sockets or triggering EADDRNOTAVAIL.

Synthetic Concurrency Benchmark with wrk

Execute a sustained load test against your reverse proxy:

wrk -t12 -c400 -d60s https://api.example.com/health

While the test is executing, monitor the socket states in another terminal:

watch -n 1 'ss -s && ss -tan state time-wait | wc -l'

Pre-Tuning vs. Post-Tuning Results

Metric Before Tuning (Default HTTP/1.0 Upstream) After Tuning (Keepalives + tcp_tw_reuse)
Max Sustainable RPS 465 req/s 42,800+ req/s
Active TIME_WAIT Sockets 28,231 (Exhausted) 140–320 (Stable)
Failed connect() Syscalls 12,410 failures (EADDRNOTAVAIL) 0 failures
HTTP 502 Bad Gateway Errors 18.4% of total requests 0.00%
Average Response Latency 240ms (Queueing stalls) 3.2ms

8. Master Configuration Quick-Reference

Save this reference table for diagnosing and hardening Linux reverse proxy environments:

Component File / Directive Setting Architectural Purpose
Kernel Ports sysctl net.ipv4.ip_local_port_range 10240 65535 Expands ephemeral port capacity from 28k to 55,295 sockets.
Socket Reuse sysctl net.ipv4.tcp_tw_reuse 1 Safely recycles TIME_WAIT sockets for outgoing upstream connections.
Timestamps sysctl net.ipv4.tcp_timestamps 1 Required by RFC 1323 for tcp_tw_reuse validation.
FIN Timeout sysctl net.ipv4.tcp_fin_timeout 15 Rapidly terminates orphan connections, saving memory.
Nginx Upstream upstream { keepalive 64; } 64–256 Maintains persistent idle TCP connections to backends.
Nginx Proxy proxy_http_version 1.1; 1.1 Enables persistent HTTP keepalive pipelining.
Nginx Header proxy_set_header Connection ""; "" Strips hop-by-hop Connection: close from upstream requests.
Nginx FastCGI fastcgi_keep_conn on; on Prevents closing FastCGI TCP connections after each PHP script execution.
PHP-FPM Socket listen = /run/php/php.sock Unix Socket Eliminates TCP stack overhead and port usage for local PHP execution.

Conclusion & Infrastructure Architecture

Error 99: Cannot assign requested address is never a random anomaly—it is a deterministic mathematical exhaustion of the TCP 4-tuple space.

By expanding the Linux ephemeral port range, enabling RFC-compliant tcp_tw_reuse, activating persistent Nginx upstream keepalives, and transitioning local PHP workloads to Unix Domain Sockets, you eliminate socket churn and unlock massive concurrency headroom.

For mission-critical production environments, high-volume e-commerce platforms, and latency-sensitive API backends, shared and noisy virtualized networking layers often become the ultimate bottleneck. Deploying on bare-metal hardware with unthrottled network interfaces and isolated kernel namespaces ensures your servers operate at maximum throughput. Discover bare-metal performance with Nextgen Dedicated Servers and localized, low-latency Dedicated Servers in Pakistan.

⚡ Uncapped Networking & Bare-Metal Compute

Tired of Socket Exhaustion and Network Bottlenecks on Virtual Hosts?

Power your high-throughput reverse proxies, WooCommerce stores, and microservices with enterprise dedicated network interfaces, unthrottled kernel sysctls, and dedicated hardware on Nextgen Hosting's tier-3 datacenter infrastructure.

Deploy High-Performance VPS → Explore Dedicated Hardware