IIS ARR Keep-Alive Timing Out at 60 Seconds

The symptom is consistent enough to be a fingerprint: a request through IIS Application Request Routing takes around 60 seconds, the backend app log shows no entry at all, and the client receives 502.3 or a bare timeout. Under 60 seconds it works. Change nothing and retry, and it works again until the next idle pause.

Sixty is not random. Three timers in the path default to values that converge near it, and the one that fires first wins. Unless you can name which timer dropped the connection, any fix is a guess that will come back on the next patch cycle.

This matters more on a Windows VPS than on dedicated hardware, because consolidated hosts often sit behind an extra NAT or load-balancing tier whose idle timers are shorter than any you configured inside Windows. Four timers in the chain, one expiry at 60 seconds.

When 60 seconds is correct behaviour

If the backend legitimately runs a 4-minute report, do not stretch the proxy timeout to five minutes — move the job to a queue and return 202. Long synchronous requests pin worker threads, and the 60-second drop is doing you a favour by surfacing the design problem.

Measure before and after with the same stopwatch method, record the timer values in the build document, and re-check them after every IIS or ARR update. If the front-end is also behind a shared Windows VPS network edge, confirm the provider’s idle value in writing — that number belongs in the same document.

Health checks that stop the guessing

Configure the ARR farm health test against a static endpoint that touches no database, with a 30-second interval and a two-failure threshold. The health check doubles as a keep-alive, and a failing node is removed before clients see the 502 rather than after.

Log the farm state change events, which land in the HTTPERR log with reason Timer_ConnectionIdle. That single string is the fastest confirmation that this is an idle timeout rather than an app crash, and it points at the timer to change.

When 60 seconds is correct behaviour

If the backend legitimately runs a 4-minute report, do not stretch the proxy timeout to five minutes — move the job to a queue and return 202. Long synchronous requests pin worker threads, and the 60-second drop is doing you a favour by surfacing the design problem.

Measure before and after with the same stopwatch method, record the timer values in the build document, and re-check them after every IIS or ARR update. If the front-end is also behind a shared Windows VPS network edge, confirm the provider’s idle value in writing — that number belongs in the same document.

Health checks that stop the guessing

Configure the ARR farm health test against a static endpoint that touches no database, with a 30-second interval and a two-failure threshold. The health check doubles as a keep-alive, and a failing node is removed before clients see the 502 rather than after.

Log the farm state change events, which land in the HTTPERR log with reason Timer_ConnectionIdle. That single string is the fastest confirmation that this is an idle timeout rather than an app crash, and it points at the timer to change.

When 60 seconds is correct behaviour

If the backend legitimately runs a 4-minute report, do not stretch the proxy timeout to five minutes — move the job to a queue and return 202. Long synchronous requests pin worker threads, and the 60-second drop is doing you a favour by surfacing the design problem.

Measure before and after with the same stopwatch method, record the timer values in the build document, and re-check them after every IIS or ARR update. If the front-end is also behind a shared Windows VPS network edge, confirm the provider’s idle value in writing — that number belongs in the same document.

Why the NAT tier usually wins

On virtualised hosting the guest sees its own interface, but the provider’s edge forwards traffic through a stateful device with a short idle timeout. Set ARR to 120 seconds and the NAT device still drops the mapping at 60. The trace looks like an ARR timeout because that is the layer that logs it.

Mitigate at both ends: lower the proxy timeout to below the NAT value so failures are fast and loggable, and enable application-level keep-alive so the socket is never idle for the full window. A 45-second heartbeat from the front-end to a cheap health endpoint keeps the mapping warm on most providers.

Health checks that stop the guessing

Configure the ARR farm health test against a static endpoint that touches no database, with a 30-second interval and a two-failure threshold. The health check doubles as a keep-alive, and a failing node is removed before clients see the 502 rather than after.

Log the farm state change events, which land in the HTTPERR log with reason Timer_ConnectionIdle. That single string is the fastest confirmation that this is an idle timeout rather than an app crash, and it points at the timer to change.

When 60 seconds is correct behaviour

If the backend legitimately runs a 4-minute report, do not stretch the proxy timeout to five minutes — move the job to a queue and return 202. Long synchronous requests pin worker threads, and the 60-second drop is doing you a favour by surfacing the design problem.

Measure before and after with the same stopwatch method, record the timer values in the build document, and re-check them after every IIS or ARR update. If the front-end is also behind a shared Windows VPS network edge, confirm the provider’s idle value in writing — that number belongs in the same document.

Why the NAT tier usually wins

On virtualised hosting the guest sees its own interface, but the provider’s edge forwards traffic through a stateful device with a short idle timeout. Set ARR to 120 seconds and the NAT device still drops the mapping at 60. The trace looks like an ARR timeout because that is the layer that logs it.

Mitigate at both ends: lower the proxy timeout to below the NAT value so failures are fast and loggable, and enable application-level keep-alive so the socket is never idle for the full window. A 45-second heartbeat from the front-end to a cheap health endpoint keeps the mapping warm on most providers.

Health checks that stop the guessing

Configure the ARR farm health test against a static endpoint that touches no database, with a 30-second interval and a two-failure threshold. The health check doubles as a keep-alive, and a failing node is removed before clients see the 502 rather than after.

Log the farm state change events, which land in the HTTPERR log with reason Timer_ConnectionIdle. That single string is the fastest confirmation that this is an idle timeout rather than an app crash, and it points at the timer to change.

When 60 seconds is correct behaviour

If the backend legitimately runs a 4-minute report, do not stretch the proxy timeout to five minutes — move the job to a queue and return 202. Long synchronous requests pin worker threads, and the 60-second drop is doing you a favour by surfacing the design problem.

Measure before and after with the same stopwatch method, record the timer values in the build document, and re-check them after every IIS or ARR update. If the front-end is also behind a shared Windows VPS network edge, confirm the provider’s idle value in writing — that number belongs in the same document.

The copy-paste fix table

SettingLocationDefaultRecommendedEffect of changing it
proxyTimeoutARR webFarm120 s90 sCaps backend wait; shorter = faster 502
app pool idle timeoutIIS App Pools20 min0 (disabled)Stops recycles on low traffic
app pool startModeIIS App PoolsOnDemandAlwaysRunningRemoves warm-up 503s
idleTimeout (preload)ASP.NET preload—AlwaysKeeps app domain alive
TCP keep-aliveBackend socketOff30 s probeDetects dead peer within 60 s
NAT / LB idle timeoutHost networkoften 60 s≥ proxyTimeoutMost common hidden cause

Change one row at a time and reproduce with a 90-second idle gap between requests. Changing all six at once produces a “fixed” system where nobody can say which value mattered, which guarantees the problem returns when a template redeploy overwrites one of them.

Why the NAT tier usually wins

On virtualised hosting the guest sees its own interface, but the provider’s edge forwards traffic through a stateful device with a short idle timeout. Set ARR to 120 seconds and the NAT device still drops the mapping at 60. The trace looks like an ARR timeout because that is the layer that logs it.

Mitigate at both ends: lower the proxy timeout to below the NAT value so failures are fast and loggable, and enable application-level keep-alive so the socket is never idle for the full window. A 45-second heartbeat from the front-end to a cheap health endpoint keeps the mapping warm on most providers.

Health checks that stop the guessing

Configure the ARR farm health test against a static endpoint that touches no database, with a 30-second interval and a two-failure threshold. The health check doubles as a keep-alive, and a failing node is removed before clients see the 502 rather than after.

Log the farm state change events, which land in the HTTPERR log with reason Timer_ConnectionIdle. That single string is the fastest confirmation that this is an idle timeout rather than an app crash, and it points at the timer to change.

When 60 seconds is correct behaviour

If the backend legitimately runs a 4-minute report, do not stretch the proxy timeout to five minutes — move the job to a queue and return 202. Long synchronous requests pin worker threads, and the 60-second drop is doing you a favour by surfacing the design problem.

Measure before and after with the same stopwatch method, record the timer values in the build document, and re-check them after every IIS or ARR update. If the front-end is also behind a shared Windows VPS network edge, confirm the provider’s idle value in writing — that number belongs in the same document.

Perfmon counters that separate the causes

Watch \Web Service(_Total)\Current Connections and \ASP.NET\Requests Queued on the backend. A flat connection count that drops to zero at the same second each minute is a proxy-side timeout. A rising request queue with stable connections is a slow backend tripping its own execution timeout.

Add \TCPv4\Connections Established on both boxes. If the front-end count exceeds the backend count by the number of idle workers, connections are being cached by ARR and dropped upstream, which is the keep-alive case rather than an app problem.

The copy-paste fix table

SettingLocationDefaultRecommendedEffect of changing it
proxyTimeoutARR webFarm120 s90 sCaps backend wait; shorter = faster 502
app pool idle timeoutIIS App Pools20 min0 (disabled)Stops recycles on low traffic
app pool startModeIIS App PoolsOnDemandAlwaysRunningRemoves warm-up 503s
idleTimeout (preload)ASP.NET preload—AlwaysKeeps app domain alive
TCP keep-aliveBackend socketOff30 s probeDetects dead peer within 60 s
NAT / LB idle timeoutHost networkoften 60 s≥ proxyTimeoutMost common hidden cause

Change one row at a time and reproduce with a 90-second idle gap between requests. Changing all six at once produces a “fixed” system where nobody can say which value mattered, which guarantees the problem returns when a template redeploy overwrites one of them.

Why the NAT tier usually wins

On virtualised hosting the guest sees its own interface, but the provider’s edge forwards traffic through a stateful device with a short idle timeout. Set ARR to 120 seconds and the NAT device still drops the mapping at 60. The trace looks like an ARR timeout because that is the layer that logs it.

Mitigate at both ends: lower the proxy timeout to below the NAT value so failures are fast and loggable, and enable application-level keep-alive so the socket is never idle for the full window. A 45-second heartbeat from the front-end to a cheap health endpoint keeps the mapping warm on most providers.

Health checks that stop the guessing

Configure the ARR farm health test against a static endpoint that touches no database, with a 30-second interval and a two-failure threshold. The health check doubles as a keep-alive, and a failing node is removed before clients see the 502 rather than after.

Log the farm state change events, which land in the HTTPERR log with reason Timer_ConnectionIdle. That single string is the fastest confirmation that this is an idle timeout rather than an app crash, and it points at the timer to change.

When 60 seconds is correct behaviour

If the backend legitimately runs a 4-minute report, do not stretch the proxy timeout to five minutes — move the job to a queue and return 202. Long synchronous requests pin worker threads, and the 60-second drop is doing you a favour by surfacing the design problem.

Measure before and after with the same stopwatch method, record the timer values in the build document, and re-check them after every IIS or ARR update. If the front-end is also behind a shared Windows VPS network edge, confirm the provider’s idle value in writing — that number belongs in the same document.

Finding it with failed request tracing

Enable tracing on the site with appcmd set config "Default Web Site" -section:system.webServer/tracing -traceFailedRequestsLogging.enabled:true, add a provider for status 502, and reproduce with a stopwatch. The XML trace shows the ARR module entry, the backend connection attempt, and the exact timestamp of the abort.

Read the delta between the last ARR event and the failure. If it is almost exactly 60000 ms with no backend response logged, the proxy gave up. If it is 60000 ms with a connection reset from the backend, the app pool or the backend’s own request timeout is the culprit, and ARR is just the messenger.

%windir%\system32\inetsrv\appcmd.exe list config -section:webFarms
netsh http show servicestate | findstr /i "request queue"
Get-Counter '\Web Service(_Total)\Current Connections' -SampleInterval 5

Perfmon counters that separate the causes

Watch \Web Service(_Total)\Current Connections and \ASP.NET\Requests Queued on the backend. A flat connection count that drops to zero at the same second each minute is a proxy-side timeout. A rising request queue with stable connections is a slow backend tripping its own execution timeout.

Add \TCPv4\Connections Established on both boxes. If the front-end count exceeds the backend count by the number of idle workers, connections are being cached by ARR and dropped upstream, which is the keep-alive case rather than an app problem.

The copy-paste fix table

SettingLocationDefaultRecommendedEffect of changing it
proxyTimeoutARR webFarm120 s90 sCaps backend wait; shorter = faster 502
app pool idle timeoutIIS App Pools20 min0 (disabled)Stops recycles on low traffic
app pool startModeIIS App PoolsOnDemandAlwaysRunningRemoves warm-up 503s
idleTimeout (preload)ASP.NET preload—AlwaysKeeps app domain alive
TCP keep-aliveBackend socketOff30 s probeDetects dead peer within 60 s
NAT / LB idle timeoutHost networkoften 60 s≥ proxyTimeoutMost common hidden cause

Change one row at a time and reproduce with a 90-second idle gap between requests. Changing all six at once produces a “fixed” system where nobody can say which value mattered, which guarantees the problem returns when a template redeploy overwrites one of them.

Why the NAT tier usually wins

On virtualised hosting the guest sees its own interface, but the provider’s edge forwards traffic through a stateful device with a short idle timeout. Set ARR to 120 seconds and the NAT device still drops the mapping at 60. The trace looks like an ARR timeout because that is the layer that logs it.

Mitigate at both ends: lower the proxy timeout to below the NAT value so failures are fast and loggable, and enable application-level keep-alive so the socket is never idle for the full window. A 45-second heartbeat from the front-end to a cheap health endpoint keeps the mapping warm on most providers.

Health checks that stop the guessing

Configure the ARR farm health test against a static endpoint that touches no database, with a 30-second interval and a two-failure threshold. The health check doubles as a keep-alive, and a failing node is removed before clients see the 502 rather than after.

Log the farm state change events, which land in the HTTPERR log with reason Timer_ConnectionIdle. That single string is the fastest confirmation that this is an idle timeout rather than an app crash, and it points at the timer to change.

When 60 seconds is correct behaviour

If the backend legitimately runs a 4-minute report, do not stretch the proxy timeout to five minutes — move the job to a queue and return 202. Long synchronous requests pin worker threads, and the 60-second drop is doing you a favour by surfacing the design problem.

Measure before and after with the same stopwatch method, record the timer values in the build document, and re-check them after every IIS or ARR update. If the front-end is also behind a shared Windows VPS network edge, confirm the provider’s idle value in writing — that number belongs in the same document.

Who actually owns the 60 seconds

Candidate one is the ARR proxy idle timeout in applicationHost.config, under <webFarm><applicationRequestRouting>. Its proxyTimeout defaults to 120 seconds, but the health-check and connection-reuse settings around it behave differently, and many farm templates ship a value of 60.

Candidate two is the application pool idle timeout, which defaults to 20 minutes — too long to explain 60 seconds on its own, but relevant after a recycle when the worker process is still warming. Candidate three is TCP keep-alive, off by default on the backend connection, which lets an intermediate firewall drop an idle socket silently.

Finding it with failed request tracing

Enable tracing on the site with appcmd set config "Default Web Site" -section:system.webServer/tracing -traceFailedRequestsLogging.enabled:true, add a provider for status 502, and reproduce with a stopwatch. The XML trace shows the ARR module entry, the backend connection attempt, and the exact timestamp of the abort.

Read the delta between the last ARR event and the failure. If it is almost exactly 60000 ms with no backend response logged, the proxy gave up. If it is 60000 ms with a connection reset from the backend, the app pool or the backend’s own request timeout is the culprit, and ARR is just the messenger.

%windir%\system32\inetsrv\appcmd.exe list config -section:webFarms
netsh http show servicestate | findstr /i "request queue"
Get-Counter '\Web Service(_Total)\Current Connections' -SampleInterval 5

Perfmon counters that separate the causes

Watch \Web Service(_Total)\Current Connections and \ASP.NET\Requests Queued on the backend. A flat connection count that drops to zero at the same second each minute is a proxy-side timeout. A rising request queue with stable connections is a slow backend tripping its own execution timeout.

Add \TCPv4\Connections Established on both boxes. If the front-end count exceeds the backend count by the number of idle workers, connections are being cached by ARR and dropped upstream, which is the keep-alive case rather than an app problem.

The copy-paste fix table

SettingLocationDefaultRecommendedEffect of changing it
proxyTimeoutARR webFarm120 s90 sCaps backend wait; shorter = faster 502
app pool idle timeoutIIS App Pools20 min0 (disabled)Stops recycles on low traffic
app pool startModeIIS App PoolsOnDemandAlwaysRunningRemoves warm-up 503s
idleTimeout (preload)ASP.NET preload—AlwaysKeeps app domain alive
TCP keep-aliveBackend socketOff30 s probeDetects dead peer within 60 s
NAT / LB idle timeoutHost networkoften 60 s≥ proxyTimeoutMost common hidden cause

Change one row at a time and reproduce with a 90-second idle gap between requests. Changing all six at once produces a “fixed” system where nobody can say which value mattered, which guarantees the problem returns when a template redeploy overwrites one of them.

Why the NAT tier usually wins

On virtualised hosting the guest sees its own interface, but the provider’s edge forwards traffic through a stateful device with a short idle timeout. Set ARR to 120 seconds and the NAT device still drops the mapping at 60. The trace looks like an ARR timeout because that is the layer that logs it.

Mitigate at both ends: lower the proxy timeout to below the NAT value so failures are fast and loggable, and enable application-level keep-alive so the socket is never idle for the full window. A 45-second heartbeat from the front-end to a cheap health endpoint keeps the mapping warm on most providers.

Health checks that stop the guessing

Configure the ARR farm health test against a static endpoint that touches no database, with a 30-second interval and a two-failure threshold. The health check doubles as a keep-alive, and a failing node is removed before clients see the 502 rather than after.

Log the farm state change events, which land in the HTTPERR log with reason Timer_ConnectionIdle. That single string is the fastest confirmation that this is an idle timeout rather than an app crash, and it points at the timer to change.

When 60 seconds is correct behaviour

If the backend legitimately runs a 4-minute report, do not stretch the proxy timeout to five minutes — move the job to a queue and return 202. Long synchronous requests pin worker threads, and the 60-second drop is doing you a favour by surfacing the design problem.

Measure before and after with the same stopwatch method, record the timer values in the build document, and re-check them after every IIS or ARR update. If the front-end is also behind a shared Windows VPS network edge, confirm the provider’s idle value in writing — that number belongs in the same document.

Leave a Comment