Skip to content

Technical deep dive

IIS DDoS Hardening: Dynamic IP Restrictions, Request Filtering and App Pool Queues

Last updated: August 2026 · Dynamic IP Restrictions, Request Filtering and app pool queue · Reading time ~19 min

A holding queue in front of a hall: arrivals wait in a bounded pen with a counter at the door, and once the pen is full new arrivals are turned away rather than crushing the hall behind it.

IIS defends above the kernel http.sys queue, at the application layer. The load-bearing features are Dynamic IP Restrictions, Request Filtering and the application-pool queue: per-source rate and concurrency limits, request-size caps, a bounded queue and rapid-fail protection. Configured in web.config or appcmd, verified with the Web Service performance counters, sized against your own baseline.

This guide hardens IIS itself against the request- and connection-exhausting part of a DDoS attack, with no device in front of it. IIS sits one layer above http.sys — the kernel request queue covered in the Windows Server hardening guide — and defends at the application layer: per-source limits, request-size caps, and a bounded worker queue that fails predictably instead of thrashing.

Every setting comes as both a web.config fragment and an appcmd or PowerShell command, with the performance counter that shows it working.

Attack shapes and the IIS features that answer them
ApplianceAttack shapeIIS featureVerification
Request flood from few sourcesDynamic IP Restrictions: DenyByRequestRate\Web Service\Total Requests; DIPR logs 429
Many connections per sourceDynamic IP Restrictions: maxConnections\Web Service\Current Connections
Slow-header (Slowloris)http.sys headerWaitTimeout; connectionTimeoutHTTP Service Request Queues counters
Large-request abuseRequest Filtering: requestLimits404.13 / 404.14 / 404.15 in the log
Worker exhaustion / crash loopApp pool queueLength; rapid-fail protection\W3SVC_W3WP\Requests / Sec; 503 rate

IIS sits above http.sys (the kernel queue covered in the Windows guide). These features defend the application; none of them helps once the attack fills the circuit ahead of the server.

0. Baseline first: read the Web Service counters

IIS state lives in the Web Service and W3SVC_W3WP performance counter sets. Record a normal week:

# Current connections and request rate across all sites
Get-Counter '\Web Service(_Total)\Current Connections',
            '\Web Service(_Total)\Total Method Requests/sec' -SampleInterval 5 -MaxSamples 3

# Per-worker request rate and the http.sys queue
Get-Counter '\W3SVC_W3WP(*)\Requests / Sec',
            '\HTTP Service Request Queues(*)\CurrentQueueSize'

# Per-client request counts from the IIS log (adjust the field index to your log format)
Import-Csv -Delimiter ' ' C:\inetpub\logs\LogFiles\W3SVC1\*.log -Header (1..20) |
  Group-Object 'H10' | Sort-Object Count -Descending | Select-Object -First 10

Current Connections and the per-client request rate are the baselines every threshold below is measured against.

1. Dynamic IP Restrictions: per-source rate and concurrency

Dynamic IP Restrictions (DIPR) is the IIS request-flood defence, the built-in equivalent of nginx’s limit_req. It requires the IP and Domain Restrictions role service, then a rate and a concurrency limit:

<!-- web.config or applicationHost.config -->
<system.webServer>
  <security>
    <dynamicIpSecurity enableLoggingOnlyMode="true">
      <!-- concurrency: max simultaneous requests per source -->
      <denyByConcurrentRequests enabled="true" maxConcurrentRequests="20" />
      <!-- rate: max requests per source per time window (ms) -->
      <denyByRequestRate enabled="true" maxRequests="100" requestIntervalInMilliseconds="2000" />
    </dynamicIpSecurity>
  </security>
</system.webServer>
# Same via appcmd
appcmd set config /section:system.webServer/security/dynamicIpSecurity `
  /denyByRequestRate.enabled:true /denyByRequestRate.maxRequests:100 `
  /denyByRequestRate.requestIntervalInMilliseconds:2000

# Deny with 429 so logs and clients read it correctly (default is 403)
appcmd set config /section:system.webServer/security/dynamicIpSecurity `
  /denyAction:"TooManyRequests"

Note enableLoggingOnlyMode="true" above — this is the IIS dry-run. It logs what it would block without blocking, exactly like nginx’s limit_req_dry_run. Leave it on for a representative week, confirm the sources it would deny are not your real traffic, then set it to false to enforce.

# Confirm what logging-only mode would have blocked
Get-Counter '\Web Service(_Total)\Total Requests'
# DIPR events appear in the IIS log with the deny status you configured

2. Proxy mode: the client IP behind ARR or a load balancer

DIPR keys on the connection address. Behind a proxy that is the proxy’s address unless you enable proxy mode so it reads X-Forwarded-For:

<dynamicIpSecurity>
  <denyByRequestRate enabled="true" maxRequests="100" requestIntervalInMilliseconds="2000" />
  <!-- evaluate the forwarded client address, not the proxy connection -->
  <proxyMode enabled="true" />
</dynamicIpSecurity>

Without proxy mode, every request shares one proxy bucket and the limit protects nothing — the same failure mode as a misconfigured real_ip in nginx.

3. Request Filtering: the oversized-request surface

Request Filtering caps request dimensions at a level below the application, rejecting an oversized request before a worker parses it:

<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="10485760"  <!-- 10 MB -->
                     maxUrl="4096"
                     maxQueryString="2048">
        <headerLimits>
          <add header="Content-type" sizeLimit="100" />
        </headerLimits>
      </requestLimits>
    </requestFiltering>
  </security>
</system.webServer>
appcmd set config /section:requestFiltering `
  /requestLimits.maxAllowedContentLength:10485760 /requestLimits.maxUrl:4096

Rejections appear in the log as substatus codes: 404.13 (content length), 404.14 (URL), 404.15 (query string). Watch those against your baseline:

Select-String -Path C:\inetpub\logs\LogFiles\W3SVC1\*.log -Pattern ' 404 1[345] ' | Measure-Object

4. Application-pool queue and rapid-fail protection

The application pool has its own request queue and its own crash-handling. Bound the queue so a flood fails predictably, and configure rapid-fail so a crashing app does not enter a restart loop:

# Queue length: requests allowed to wait for a worker before 503
Set-ItemProperty "IIS:\AppPools\DefaultAppPool" -Name queueLength -Value 4000

# Rapid-fail: take the pool offline after N failures in an interval, don't thrash
Set-ItemProperty "IIS:\AppPools\DefaultAppPool" -Name failure.rapidFailProtection -Value $true
Set-ItemProperty "IIS:\AppPools\DefaultAppPool" -Name failure.rapidFailProtectionMaxCrashes -Value 5
Set-ItemProperty "IIS:\AppPools\DefaultAppPool" -Name failure.rapidFailProtectionInterval -Value "00:05:00"

# Recycle on a schedule, not on a fixed request count, to avoid mid-attack recycles
Set-ItemProperty "IIS:\AppPools\DefaultAppPool" -Name recycling.periodicRestart.requests -Value 0

# CPU throttle: cap or throttle the pool rather than let one site starve the box
Set-ItemProperty "IIS:\AppPools\DefaultAppPool" -Name cpu.limit -Value 80000   # 80%
Set-ItemProperty "IIS:\AppPools\DefaultAppPool" -Name cpu.action -Value Throttle
# 503s from a full queue or an offline pool
Get-Counter '\W3SVC_W3WP(*)\Total HTTP Requests Served'
Select-String -Path C:\inetpub\logs\LogFiles\W3SVC1\*.log -Pattern ' 503 ' | Measure-Object

A 503 rate climbing while workers look idle means the queue or http.sys is rejecting upstream of the worker — the signature that the request rate has outgrown what the pool is allowed to absorb.

5. Connection timeouts

The connection timeout and the http.sys header-wait timeout evict slow connections. The header-wait timeout is the IIS-facing Slowloris defence and is set at the http.sys layer:

# Site connection timeout (idle)
Set-WebConfigurationProperty -Filter system.applicationHost/sites/siteDefaults/limits `
  -Name connectionTimeout -Value "00:01:00"

# http.sys header wait timeout — registry, HTTP service parameters
# (covered at the OS layer in the Windows guide; this is the IIS-relevant key)

The signals to wire into monitoring

Get-Counter @(
  '\Web Service(_Total)\Current Connections',
  '\Web Service(_Total)\Total Method Requests/sec',
  '\HTTP Service Request Queues(_Total)\CurrentQueueSize',
  '\HTTP Service Request Queues(_Total)\RejectedRequests',
  '\W3SVC_W3WP(_Total)\Requests / Sec'
) -SampleInterval 5 -MaxSamples 1

Current Connections rising with a flat request rate is a connection-hold attack; RejectedRequests climbing is http.sys shedding load before IIS; a 503 rate with idle workers is the app-pool queue full. Each points at a different limit.

The honest limit of IIS hardening

Everything here defends the application edge against request-rate and connection exhaustion. Two cases sit outside it.

The first is circuit saturation: volume that fills the pipe never reaches http.sys, let alone IIS.

Where the on-premise layer stops being able to help Your 10 Gbps access circuit Circuit already saturated — upstream only On-premise appliance mitigates 2 Gbps 8 Gbps 25 Gbps 120 Gbps 1 Tbps+ Attack volume (log scale)
Below the circuit, IIS's limits have a share; above it, no setting means anything.

The second is the layer beneath IIS — the Windows TCP stack, WFP and http.sys queue, which a request passes before a worker sees it, covered in the Windows Server hardening guide. IIS hardening assumes that layer is in place. Above the circuit, the answer is the network tier; IIS keeps the application edge standing until then.

Order of application

  1. Record the baseline (Section 0): connections, request rate, http.sys queue.
  2. Install IP and Domain Restrictions; add DIPR rate and concurrency in logging-only mode.
  3. Enable proxy mode if there is any proxy in front, before trusting the limits.
  4. Observe logging-only for a week; then enforce.
  5. Add Request Filtering limits.
  6. Bound the app-pool queue and set rapid-fail protection and CPU throttle.
  7. Wire the counters into monitoring.

Step 3 comes before enforcement for the same reason it does in nginx: a per-source limit keyed on the proxy address protects nothing while looking like protection.

Frequently asked questions

Dynamic IP Restrictions or a firewall rule — which does the rate limiting?
Dynamic IP Restrictions, for anything HTTP-aware. A Windows Firewall rule blocks an address wholesale; DIPR counts requests and concurrent connections per source over a window and returns a configurable status once a source crosses the threshold, which is what you want for a request flood that looks like normal HTTP. It is the built-in IIS equivalent of nginx's limit_req. Install the IP and Domain Restrictions role service, then set DenyByRequestRate and DenyByConcurrentRequests, and choose a deny status of 429 so clients and logs read it correctly.
What does Request Filtering actually stop?
The oversized-request surface, cheaply and early. requestLimits caps content length, URL length, query-string length and individual header size, so a request built to exhaust memory or a parser is rejected by http.sys-level filtering before a worker processes it. It will not stop a flood of normal-sized requests — that is Dynamic IP Restrictions' job — but it closes the class of attack that sends one enormous request, and it costs nothing to run.
How does the application-pool queue relate to the http.sys queue?
They are two queues in sequence. http.sys, the kernel driver, accepts connections and queues requests first; the application pool then has its own queueLength for requests waiting on a worker. Under a flood, http.sys fills first and starts rejecting with a 503 before IIS worker processes are even involved, which is why a site can return 503s while the worker looks idle. The Windows guide covers the http.sys side; here you bound the app-pool queue and configure rapid-fail protection so a crashing worker does not enter a restart loop under load.
What is rapid-fail protection and why does it matter in an attack?
It stops IIS from restarting a worker process endlessly when the application is failing. Under a DDoS that pushes an app into repeated crashes, a worker that restarts every few seconds consumes CPU on startup and never serves traffic. Rapid-fail protection takes the pool offline after a set number of failures in an interval and returns 503, which is a cleaner failure than a restart loop — it fails predictably instead of thrashing. Tune the failure count and interval so a genuine transient does not trip it, but an attack does.
Where does the client IP come from behind ARR or a load balancer?
From the X-Forwarded-For header, and Dynamic IP Restrictions must be told to read it or every request is attributed to the proxy. IIS exposes this as the "Proxy Mode" option in Dynamic IP Restrictions, which makes it evaluate the forwarded client address instead of the connection address. Without it, one proxy address shares a single rate bucket and the limit is meaningless — the same failure mode as nginx keyed on the wrong address.
Do these settings stop a volumetric attack?
No. If the attack fills the circuit in front of the server, neither http.sys nor IIS receives the requests and no setting applies. Everything here defends against request-rate and connection exhaustion at the application edge. Volume above the circuit is a network-tier problem and is not solved in web.config.

Published: August 2026

This guide is updated as vendors release new models and pricing. How we compare vendors