An IIS site proxied through Application Request Routing that dies at almost exactly 60 seconds is not a bug in your application. It is the ARR proxy timeout, and the “exactly” is the tell: ARR’s default proxyTimeout is 120 seconds at the server level, but several templates and Azure App Service front ends inherit a 60-second idle limit upstream, and IIS’s own connection timeout defaults to 120. A WebSocket that pings every 90 seconds then gets torn down every time, with a 502 in the browser and no error in the app log.
This is a timeout-chain problem. Three independent timers govern a proxied WebSocket — the upstream idle timeout, the ARR proxy timeout, and the application’s own keep-alive — and the connection lives only as long as the shortest of them minus the ping interval. The sections below map the chain, show how to prove which timer fired, and give a value table that keeps connections alive without leaking sockets. Baseline VPS sizing for the ARR role is on the Windows VPS plan comparison.
The three timers in the chain
Timer one is system.webServer/webLimits/connectionTimeout, which defaults to 00:02:00 and applies to idle connections at the HTTP.sys level. Timer two is ARR’s proxyTimeout under system.webServer/proxy, also 120 seconds by default, measured from the last byte relayed. Timer three is whatever your app does to keep the socket warm — a WebSocket ping, a Socket.IO heartbeat, or nothing at all. For non-WebSocket requests, timer four is the request timeout inside the app itself.
The effective lifetime is not the minimum of the three but the minimum minus jitter: a WebSocket with no application-level ping is closed at 60 or 120 seconds depending on which tier resets first, and any load balancer in front with a 60-second idle timeout wins regardless of what IIS is configured to allow. Always look at the outermost hop first.
Why 60 seconds shows up so often
Sixty seconds is the default idle timeout on Azure Load Balancer and Application Gateway’s public front end, on many cloud WAFs, and on several managed proxies that sit in front of IIS. It also matches the classic squid and nginx proxy_read_timeout default. When a customer reports “the connection drops at one minute”, the ARR layer is usually innocent and the front door is the culprit.
The consequence for WebSockets is arithmetic. If the outer idle timeout is 60 seconds and your ping interval is 55, you survive. If the ping interval is 90, you never survive, no matter how generous the ARR settings are. Set the ping interval to roughly half the shortest idle timeout in the chain and the connection becomes stable without changing any proxy configuration at all.
Configuring ARR for long-lived connections
Two settings do most of the work: raise proxyTimeout on the server node so ARR does not cut the stream, and make sure enabled is true on the proxy element. In applicationHost.config, under <system.webServer><proxy>, set proxyTimeout="00:10:00", timeout="00:10:00", and leave preserveHostHeader="true". On Windows Server, install with Install-WindowsFeature Web-Server,Web-WebSockets,Web-ApplicationRequestRouting -IncludeManagementTools.
Apply with appcmd rather than hand-editing XML: appcmd.exe set config -section:system.webServer/proxy /proxyTimeout:"00:10:00" /commit:apphost. Restart the W3SVC service or recycle the app pool, because proxy settings are read at worker process start. A common failure mode is changing the site-level web.config, where system.webServer/proxy is locked and the change silently does not apply.
Proving which timer fired with netstat and TIME_WAIT
Run netstat -ano | findstr :443 on the ARR server while reproducing the drop. A connection living in ESTABLISHED until the exact cutoff and then vanishing with no FIN from the client points at a proxy-side timeout. A flood of TIME_WAIT entries on the ARR-to-backend leg, at roughly the ping interval, means the client is reconnecting every cycle and ARR is closing the upstream each time.
Count them to get the reconnect rate: (Get-NetTCPConnection -State TimeWait -RemotePort 8080).Count. If that number grows by one every 60 seconds per client, the socket is being re-established on a fixed cadence. IIS logs with cs-host and time-taken will show the matching requests at 60000-61000 ms, which is the smoking gun.
Recommended values
| Layer | Setting | Default | Recommended |
|---|---|---|---|
| Front-end LB | idle timeout | 60 s | Raise to 300 s or ping below it |
| ARR | proxyTimeout | 120 s | 600 s |
| ARR | timeout (connect) | 120 s | 30 s |
| IIS | webLimits connectionTimeout | 120 s | 600 s |
| App pool | idleTimeout | 20 min | 0 (disabled) |
| App | WS ping interval | none | ≤ half shortest idle |
| App | pong deadline | none | 2 × ping interval |
Load test before you tune in production
Reproduce the drop on a staging node with a ping interval of 90 seconds and time-taken logging enabled, then confirm the change moves the cutoff from 60 to your new value. Without that measurement you cannot distinguish an idle timeout from a genuine application exception that closes the socket, and the second problem gets masked every time you raise a number.
Keep the smallest set of changes that produces a clean 15-minute idle WebSocket in netstat, then document the chain so the next person knows which timer owns what. If your current VPS cannot hold a long-lived proxy process and log at the same time, the disk and memory limits are worth reviewing on the provider comparison table before you tune further.
The idle app pool that kills quiet WebSockets
Even with every proxy timer raised, a WebSocket-only workload looks idle to IIS. The application pool’s idleTimeout defaults to 20 minutes and recycles the worker even though sockets are open, because managed WebSocket connections do not always count as activity for the idle monitor. Set idleTimeout="00:00:00" on the pool in applicationHost.config, or via appcmd set apppool "DefaultAppPool" /idleTimeout:00:00:00.
Also disable the periodic recycle if the app tolerates it: Set-ItemProperty IIS:\AppPools\DefaultAppPool -Name recycling.periodicRestart.time -Value 0. Otherwise the 1,740-minute default recycles the pool every 29 hours and every connected client is dropped at once, on a schedule that looks random until you notice the rhythm.
Load test before you tune in production
Reproduce the drop on a staging node with a ping interval of 90 seconds and time-taken logging enabled, then confirm the change moves the cutoff from 60 to your new value. Without that measurement you cannot distinguish an idle timeout from a genuine application exception that closes the socket, and the second problem gets masked every time you raise a number.
Keep the smallest set of changes that produces a clean 15-minute idle WebSocket in netstat, then document the chain so the next person knows which timer owns what. If your current VPS cannot hold a long-lived proxy process and log at the same time, the disk and memory limits are worth reviewing on the provider comparison table before you tune further.


