🖥️ IIS Interview Questions
40 in-depth questions covering IIS architecture, SSL/TLS, application pools, URL Rewrite, ARR load balancing, security hardening, PowerShell automation, performance tuning, and CI/CD — with theory, real configs, real-world scenarios, and common mistakes.
IIS (Internet Information Services) processes requests through a two-tier architecture:
1. Kernel Mode — HTTP.sys
- A Windows kernel-mode driver that listens for HTTP/HTTPS requests directly.
- Performs URL routing, SSL termination, kernel-mode caching, and connection management.
- Extremely fast — responses served from kernel cache never touch user-mode code.
- Queues requests to the correct application pool's request queue.
2. User Mode — W3WP.exe (Worker Process)
- Each application pool runs as a separate
w3wp.exeprocess with its own memory space. - The IIS pipeline (modules) processes the request: authentication, authorization, URL rewrite, compression, static files, logging.
- ISAPI filters/handlers (legacy) or IIS modules (modern) execute in the pipeline.
ASP.NET Core Module (ANCM) — two hosting models:
- In-Process (default, recommended) — ASP.NET Core runs inside w3wp.exe. Fastest option (~3× faster than out-of-process). Uses
IISHttpServerinstead of Kestrel. - Out-of-Process — IIS reverse-proxies to a separate Kestrel process. More isolation but adds a network hop.
<!-- web.config — In-Process hosting (default, fastest) -->
<configuration>
<system.webServer>
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
hostingModel="InProcess"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout" />
</system.webServer>
</configuration>
<!-- web.config — Out-of-Process hosting (reverse proxy to Kestrel) -->
<configuration>
<system.webServer>
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
hostingModel="OutOfProcess"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</configuration>
# PowerShell — Check IIS worker processes
Get-Process w3wp | Select-Object Id, CPU, WorkingSet64,
@{Name="AppPool";Expression={
(Get-WmiObject Win32_Process -Filter "ProcessId=$($_.Id)").CommandLine
}}
# PowerShell — List all application pools and their state
Import-Module WebAdministration
Get-ChildItem IIS:\AppPools | Select-Object Name, State, ManagedRuntimeVersion
# Check which hosting model is active
Get-WebConfigurationProperty -Filter "system.webServer/aspNetCore" `
-PSPath "IIS:\Sites\MySite" -Name "hostingModel"
A team deployed an ASP.NET Core 8 API on IIS and noticed 3× slower response times compared to Kestrel standalone. Investigation revealed the web.config had hostingModel="OutOfProcess" — every request went through an extra HTTP hop (IIS → Kestrel). Changing to InProcess eliminated the proxy hop, matching Kestrel's raw performance while retaining IIS features (Windows Auth, IP restrictions, request filtering).
What is the difference between HTTP.sys and Kestrel? Can you use HTTP.sys without IIS?
SSL/TLS in IIS involves several layers:
1. Certificate Binding
- SSL certificates are bound to a site + port + IP combination (or hostname via SNI).
- SNI (Server Name Indication) allows multiple SSL certificates on the same IP:443 — the client sends the hostname during the TLS handshake, and IIS picks the correct certificate.
- Certificates are stored in the Windows Certificate Store (Local Machine → Personal/Web Hosting).
2. HTTPS Redirect
- The URL Rewrite Module redirects all HTTP (port 80) to HTTPS (port 443) with a 301 permanent redirect.
- Alternative: ASP.NET Core's
UseHttpsRedirection()middleware (works with any web server).
3. HSTS (HTTP Strict Transport Security)
- Response header
Strict-Transport-Security: max-age=31536000; includeSubDomains. - Tells browsers to only use HTTPS for this domain for the specified duration.
- Can be set in IIS via custom response headers or ASP.NET Core's
UseHsts().
4. TLS Hardening
- Disable TLS 1.0/1.1 (vulnerable) — keep only TLS 1.2 and 1.3.
- Configure cipher suite order — prefer ECDHE + AES-GCM.
- Configured via Windows Registry, Group Policy, or tools like IIS Crypto.
# ── PowerShell: Bind SSL certificate to a site ──
Import-Module WebAdministration
# Get certificate thumbprint
$cert = Get-ChildItem Cert:\LocalMachine\My |
Where-Object { $_.Subject -like "*example.com*" }
# Bind HTTPS with SNI (multiple certs on same IP:443)
New-WebBinding -Name "MySite" -Protocol "https" -Port 443 `
-HostHeader "www.example.com" -SslFlags 1 # 1 = SNI enabled
$binding = Get-WebBinding -Name "MySite" -Protocol "https"
$binding.AddSslCertificate($cert.Thumbprint, "My")
# ── web.config: HTTPS Redirect via URL Rewrite ──
# <system.webServer>
# <rewrite>
# <rules>
# <rule name="HTTPS Redirect" stopProcessing="true">
# <match url="(.*)" />
# <conditions>
# <add input="{HTTPS}" pattern="^OFF$" />
# </conditions>
# <action type="Redirect" url="https://{HTTP_HOST}/{R:1}"
# redirectType="Permanent" />
# </rule>
# </rules>
# </rewrite>
# </system.webServer>
# ── web.config: Add HSTS header ──
# <system.webServer>
# <httpProtocol>
# <customHeaders>
# <add name="Strict-Transport-Security"
# value="max-age=31536000; includeSubDomains; preload" />
# </customHeaders>
# </httpProtocol>
# </system.webServer>
# ── PowerShell: Disable TLS 1.0 and 1.1 ──
# Disable TLS 1.0
New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" -Force
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" `
-Name "Enabled" -Value 0 -Type DWord
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" `
-Name "DisabledByDefault" -Value 1 -Type DWord
# Disable TLS 1.1
New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server" -Force
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server" `
-Name "Enabled" -Value 0 -Type DWord
# Verify enabled protocols
Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server"
A PCI DSS audit flagged a payment API for supporting TLS 1.0. The team used IIS Crypto (GUI tool) to disable TLS 1.0/1.1 and weak cipher suites, then verified with SSL Labs (ssllabs.com) — score improved from C to A+. They also enabled HSTS with preload and submitted the domain to the browser preload list, preventing any HTTP downgrade attacks.
What is SNI and why was it introduced? What happens if a client does not support SNI?
An application pool is an isolation boundary in IIS. Each pool runs one or more w3wp.exe worker processes with its own memory, identity, and lifecycle.
Key settings:
- Recycling — periodically restarts the worker process to reclaim memory and clean up resources.
- Regular time interval — default 1740 min (29 hours, intentionally not 24h to avoid same-time-every-day restarts).
- Specific times — e.g., 3:00 AM during low traffic.
- Memory-based — recycle when virtual/private memory exceeds a threshold.
- Overlapped recycling — new worker starts before old one shuts down (zero-downtime restarts).
- Identity — the Windows account the worker process runs under:
ApplicationPoolIdentity(default, recommended) — virtual account per pool, least privilege.NetworkService— shared across pools, has network access.LocalSystem— full admin rights — never use in production.- Custom domain account — for accessing network resources (file shares, SQL Server with Windows Auth).
- Idle Timeout — shuts down the worker if no requests for 20 min (default). Set to 0 for always-on apps. Or use Idle Timeout Action: Suspend (IIS 8.5+) to suspend instead of terminate — faster resume.
- Rapid-Fail Protection — if a worker crashes X times within Y minutes, the pool is stopped to prevent crash loops. Default: 5 failures in 5 minutes.
# ── PowerShell: Create and configure an application pool ──
Import-Module WebAdministration
# Create app pool
New-WebAppPool -Name "MyAppPool"
# Set .NET CLR version (empty string = No Managed Code for .NET Core)
Set-ItemProperty "IIS:\AppPools\MyAppPool" -Name "managedRuntimeVersion" -Value ""
# Set identity to ApplicationPoolIdentity (most secure default)
Set-ItemProperty "IIS:\AppPools\MyAppPool" `
-Name "processModel.identityType" -Value "ApplicationPoolIdentity"
# ── Recycling Configuration ──
# Disable regular time interval recycling
Set-ItemProperty "IIS:\AppPools\MyAppPool" `
-Name "recycling.periodicRestart.time" -Value "00:00:00"
# Set scheduled recycling at 3 AM
Set-ItemProperty "IIS:\AppPools\MyAppPool" `
-Name "recycling.periodicRestart.schedule" -Value @{value="03:00:00"}
# Recycle when private memory exceeds 2 GB
Set-ItemProperty "IIS:\AppPools\MyAppPool" `
-Name "recycling.periodicRestart.privateMemory" -Value 2097152 # KB
# ── Idle Timeout ──
# Set to 0 for always-on (no idle shutdown)
Set-ItemProperty "IIS:\AppPools\MyAppPool" `
-Name "processModel.idleTimeout" -Value "00:00:00"
# Or use Suspend action (faster resume, IIS 8.5+)
Set-ItemProperty "IIS:\AppPools\MyAppPool" `
-Name "processModel.idleTimeoutAction" -Value "Suspend"
# ── Rapid-Fail Protection ──
# 5 failures within 5 minutes stops the pool
Set-ItemProperty "IIS:\AppPools\MyAppPool" `
-Name "failure.rapidFailProtection" -Value $true
Set-ItemProperty "IIS:\AppPools\MyAppPool" `
-Name "failure.maxFailures" -Value 5
Set-ItemProperty "IIS:\AppPools\MyAppPool" `
-Name "failure.resetInterval" -Value "00:05:00"
# ── Start Mode: AlwaysRunning (IIS 8+) ──
Set-ItemProperty "IIS:\AppPools\MyAppPool" -Name "startMode" -Value "AlwaysRunning"
# Verify all settings
Get-ItemProperty "IIS:\AppPools\MyAppPool" | Select-Object *
A banking web app experienced periodic 10-second hangs at 2:29 PM daily. Investigation revealed the default 29-hour recycling window had drifted to overlap with peak trading hours. The fix: disable interval-based recycling, schedule recycling at 3 AM, set idle timeout to 0 (always-on), and enable Application Initialization to warm up the app before serving traffic. Downtime during recycles dropped from 10 seconds to zero (overlapped recycling).
What is Application Initialization in IIS? How does it ensure zero-downtime during app pool recycling?
IIS performance tuning involves multiple layers:
1. Compression
- Static compression — compresses files once, caches the result on disk. Enable for CSS, JS, HTML, SVG. Near-zero CPU cost after first request.
- Dynamic compression — compresses responses on-the-fly (API JSON, dynamic HTML). Trades CPU for bandwidth. Use Brotli (br) over Gzip for 15-20% smaller payloads.
- Set
dynamicCompressionBeforeCachecarefully — compressing before output caching wastes CPU.
2. Kernel-Mode Caching (HTTP.sys cache)
- Responses are served directly from the Windows kernel — never touches w3wp.exe.
- Up to 10× faster than user-mode caching. Works for static files and anonymous GET requests.
- Conditions: response must have cache-friendly headers, no authentication, no query string variance (configurable).
3. HTTP/2
- IIS 10 (Windows Server 2016+) supports HTTP/2 over TLS by default.
- Benefits: multiplexed streams (no head-of-line blocking), header compression (HPACK), server push.
- Requires HTTPS — HTTP/2 is negotiated via ALPN during the TLS handshake.
4. Connection & Queue Limits
- Queue length — max requests waiting in the app pool queue. Default 1000. Increase for bursty traffic.
- Max concurrent connections — HTTP.sys level. Default unlimited, but OS limits apply.
- Max worker processes (Web Garden) — multiple w3wp.exe per pool. Rarely beneficial — use with care (breaks in-memory session state).
<!-- web.config: Enable static + dynamic compression -->
<configuration>
<system.webServer>
<!-- Compression -->
<urlCompression doStaticCompression="true" doDynamicCompression="true" />
<httpCompression>
<dynamicTypes>
<add mimeType="application/json" enabled="true" />
<add mimeType="application/xml" enabled="true" />
<add mimeType="text/*" enabled="true" />
</dynamicTypes>
<staticTypes>
<add mimeType="text/*" enabled="true" />
<add mimeType="application/javascript" enabled="true" />
<add mimeType="image/svg+xml" enabled="true" />
</staticTypes>
</httpCompression>
<!-- Output Caching -->
<caching>
<profiles>
<add extension=".css" policy="CacheForTimePeriod"
kernelCachePolicy="CacheForTimePeriod" duration="01:00:00" />
<add extension=".js" policy="CacheForTimePeriod"
kernelCachePolicy="CacheForTimePeriod" duration="01:00:00" />
<add extension=".jpg" policy="CacheForTimePeriod"
kernelCachePolicy="CacheForTimePeriod" duration="24:00:00" />
</profiles>
</caching>
<!-- Static file cache headers -->
<staticContent>
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="30.00:00:00" />
</staticContent>
</system.webServer>
</configuration>
# ── PowerShell: Performance settings ──
Import-Module WebAdministration
# Increase app pool queue length for bursty traffic
Set-ItemProperty "IIS:\AppPools\MyAppPool" `
-Name "queueLength" -Value 5000
# Enable HTTP/2 (IIS 10+, enabled by default over TLS)
# Verify: curl -I --http2 https://mysite.com
# Look for: HTTP/2 200
# Monitor kernel cache hit ratio
netsh http show cachestate | Select-Object -First 50
# Monitor active requests and connections
Get-Counter "\Web Service(_Total)\Current Connections"
Get-Counter "\HTTP Service Request Queues(*)\CurrentQueueSize"
An e-commerce site on IIS served 500 product images and large JSON API responses. Enabling static compression saved 70% bandwidth on CSS/JS. Dynamic compression (Gzip) reduced API response sizes from 2MB to 300KB. Kernel-mode caching for product images served 50K req/s directly from HTTP.sys without touching w3wp.exe. Combined, these changes reduced page load time from 4.2s to 1.1s and cut hosting bandwidth costs by 60%.
What is the difference between kernel-mode caching and IIS output caching? When does kernel cache not work?
IIS troubleshooting uses several diagnostic tools:
1. Failed Request Tracing (FREB / FRT)
- Captures detailed trace logs for requests matching specific criteria (status codes, time taken, URL patterns).
- Generates XML files viewable as HTML with a complete timeline of every IIS module that touched the request.
- Zero overhead when no matching requests occur — safe for production.
- Configure via IIS Manager → Failed Request Tracing Rules, or via web.config.
2. Common HTTP Error Codes
- 500.19 — invalid web.config (bad XML, locked section). Check
applicationHost.configlocking. - 500.21 — ASP.NET Core Module not installed or wrong bitness (32-bit pool, 64-bit app).
- 502.5 — ASP.NET Core process failed to start (missing runtime, bad config, startup exception).
- 503 — application pool stopped (rapid-fail protection triggered, or manually stopped).
- 403.14 — directory browsing disabled and no default document found.
3. W3WP Crash Analysis
- Enable crash dumps via
procdumpor Windows Error Reporting. - Check Event Viewer → Application/System logs for w3wp termination events.
- Use Debug Diagnostic Tool (DebugDiag) to analyse memory dumps for hangs, memory leaks, and crashes.
- ETW (Event Tracing for Windows) with PerfView for detailed runtime diagnostics.
# ── Enable Failed Request Tracing via PowerShell ──
Import-Module WebAdministration
# Enable tracing for the site
Set-WebConfigurationProperty -Filter "system.webServer/tracing/traceFailedRequests" `
-PSPath "IIS:\Sites\MySite" -Name "enabled" -Value $true
# Add rule: trace 500 errors and slow requests (>10 seconds)
Add-WebConfigurationProperty -Filter "system.webServer/tracing/traceFailedRequests" `
-PSPath "IIS:\Sites\MySite" -Name "." -Value @{
path="*"
customActionExe=""
customActionParams=""
customActionTriggerLimit=0
}
# web.config: Failed Request Tracing configuration
# <system.webServer>
# <tracing>
# <traceFailedRequests>
# <add path="*">
# <traceAreas>
# <add provider="ASP" verbosity="Verbose" />
# <add provider="ASPNET" areas="Infrastructure,Module,Page,AppServices"
# verbosity="Verbose" />
# <add provider="WWW Server" areas="Authentication,Security,Filter,
# StaticFile,CGI,Compression,Cache,RequestNotifications,Module,
# FastCGI,WebSocket,ANCM" verbosity="Verbose" />
# </traceAreas>
# <failureDefinitions statusCodes="500-599" timeTaken="00:00:10" />
# </add>
# </traceFailedRequests>
# </tracing>
# </system.webServer>
# ── Common Troubleshooting Commands ──
# Check if app pool is running
Get-WebAppPoolState -Name "MyAppPool"
# Restart an app pool (overlapped recycling)
Restart-WebAppPool -Name "MyAppPool"
# View recent IIS log entries
Get-Content "C:\inetpub\logs\LogFiles\W3SVC1\u_ex*.log" -Tail 50
# Check for 500 errors in logs
Select-String -Path "C:\inetpub\logs\LogFiles\W3SVC1\*.log" `
-Pattern " 500 " | Select-Object -Last 20
# Enable stdout logging for ASP.NET Core startup errors
# web.config: stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout"
# Capture crash dump with procdump
# procdump -ma -e w3wp.exe C:\dumps\
# Analyse with DebugDiag or WinDbg
# Check Event Viewer for app pool crashes
Get-EventLog -LogName System -Source "WAS" -Newest 10 |
Format-List TimeGenerated, Message
A production API was returning 502.5 errors after deployment. The team enabled stdoutLogEnabled in web.config and found the ASP.NET Core app was crashing on startup due to a missing appsettings.Production.json. Failed Request Tracing showed the ANCM module receiving a process exit code of 1. The fix: add the missing config file and set ASPNETCORE_ENVIRONMENT to Production. FREB logs showed the exact module and timestamp where the failure occurred — diagnosis took 5 minutes instead of hours.
How do you configure proactive health monitoring in IIS? What is Application Request Routing (ARR) and how does it enable load balancing?
Three .NET web servers, each designed for different scenarios:
IIS (Internet Information Services)
- Full-featured Windows web server for production deployments.
- Features: SSL termination, Windows Auth, request filtering, app pool isolation, kernel-mode caching, ARR load balancing.
- Runs as a Windows Service — always on, managed by the OS.
- Hosts ASP.NET Core via the ASP.NET Core Module (ANCM).
IIS Express
- Lightweight, user-mode version of IIS for development only.
- No admin rights needed. Runs per-user, per-project.
- Supports most IIS features (SSL, Windows Auth) without installing full IIS.
- Used by Visual Studio for F5 debugging (legacy — now Kestrel is default).
Kestrel
- Cross-platform, high-performance web server built into ASP.NET Core.
- Default server for all .NET 6+ projects.
- In production, should sit behind a reverse proxy (IIS, Nginx, Apache) for security features.
- Can be used standalone for internal microservices not exposed to the internet.
# ── Development: Kestrel (default for .NET 6+) ──
dotnet new webapi -n MyApi
dotnet run
# Kestrel starts on http://localhost:5000, https://localhost:5001
# ── Development: IIS Express (Visual Studio) ──
# In launchSettings.json:
# {
# "iisSettings": {
# "iisExpress": {
# "applicationUrl": "http://localhost:5100",
# "sslPort": 44300
# }
# }
# }
# ── Production: IIS + Kestrel (In-Process) ──
# Publish → deploy to IIS site folder
dotnet publish -c Release -o ./publish
# web.config: hostingModel="InProcess"
# IIS runs the app inside w3wp.exe
# ── Production: IIS as Reverse Proxy (Out-of-Process) ──
# web.config: hostingModel="OutOfProcess"
# IIS → forwards to → Kestrel (separate process)
# ── Production: Kestrel standalone (internal microservice) ──
# No IIS needed — direct Kestrel
# Only for services NOT exposed to the internet
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseKestrel(options =>
{
options.ListenAnyIP(5000);
options.ListenAnyIP(5001, listenOptions => listenOptions.UseHttps());
});
A development team used IIS Express during local development for Windows Authentication testing. In production, the public-facing API ran behind IIS (in-process hosting) for SSL termination, request filtering, and Windows Auth. Internal microservices used Kestrel standalone behind a Kubernetes ingress controller since they ran on Linux containers.
Can Kestrel handle production traffic without a reverse proxy? What are the security risks?
IIS organises content in a three-level hierarchy:
Site
- Top-level container. Each site has its own bindings (IP, port, host header) and physical path.
- A server can host multiple sites (e.g., www.example.com and api.example.com).
- Each site has a unique Site ID and its own log files.
Application
- A grouping of content within a site that runs under a specific application pool.
- Every site has a root application ("/") by default.
- Additional applications (e.g., "/api", "/admin") can use different app pools for isolation.
- Each application has its own
web.configand can run a different .NET version.
Virtual Directory
- Maps a URL path to a physical folder that may be outside the site's root.
- Runs under the parent application's app pool — no separate isolation.
- Common use: serving shared content (images, docs) from a network share or different drive.
Key rule: An Application is always a Virtual Directory, but a Virtual Directory is not always an Application.
# ── PowerShell: Create a site ──
Import-Module WebAdministration
New-Website -Name "MyWebsite" `
-PhysicalPath "C:\inetpub\mywebsite" `
-Port 80 -HostHeader "www.example.com" `
-ApplicationPool "MyAppPool"
# ── Add an application under the site ──
# /api runs in its own app pool for isolation
New-WebApplication -Name "api" `
-Site "MyWebsite" `
-PhysicalPath "C:\apps\my-api" `
-ApplicationPool "ApiAppPool"
# URL: www.example.com/api
# ── Add a virtual directory ──
# /docs maps to a shared drive — no separate app pool
New-WebVirtualDirectory -Name "docs" `
-Site "MyWebsite" -Application "/" `
-PhysicalPath "\\fileserver\shared\docs"
# URL: www.example.com/docs
# ── View the hierarchy ──
Get-ChildItem "IIS:\Sites\MyWebsite" -Recurse |
Format-Table Name, PhysicalPath, ApplicationPool
# Result:
# Name PhysicalPath ApplicationPool
# / C:\inetpub\mywebsite MyAppPool
# /api C:\apps\my-api ApiAppPool (Application)
# /docs \fileserver\shared\docs (inherits) (Virtual Dir)
A company hosted a CMS (root "/"), a REST API ("/api"), and an admin panel ("/admin") under one site. The API and admin panel were separate Applications with their own app pools — so a memory leak in the API couldn't crash the CMS. A Virtual Directory "/shared-assets" pointed to a network share for images used by all three apps.
How does web.config inheritance work across Sites, Applications, and Virtual Directories?
A binding tells IIS which requests to route to which site. Each binding has three components:
- IP Address — specific IP or "*" (all unassigned IPs).
- Port — typically 80 (HTTP) or 443 (HTTPS).
- Host Header — the domain name (e.g., "www.example.com"). Allows multiple sites on the same IP:port.
How IIS resolves requests (priority order):
- Exact IP + exact port + exact host header (most specific)
- Exact IP + exact port + no host header
- Wildcard IP (*) + exact port + exact host header
- Wildcard IP (*) + exact port + no host header (catch-all)
HTTPS bindings require a certificate. Two approaches:
- SNI (Server Name Indication) — recommended. Multiple SSL certs on the same IP:443. Client sends hostname during TLS handshake.
- IP-based — each certificate needs its own IP address (legacy, wasteful).
Wildcard certificates (*.example.com) cover all subdomains — one cert for api.example.com, www.example.com, admin.example.com.
# ── PowerShell: Add bindings ──
Import-Module WebAdministration
# HTTP binding with host header
New-WebBinding -Name "MySite" -Protocol "http" `
-Port 80 -HostHeader "www.example.com"
# Second domain on the same site
New-WebBinding -Name "MySite" -Protocol "http" `
-Port 80 -HostHeader "example.com"
# HTTPS with SNI (multiple certs on same IP:443)
New-WebBinding -Name "MySite" -Protocol "https" `
-Port 443 -HostHeader "www.example.com" -SslFlags 1
# HTTPS with wildcard certificate
New-WebBinding -Name "MySite" -Protocol "https" `
-Port 443 -HostHeader "*.example.com" -SslFlags 1
# Bind certificate to HTTPS binding
$cert = Get-ChildItem Cert:\LocalMachine\My |
Where-Object { $_.Subject -like "*.example.com" }
$binding = Get-WebBinding -Name "MySite" -Protocol "https" `
-HostHeader "www.example.com"
$binding.AddSslCertificate($cert.Thumbprint, "My")
# View all bindings for a site
Get-WebBinding -Name "MySite" |
Format-Table Protocol, bindingInformation, sslFlags
# ── Specific IP binding (restrict to one NIC) ──
New-WebBinding -Name "AdminSite" -Protocol "https" `
-IPAddress "10.0.0.5" -Port 443 -HostHeader "admin.internal.com"
A hosting provider ran 200 customer sites on a single server. Each site had an HTTP binding with a unique host header on *:80. For HTTPS, they used SNI bindings on *:443 with individual Let's Encrypt certificates. A wildcard certificate (*.hosting.com) covered all platform subdomains. One admin site was bound to an internal-only IP address for security.
What is the difference between "All Unassigned" (*) and a specific IP in bindings? When would you use each?
IIS logs every HTTP request to text files. Understanding log configuration is essential for troubleshooting and compliance.
Log formats:
- W3C Extended (default, recommended) — space-delimited, customisable fields. Most tools (Log Parser, ELK) support it.
- IIS — fixed-format, comma-delimited. Legacy.
- NCSA — common log format from Apache world.
- Custom — define your own fields.
Key W3C log fields:
date,time— when (UTC by default)s-ip— server IP;c-ip— client IPcs-method— GET, POST, etc.cs-uri-stem— URL path;cs-uri-query— query stringsc-status— HTTP status code;sc-substatus— IIS substatussc-bytes— response size;time-taken— millisecondscs(User-Agent)— browser/bot identifier
Log file rollover: by schedule (hourly/daily/weekly) or by size. Default: daily, one file per site.
Important: time-taken includes network time, not just server processing. Logged in UTC by default.
# ── PowerShell: Configure logging ──
Import-Module WebAdministration
# Set log format to W3C
Set-WebConfigurationProperty -Filter "system.applicationHost/sites/site[@name='MySite']/logFile" `
-Name "logFormat" -Value "W3C"
# Set log directory
Set-WebConfigurationProperty -Filter "system.applicationHost/sites/site[@name='MySite']/logFile" `
-Name "directory" -Value "D:\Logs\IIS"
# Set rollover: daily
Set-WebConfigurationProperty -Filter "system.applicationHost/sites/site[@name='MySite']/logFile" `
-Name "period" -Value "Daily"
# Enable additional useful fields
Set-WebConfigurationProperty -Filter "system.applicationHost/sites/site[@name='MySite']/logFile" `
-Name "logExtFileFlags" `
-Value "Date,Time,ClientIP,UserName,ServerIP,Method,UriStem,UriQuery,HttpStatus,HttpSubStatus,Win32Status,BytesSent,BytesRecv,TimeTaken,ServerPort,UserAgent,Referer,ProtocolVersion"
# ── Sample W3C log line ──
# 2026-05-30 14:23:15 10.0.0.5 GET /api/products - 443 - 192.168.1.100
# Mozilla/5.0 https://example.com 200 0 0 45 TLSv1.2
# ── Analysing logs with PowerShell ──
# Find all 500 errors
Select-String -Path "D:\Logs\IIS\W3SVC1\*.log" -Pattern " 500 " |
Select-Object -Last 20
# Find slowest requests (time-taken > 5000ms)
Get-Content "D:\Logs\IIS\W3SVC1\u_ex260530.log" |
Where-Object { $_ -notmatch "^#" } |
ForEach-Object {
$fields = $_ -split " "
if ($fields.Count -ge 15 -and [int]$fields[14] -gt 5000) { $_ }
}
# ── Log cleanup script (keep 30 days) ──
Get-ChildItem "D:\Logs\IIS" -Recurse -Filter "*.log" |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
Remove-Item -Force
A site was running out of disk space monthly. Investigation showed IIS logs were consuming 50GB — the default log directory was on the C: drive with no cleanup policy. The fix: moved logs to D:\Logs\IIS, enabled daily rollover, created a scheduled task to delete logs older than 30 days, and shipped logs to an ELK stack for long-term analysis.
What is Enhanced Logging in IIS 8.5+? How do you log custom request/response headers?
Request Filtering is a built-in IIS security module that rejects malicious requests before they reach your application. It operates at the kernel level (HTTP.sys) for maximum performance.
Key filtering rules:
- URL length limits — rejects URLs longer than
maxUrl(default 4096 bytes). Prevents buffer overflow attempts. - Query string limits —
maxQueryString(default 2048 bytes). - Content length —
maxAllowedContentLength(default 30MB). Prevents large upload DoS. - HTTP verbs — allow only GET, POST, HEAD, etc. Block TRACE, DELETE if not needed.
- File extensions — block dangerous extensions (.exe, .bat, .config, .cs).
- Hidden segments — block access to folders like
bin,App_Data,.git,node_modules. - Double-encoded URLs — block requests with double-encoded characters (path traversal attacks).
- High-bit characters — block non-ASCII characters in URLs if not needed.
Request Filtering returns 404 substatus codes: 404.5 (URL sequence denied), 404.6 (verb denied), 404.7 (extension denied), 404.8 (hidden segment), 404.14 (URL too long), 404.15 (query string too long).
<!-- web.config: Request Filtering configuration -->
<configuration>
<system.webServer>
<security>
<requestFiltering>
<!-- URL and query string limits -->
<requestLimits maxUrl="4096"
maxQueryString="2048"
maxAllowedContentLength="52428800" /> <!-- 50MB -->
<!-- Block dangerous file extensions -->
<fileExtensions>
<add fileExtension=".exe" allowed="false" />
<add fileExtension=".bat" allowed="false" />
<add fileExtension=".cmd" allowed="false" />
<add fileExtension=".config" allowed="false" />
<add fileExtension=".cs" allowed="false" />
<add fileExtension=".csproj" allowed="false" />
</fileExtensions>
<!-- Block dangerous URL sequences -->
<denyUrlSequences>
<add sequence=".." /> <!-- path traversal -->
<add sequence=".git" /> <!-- source control -->
<add sequence="web.config" />
<add sequence="node_modules" />
</denyUrlSequences>
<!-- Allow only needed HTTP verbs -->
<verbs>
<add verb="GET" allowed="true" />
<add verb="POST" allowed="true" />
<add verb="HEAD" allowed="true" />
<add verb="OPTIONS" allowed="true" />
<add verb="TRACE" allowed="false" />
<add verb="DELETE" allowed="false" />
</verbs>
<!-- Block hidden segments -->
<hiddenSegments>
<add segment="bin" />
<add segment="App_Data" />
<add segment="App_Code" />
<add segment=".git" />
</hiddenSegments>
</requestFiltering>
</security>
</system.webServer>
</configuration>
A security audit found that an IIS server was exposing .git folders and web.config files to the internet. An attacker had already downloaded the .git directory and extracted source code containing database credentials. Adding hidden segment rules for ".git", blocking ".config" extension, and denying ".." URL sequences immediately closed these attack vectors — all without modifying application code.
What is the difference between Request Filtering and URL Rewrite for security? Can they work together?
The URL Rewrite Module modifies incoming URLs before IIS processes them (rewrite) or sends redirects back to the client (redirect).
Rewrite vs Redirect:
- Rewrite — internal server-side URL change. Client sees the original URL. Used for clean URLs, routing legacy paths.
- Redirect — sends 301/302 to the client. Browser navigates to the new URL. Used for domain canonicalization, HTTP→HTTPS.
Rule components:
- Match — regex or wildcard pattern against the URL path.
- Conditions — additional checks against server variables (HTTPS, HTTP_HOST, QUERY_STRING, HTTP_USER_AGENT).
- Action — Rewrite, Redirect (301/302/307), AbortRequest, or CustomResponse.
- stopProcessing — if true, no further rules are evaluated after a match.
Inbound rules process incoming requests. Outbound rules modify response content (rewrite links in HTML).
Rules are evaluated in order — first match wins (if stopProcessing is true).
<!-- web.config: Common URL Rewrite patterns -->
<system.webServer>
<rewrite>
<rules>
<!-- 1. HTTP → HTTPS redirect -->
<rule name="HTTPS Redirect" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="^OFF$" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}"
redirectType="Permanent" />
</rule>
<!-- 2. www canonicalization (non-www → www) -->
<rule name="Add WWW" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTP_HOST}" pattern="^example\.com$" />
</conditions>
<action type="Redirect" url="https://www.example.com/{R:1}"
redirectType="Permanent" />
</rule>
<!-- 3. Clean URLs: /products/42 → /product.aspx?id=42 -->
<rule name="Clean Product URL">
<match url="^products/(\d+)$" />
<action type="Rewrite" url="/product.aspx?id={R:1}" />
</rule>
<!-- 4. Remove trailing slash -->
<rule name="Remove Trailing Slash" stopProcessing="true">
<match url="(.+)/$" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Redirect" url="{R:1}" redirectType="Permanent" />
</rule>
<!-- 5. Block hotlinking of images -->
<rule name="Block Hotlinking" stopProcessing="true">
<match url=".*\.(jpg|jpeg|png|gif|svg)$" />
<conditions>
<add input="{HTTP_REFERER}" pattern="^$" negate="true" />
<add input="{HTTP_REFERER}" pattern="example\.com" negate="true" />
</conditions>
<action type="CustomResponse" statusCode="403" />
</rule>
</rules>
</rewrite>
</system.webServer>
A site migration changed all product URLs from /product.aspx?id=42 to /products/42. URL Rewrite rules handled both directions: an inbound rewrite mapped clean URLs to the legacy handler, and a 301 redirect sent users/bots from old URLs to new ones. SEO rankings were preserved because search engines followed the 301 redirects to the new canonical URLs.
How do outbound rules work? When would you rewrite URLs in the response body?
Application Initialization (App Init) pre-loads your application before the first request arrives, eliminating cold-start delays.
Two components work together:
- App Pool Start Mode: AlwaysRunning — worker process starts immediately when the app pool is created or recycled (instead of waiting for the first request).
- Site Preload: true — sends a fake internal request to a warmup URL, triggering your application's startup code (DI container, EF Core model compilation, cache warming).
Overlapped Recycling + App Init:
- IIS starts a new worker process.
- The new process receives the warmup request and completes initialization.
- Only after warmup completes does IIS route real traffic to the new process.
- The old process finishes existing requests and shuts down.
- Result: zero-downtime recycling.
You can also configure a custom warmup page (e.g., /health/warmup) that triggers specific initialization like database connections and cache loading.
# ── PowerShell: Configure Application Initialization ──
Import-Module WebAdministration
# Step 1: Set app pool to AlwaysRunning
Set-ItemProperty "IIS:\AppPools\MyAppPool" `
-Name "startMode" -Value "AlwaysRunning"
# Step 2: Enable preload for the site
Set-ItemProperty "IIS:\Sites\MySite" `
-Name "applicationDefaults.preloadEnabled" -Value $true
# ── web.config: Custom warmup URL ──
# <system.webServer>
# <applicationInitialization
# remapManagedRequestsTo="loading.html"
# skipManagedModules="false"
# doAppInitAfterRestart="true">
# <add initializationPage="/health/warmup" />
# <add initializationPage="/api/cache/prime" />
# </applicationInitialization>
# </system.webServer>
# remapManagedRequestsTo — shows a "loading" page to users during warmup
# doAppInitAfterRestart — re-runs warmup after every recycle
# ── ASP.NET Core warmup endpoint ──
# app.MapGet("/health/warmup", async (AppDbContext db, IMemoryCache cache) =>
# {
# // Trigger EF Core model compilation
# await db.Database.CanConnectAsync();
# // Prime caches
# var products = await db.Products.ToListAsync();
# cache.Set("products", products, TimeSpan.FromMinutes(30));
# return Results.Ok("Warmed up");
# });
# ── Verify app init is working ──
# Check if worker process is running even with no traffic
Get-Process w3wp | Select-Object Id, StartTime, WorkingSet64
# Check event log for warmup completion
Get-EventLog -LogName System -Source "WAS" -Newest 5
An insurance quoting API took 12 seconds to serve the first request after recycling — EF Core model compilation, DI container build, and rate table cache loading. After enabling Application Initialization with a /health/warmup endpoint that triggered all three, plus overlapped recycling, the first real user request after recycle was served in 50ms. A "loading.html" splash page was shown during warmup.
How does IIS Application Initialization compare to ASP.NET Core's IHostedService for warmup?
Windows Authentication (Integrated Windows Authentication) allows users to log in with their Active Directory credentials — no login form needed.
Three protocols:
- Kerberos — ticket-based, most secure. Supports delegation (server can impersonate user to access backend resources like SQL Server). Requires SPNs (Service Principal Names) configured in Active Directory. Works only within the same domain/forest.
- NTLM — challenge/response, fallback when Kerberos fails. No delegation support (double-hop problem). Works across domains and non-domain machines. Each request requires re-authentication (no ticket caching).
- Negotiate — wrapper protocol that tries Kerberos first, falls back to NTLM. This is the recommended setting in IIS.
The Double-Hop Problem:
- User → Web Server (Kerberos ticket ✅) → SQL Server (need user identity)
- With NTLM: credentials cannot be forwarded — SQL sees the app pool identity, not the user.
- With Kerberos delegation: the web server can request a ticket to SQL on behalf of the user.
- Requires Constrained Delegation configured in AD for the app pool's service account.
# ── PowerShell: Enable Windows Auth in IIS ──
Import-Module WebAdministration
# Disable Anonymous Authentication
Set-WebConfigurationProperty -Filter "/system.webServer/security/authentication/anonymousAuthentication" `
-PSPath "IIS:\Sites\MySite" -Name "enabled" -Value $false
# Enable Windows Authentication
Set-WebConfigurationProperty -Filter "/system.webServer/security/authentication/windowsAuthentication" `
-PSPath "IIS:\Sites\MySite" -Name "enabled" -Value $true
# Set providers: Negotiate (tries Kerberos first, falls back to NTLM)
# In applicationHost.config or via IIS Manager:
# <windowsAuthentication enabled="true">
# <providers>
# <add value="Negotiate" />
# <add value="NTLM" />
# </providers>
# </windowsAuthentication>
# ── Register SPN for Kerberos ──
# Required when using a custom domain account for the app pool
# Run on Domain Controller:
setspn -S HTTP/www.example.com DOMAIN\AppPoolAccount
setspn -S HTTP/webserver01 DOMAIN\AppPoolAccount
# Verify SPNs
setspn -L DOMAIN\AppPoolAccount
# ── ASP.NET Core: Access Windows identity ──
# app.MapGet("/whoami", (HttpContext ctx) =>
# {
# var identity = (WindowsIdentity)ctx.User.Identity!;
# return new
# {
# Name = identity.Name, // DOMAIN\username
# AuthType = identity.AuthenticationType, // Kerberos or NTLM
# IsKerberos = identity.AuthenticationType == "Kerberos"
# };
# });
# ── Constrained Delegation (AD) ──
# In AD: App pool account → Properties → Delegation tab
# "Trust this user for delegation to specified services only"
# Add: MSSQLSvc/sqlserver.domain.com:1433
An intranet reporting app needed to query SQL Server as the logged-in user (row-level security). Initial setup used NTLM — SQL Server always saw the app pool identity (double-hop). The fix: switched the app pool to a domain service account, registered SPNs for the hostname, and configured Constrained Delegation in Active Directory to allow delegation to the SQL Server SPN. Users' Kerberos tickets now flowed through to SQL.
What is Kerberos Constrained Delegation vs Resource-Based Constrained Delegation? When do you use each?
IIS serves static files through the StaticFileModule — one of the fastest paths because it can be served from the kernel-mode cache (HTTP.sys).
MIME Types:
- IIS only serves files with a registered MIME type. Unknown extensions return 404.3.
- Common additions needed:
.json,.woff2,.webp,.svg,.webmanifest. - Configure in
web.configunder<staticContent>.
Client-Side Caching:
Cache-Control: max-age=N— browser caches for N seconds without revalidation.Expires— absolute expiration date (older method).- Configure via
<clientCache>in web.config.
ETags and Conditional Requests:
- IIS generates an ETag (entity tag) for each file — a hash of the file's last-modified time and size.
- Browser sends
If-None-Match: etagon subsequent requests. - If file unchanged, IIS returns 304 Not Modified (no body) — saves bandwidth.
- Problem in web farms: different servers generate different ETags for the same file. Disable ETags or use consistent generation.
<!-- web.config: Static file configuration -->
<system.webServer>
<!-- MIME types for modern file formats -->
<staticContent>
<!-- Remove then add to avoid duplicates -->
<remove fileExtension=".json" />
<mimeMap fileExtension=".json" mimeType="application/json" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".woff2" mimeType="font/woff2" />
<remove fileExtension=".webp" />
<mimeMap fileExtension=".webp" mimeType="image/webp" />
<remove fileExtension=".webmanifest" />
<mimeMap fileExtension=".webmanifest"
mimeType="application/manifest+json" />
<remove fileExtension=".svg" />
<mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
<!-- Client caching: 30 days for all static files -->
<clientCache cacheControlMode="UseMaxAge"
cacheControlMaxAge="30.00:00:00" />
</staticContent>
<!-- Per-folder cache rules using location -->
<!-- Cache images for 1 year -->
<!-- <location path="images">
<system.webServer>
<staticContent>
<clientCache cacheControlMaxAge="365.00:00:00" />
</staticContent>
</system.webServer>
</location> -->
<!-- Disable caching for API responses -->
<httpProtocol>
<customHeaders>
<add name="X-Content-Type-Options" value="nosniff" />
</customHeaders>
</httpProtocol>
</system.webServer>
A site returned 404.3 errors for .woff2 font files and .webp images after deploying to a new IIS server. The MIME types were not registered. Adding the MIME mappings in web.config fixed both. They also set max-age to 365 days for versioned assets (style.v2.css) and 0 for index.html to ensure users always got the latest page but cached assets aggressively.
What is the difference between IIS kernel-mode caching and output caching? When is each used?
CORS (Cross-Origin Resource Sharing) controls which external domains can make AJAX requests to your IIS-hosted API.
How CORS works:
- Browser sends an
Originheader with the request. - Simple requests (GET/POST with standard headers) — server responds with
Access-Control-Allow-Origin. - Preflight requests (PUT/DELETE, custom headers) — browser sends an OPTIONS request first. Server must respond with allowed methods, headers, and origin.
Configuration in IIS — two approaches:
- IIS CORS Module (recommended for IIS 10+) — dedicated module with proper preflight handling.
- Custom headers in web.config — works but has limitations (can't conditionally set origins, preflight handling is manual).
- ASP.NET Core CORS middleware — handles CORS in the application layer (works with any server).
Security warning: Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true is forbidden by browsers — you must specify exact origins when using credentials.
<!-- web.config: IIS CORS Module (IIS 10+) -->
<system.webServer>
<cors enabled="true" failUnlistedOrigins="true">
<add origin="https://www.example.com"
allowCredentials="true"
maxAge="3600">
<allowHeaders allowAllRequestedHeaders="true" />
<allowMethods>
<add method="GET" />
<add method="POST" />
<add method="PUT" />
<add method="DELETE" />
</allowMethods>
</add>
<add origin="https://admin.example.com"
allowCredentials="true">
<allowHeaders allowAllRequestedHeaders="true" />
<allowMethods>
<add method="GET" />
<add method="POST" />
</allowMethods>
</add>
</cors>
</system.webServer>
<!-- Fallback: Custom headers approach (older IIS) -->
<!-- <system.webServer>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="https://www.example.com" />
<add name="Access-Control-Allow-Methods" value="GET,POST,PUT,DELETE" />
<add name="Access-Control-Allow-Headers" value="Content-Type,Authorization" />
<add name="Access-Control-Max-Age" value="3600" />
</customHeaders>
</httpProtocol>
</system.webServer> -->
# ── ASP.NET Core CORS (application layer) ──
# builder.Services.AddCors(options =>
# {
# options.AddPolicy("AllowFrontend", policy =>
# {
# policy.WithOrigins("https://www.example.com", "https://admin.example.com")
# .AllowCredentials()
# .AllowAnyHeader()
# .WithMethods("GET", "POST", "PUT", "DELETE")
# .SetPreflightMaxAge(TimeSpan.FromHours(1));
# });
# });
# app.UseCors("AllowFrontend");
A React SPA at app.example.com called an API at api.example.com. The API had CORS configured with custom headers in web.config, but PUT requests failed because IIS returned 405 for the OPTIONS preflight — the custom headers approach doesn't handle preflight automatically. Installing the IIS CORS Module and configuring it properly handled preflight requests natively, fixing the issue.
What is the difference between CORS and CSP (Content Security Policy)? How do they complement each other?
IIS configuration uses a hierarchical inheritance model. Settings flow downward and can be overridden at each level:
- machine.config — .NET framework defaults (C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config).
- applicationHost.config — IIS server-level settings (C:\Windows\System32\inetsrv\config). Defines sites, app pools, global modules.
- root web.config — .NET root-level config.
- Site web.config — site root.
- Application/Virtual Directory web.config — app or subfolder level.
Section Locking — administrators can lock sections in applicationHost.config to prevent web.config overrides:
overrideModeDefault="Deny"— prevents all sites from changing a section.overrideModeDefault="Allow"— sites can override in their web.config.<location path="MySite" overrideMode="Allow">— unlock for a specific site only.
Common locked sections: windowsAuthentication, anonymousAuthentication, handlers, modules — these are locked by default because they affect server security.
Attempting to set a locked section in web.config results in a 500.19 error with "Config Error: This configuration section cannot be used at this path."
# ── applicationHost.config: Section locking ──
# Default lock state is defined in <configSections>:
# <section name="windowsAuthentication"
# overrideModeDefault="Deny" />
# ↑ No site can enable/disable Windows Auth via web.config
# ── Unlock a section for a specific site ──
# In applicationHost.config:
# <location path="MySite" overrideMode="Allow">
# <system.webServer>
# <security>
# <authentication>
# <windowsAuthentication />
# </authentication>
# </security>
# </system.webServer>
# </location>
# ── PowerShell: Unlock/Lock sections ──
Import-Module WebAdministration
# Unlock handlers section for a specific site
Set-WebConfiguration -Filter "/system.webServer/handlers" `
-PSPath "IIS:" -Location "MySite" `
-Metadata "overrideMode" -Value "Allow"
# Lock it back
Set-WebConfiguration -Filter "/system.webServer/handlers" `
-PSPath "IIS:" -Location "MySite" `
-Metadata "overrideMode" -Value "Deny"
# ── Check effective configuration ──
# Shows merged config from all levels
Get-WebConfiguration -Filter "/system.webServer/defaultDocument" `
-PSPath "IIS:\Sites\MySite\subfolder"
# ── web.config: <location> for path-specific settings ──
# <configuration>
# <location path="admin">
# <system.webServer>
# <security>
# <authorization>
# <remove users="*" />
# <add accessType="Allow" roles="Administrators" />
# </authorization>
# </security>
# </system.webServer>
# </location>
# </configuration>
A developer tried to enable Windows Authentication in web.config and got a 500.19 error. The section was locked at the server level (overrideModeDefault="Deny"). The server admin unlocked it for that specific site using a
How do you troubleshoot which level of config is causing a problem? What tools show merged configuration?
IIS has two error-handling systems that often confuse developers:
- httpErrors (IIS-level,
<system.webServer>) — handles errors for ALL content (static files, ASP.NET, PHP). This is the recommended approach. - customErrors (ASP.NET-level,
<system.web>) — handles errors only for ASP.NET-managed requests. Does not catch errors for static files or non-.NET content.
httpErrors modes:
errorMode="Custom"— always shows custom error pages (production).errorMode="Detailed"— always shows detailed errors (development only!).errorMode="DetailedLocalOnly"— detailed for localhost, custom for remote (default).
Response modes:
File— serves a static HTML file. Fastest. No dynamic content.ExecuteURL— executes an internal URL (e.g., /errors/404.aspx). Allows dynamic pages. Must return 200 — IIS sets the actual status code.Redirect— 302 redirect to error page URL. Loses the original status code (bad for SEO).
existingResponse: Controls what happens when the app already set an error response — PassThrough (keep app's response), Replace (use IIS error page), Auto (replace only if app sent an empty error body).
<!-- web.config: httpErrors (recommended) -->
<system.webServer>
<httpErrors errorMode="Custom"
existingResponse="Auto"
defaultResponseMode="File">
<!-- Clear inherited error pages -->
<remove statusCode="404" />
<remove statusCode="500" />
<remove statusCode="403" />
<!-- Static file error pages (fastest) -->
<error statusCode="404"
path="D:\Sites\MySite\errors\404.html"
responseMode="File" />
<!-- Dynamic error page (can log, show details) -->
<error statusCode="500"
path="/errors/500.aspx"
responseMode="ExecuteURL" />
<!-- Forbidden -->
<error statusCode="403"
path="D:\Sites\MySite\errors\403.html"
responseMode="File" />
</httpErrors>
</system.webServer>
<!-- ASP.NET customErrors (legacy, .NET Framework only) -->
<!-- <system.web>
<customErrors mode="On" defaultRedirect="/errors/general">
<error statusCode="404" redirect="/errors/404" />
<error statusCode="500" redirect="/errors/500" />
</customErrors>
</system.web> -->
# ASP.NET Core: UseStatusCodePagesWithReExecute
# app.UseStatusCodePagesWithReExecute("/errors/{0}");
# app.UseExceptionHandler("/errors/500");
# // {0} is replaced with the status code
# // /errors/404 renders a friendly 404 page
# // The response keeps the correct HTTP status code
A production ASP.NET app was configured with customErrors only. When a user requested /images/logo.xyz (static file, no MIME type), IIS returned its ugly default 404 page instead of the custom one — because customErrors only handles ASP.NET requests. Switching to httpErrors with errorMode="Custom" fixed all error pages for both static and dynamic content.
How does existingResponse="PassThrough" interact with ASP.NET Core exception handling middleware?
IIS provides two layers of IP-based access control:
1. Static IP Restrictions (ipSecurity):
- Allow or deny specific IP addresses or ranges.
allowUnlisted="true"— default allow, deny specific IPs (blacklist).allowUnlisted="false"— default deny, allow specific IPs (whitelist).- Supports CIDR notation:
192.168.1.0with subnet mask255.255.255.0. - Processed in order — first match wins.
2. Dynamic IP Restrictions (dynamicIpSecurity):
- Automatically blocks IPs based on behavior patterns — no manual configuration of IPs.
- Concurrent request limit — blocks IPs making too many simultaneous requests (DoS protection).
- Request rate limit — blocks IPs exceeding N requests in a time window (brute-force protection).
- Action: Deny (403), Abort (drop connection), or log only.
- Works at the kernel level (HTTP.sys) — blocks before the request reaches managed code.
Proxy considerations: When behind a load balancer, client IPs appear as the LB's IP. Enable enableProxyMode="true" to check the X-Forwarded-For header instead.
<!-- web.config: Static IP Restrictions -->
<system.webServer>
<security>
<ipSecurity allowUnlisted="true">
<!-- Blacklist: block specific bad actors -->
<add ipAddress="203.0.113.50" allowed="false" />
<!-- Block an entire range -->
<add ipAddress="198.51.100.0"
subnetMask="255.255.255.0" allowed="false" />
</ipSecurity>
</security>
</system.webServer>
<!-- Whitelist: only allow specific IPs (admin panel) -->
<!-- <location path="admin">
<system.webServer>
<security>
<ipSecurity allowUnlisted="false">
<add ipAddress="10.0.0.0" subnetMask="255.255.0.0" allowed="true" />
<add ipAddress="192.168.1.100" allowed="true" />
</ipSecurity>
</security>
</system.webServer>
</location> -->
<!-- Dynamic IP Security: auto-block abusive IPs -->
<system.webServer>
<security>
<dynamicIpSecurity enableLoggingOnlyMode="false"
enableProxyMode="true">
<!-- Block if >20 concurrent connections from same IP -->
<denyByConcurrentRequests enabled="true"
maxConcurrentRequests="20" />
<!-- Block if >100 requests in 10 seconds from same IP -->
<denyByRequestRate enabled="true"
maxRequests="100"
requestIntervalInMilliseconds="10000" />
</dynamicIpSecurity>
</security>
</system.webServer>
# ── PowerShell: Check blocked IPs ──
# Get-WebConfigurationProperty -Filter `
# "system.webServer/security/dynamicIpSecurity" `
# -PSPath "IIS:\Sites\MySite" -Name "denyAction"
# Check IIS logs for 403.501 (dynamic) or 403.6 (static) entries
A login page was hit by a brute-force attack — thousands of POST requests per minute from rotating IPs. Static IP blocking was ineffective because IPs kept changing. Dynamic IP Security with denyByRequestRate (50 requests per 30 seconds) automatically blocked attacking IPs after they exceeded the threshold. The denyAction was set to AbortRequest to drop connections silently instead of returning 403.
How does Dynamic IP Security interact with a CDN like Cloudflare? What additional configuration is needed?
IIS uses a modular pipeline architecture. Every feature (authentication, compression, logging, static files) is implemented as a module that plugs into the request pipeline.
Native modules (C++ DLLs):
- Run in the IIS worker process (w3wp.exe) natively.
- Higher performance, lower overhead.
- Examples:
StaticFileModule,HttpLoggingModule,UriCacheModule,RequestFilteringModule. - Registered globally in
applicationHost.configunder<globalModules>.
Managed modules (.NET assemblies):
- Run inside the CLR (Common Language Runtime) within w3wp.exe.
- Written in C#/VB.NET, implement
IHttpModule. - Examples:
FormsAuthentication,Session,OutputCache,UrlAuthorization. - Configured in
web.configunder<modules>.
Pipeline events (in order): BeginRequest → AuthenticateRequest → AuthorizeRequest → ResolveRequestCache → MapRequestHandler → AcquireRequestState → ExecuteRequestHandler → ReleaseRequestState → UpdateRequestCache → LogRequest → EndRequest.
Modules subscribe to specific events. For example, WindowsAuthenticationModule subscribes to AuthenticateRequest.
# ── applicationHost.config: Global native modules ──
# <globalModules>
# <add name="StaticFileModule" image="%windir%\System32\inetsrv\static.dll" />
# <add name="HttpLoggingModule" image="%windir%\System32\inetsrv\loghttp.dll" />
# <add name="RequestFilteringModule" image="%windir%\System32\inetsrv\modrqflt.dll" />
# </globalModules>
# ── web.config: Site-level module configuration ──
# <system.webServer>
# <modules>
# <!-- Remove a module you don't need (reduces attack surface) -->
# <remove name="WebDAVModule" />
#
# <!-- Add a custom managed module -->
# <add name="MyLoggingModule"
# type="MyApp.Modules.RequestLogger, MyApp"
# preCondition="managedHandler" />
# </modules>
# </system.webServer>
# ── Custom IHttpModule (C#) ──
# public class RequestLogger : IHttpModule
# {
# public void Init(HttpApplication app)
# {
# app.BeginRequest += (s, e) =>
# {
# var ctx = ((HttpApplication)s).Context;
# var start = DateTime.UtcNow;
# ctx.Items["RequestStart"] = start;
# };
# app.EndRequest += (s, e) =>
# {
# var ctx = ((HttpApplication)s).Context;
# var start = (DateTime)ctx.Items["RequestStart"];
# var duration = DateTime.UtcNow - start;
# Debug.WriteLine($"{ctx.Request.Url} took {duration.TotalMs}ms");
# };
# }
# public void Dispose() { }
# }
# ── PowerShell: List all modules for a site ──
Import-Module WebAdministration
Get-WebManagedModule -PSPath "IIS:\Sites\MySite"
Get-WebGlobalModule | Select-Object Name, Image | Format-Table
# ── Disable modules you don't need (security hardening) ──
# Remove-WebGlobalModule -Name "WebDAVModule"
# This prevents WebDAV PUT/DELETE attacks on your server
An ASP.NET Web API returned 405 Method Not Allowed for PUT and DELETE requests. The cause: WebDAVModule was intercepting those HTTP verbs before the API handler could process them. Removing WebDAVModule and the WebDAV handler from web.config fixed the issue. The team then audited all modules and removed unused ones (CGI, ISAPI, ServerSideInclude) to reduce the attack surface.
What is the difference between preCondition="managedHandler" and no precondition? When does a managed module run for static files?
IIS Output Caching stores rendered responses so subsequent requests skip the entire processing pipeline. Two cache levels:
User-Mode Cache (w3wp.exe process memory):
- Caches dynamic responses in the worker process.
- Supports
varyByQueryString,varyByHeaders— different cached versions for different parameters. - Can cache responses from ASP.NET, PHP, or any handler.
- Cached content is lost on app pool recycle.
Kernel-Mode Cache (HTTP.sys, outside w3wp.exe):
- Responses are served directly from kernel memory — requests never reach the worker process.
- Extremely fast: 10x-100x more throughput than user-mode.
- Limitations: no authentication, no HTTPS (in older versions), no varyByHeaders except Accept-Encoding.
- Only works for anonymous, cacheable content.
Cache invalidation:
- Time-based:
duration(TTL from first cache). - File-change: cache invalidated when the underlying file changes.
- Programmatic:
HttpResponse.RemoveOutputCacheItem().
Cache rules can be set in web.config (IIS-level) or via [OutputCache] attribute (ASP.NET).
<!-- web.config: IIS Output Caching rules -->
<system.webServer>
<caching enabled="true" enableKernelCache="true">
<profiles>
<!-- Cache .aspx pages for 60 seconds, vary by query string -->
<add extension=".aspx"
policy="CacheForTimePeriod"
duration="00:01:00"
kernelCachePolicy="CacheForTimePeriod"
varyByQueryString="id,page,category" />
<!-- Cache static JSON API responses for 5 minutes -->
<add extension=".json"
policy="CacheForTimePeriod"
duration="00:05:00"
kernelCachePolicy="CacheForTimePeriod" />
<!-- Cache images until file changes -->
<add extension=".jpg"
policy="CacheUntilChange"
kernelCachePolicy="CacheUntilChange" />
<!-- Don't cache personalized content -->
<add extension=".ashx"
policy="DontCache"
kernelCachePolicy="DontCache" />
</profiles>
</caching>
</system.webServer>
# ── ASP.NET Output Cache (application layer) ──
# [OutputCache(Duration = 300, VaryByParam = "id",
# Location = OutputCacheLocation.Server)]
# public ActionResult ProductDetail(int id) { ... }
# ── Monitor cache performance ──
# Performance counters to watch:
# - "Web Service Cache\Kernel: URI Cache Hits %" — should be >80%
# - "Web Service Cache\Output Cache Current Items"
# - "Web Service Cache\Total URIs Cached"
# PowerShell: Check kernel cache entries
# netsh http show cachestate
# ── ASP.NET Core Response Caching ──
# builder.Services.AddResponseCaching();
# app.UseResponseCaching();
# [ResponseCache(Duration = 300, VaryByQueryKeys = new[] { "id" })]
# public IActionResult Get(int id) { ... }
A product listing page was hitting the database on every request — 200ms response time under load. Adding IIS Output Caching with a 30-second duration and varyByQueryString="category,page" reduced response time to 2ms for cached requests. The kernel-mode cache handled 95% of traffic because the pages were anonymous. Database load dropped by 90%.
How does IIS output caching interact with CDN caching? Can they conflict?
Application Request Routing (ARR) is an IIS extension that turns IIS into a Layer 7 load balancer and reverse proxy.
Core concepts:
- Server Farm — a group of backend servers (content servers) that ARR distributes traffic to.
- Health Check — ARR periodically pings a URL on each server. Unhealthy servers are removed from rotation.
- Load Balancing Algorithms: Round Robin, Weighted Round Robin, Least Current Requests, Least Response Time, Server Variable Hash, Query String Hash.
- Client Affinity (sticky sessions) — ensures a client always hits the same backend server using a cookie (ARRAffinity).
Reverse Proxy:
- ARR forwards requests to backend servers and returns their responses.
- Clients never see backend servers — IIS is the single entry point.
- Supports SSL offloading — HTTPS terminates at ARR, HTTP to backends.
- Preserves or rewrites
Hostheader, addsX-Forwarded-For,X-ARR-LOG-IDfor tracing.
Common uses: load balancing multiple IIS/Kestrel servers, reverse proxying to Node.js/Java apps, blue-green deployments, canary releases.
# ── PowerShell: Create ARR Server Farm ──
Import-Module WebAdministration
# Create a server farm
Add-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms" -Name "." `
-Value @{name="MyFarm"}
# Add backend servers
Add-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms/webFarm[@name='MyFarm']" `
-Name "." -Value @{address="192.168.1.10";enabled="true"}
Add-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms/webFarm[@name='MyFarm']" `
-Name "." -Value @{address="192.168.1.11";enabled="true"}
# ── applicationHost.config: ARR Configuration ──
# <webFarms>
# <webFarm name="MyFarm" enabled="true">
# <server address="192.168.1.10" enabled="true">
# <applicationRequestRouting weight="100"
# httpPort="5000" />
# </server>
# <server address="192.168.1.11" enabled="true">
# <applicationRequestRouting weight="100"
# httpPort="5000" />
# </server>
# <applicationRequestRouting>
# <protocol>
# <cache enabled="false" />
# </protocol>
# <loadBalancing algorithm="WeightedRoundRobin" />
# <healthCheck url="http://YOURSERVER/health"
# interval="00:00:30"
# responseMatch="Healthy" />
# <affinity useCookie="true"
# cookieName="ARRAffinity" />
# </applicationRequestRouting>
# </webFarm>
# </webFarms>
# ── URL Rewrite rule to route traffic to the farm ──
# <rewrite>
# <rules>
# <rule name="Route to Farm" stopProcessing="true">
# <match url=".*" />
# <action type="Rewrite" url="http://MyFarm/{R:0}" />
# </rule>
# </rules>
# </rewrite>
# ── Verify ARR is working ──
# Check X-Powered-By and ARR headers
# curl -v https://www.example.com
# Look for: X-ARR-LOG-ID in response headers
A company needed zero-downtime deployments for their ASP.NET Core app. They set up ARR with two server farms — "Blue" and "Green". During deployment, the new version was deployed to the inactive farm, health checks verified it, then a URL Rewrite rule switch routed all traffic to the new farm. Rollback was instant — just switch the rule back. ARR health checks auto-removed any server that failed the /health endpoint.
How do you configure ARR for blue-green deployments with zero-downtime switchover?
WebSocket Protocol (RFC 6455) enables full-duplex, persistent communication between client and server over a single TCP connection.
IIS WebSocket support (IIS 8.0+):
- Requires the WebSocket Protocol feature installed (Windows Feature: Web-WebSockets).
- The
IIS WebSocket Modulehandles the HTTP → WebSocket upgrade handshake. - After upgrade, the connection is persistent — no HTTP request/response overhead.
How the handshake works:
- Client sends HTTP GET with
Upgrade: websocketandConnection: Upgradeheaders. - IIS validates the request and returns 101 Switching Protocols.
- Connection is upgraded — both sides can send frames at any time.
Configuration considerations:
- Connection limits — each WebSocket holds a persistent connection. Default IIS limits may be too low.
- Idle timeout — WebSocket connections don't have the same idle timeout as HTTP. Configure
webSocketsettings to control ping/pong keepalive. - App pool recycling — kills all WebSocket connections. Use overlapped recycling and reconnection logic.
- ARR/Reverse proxy — must support WebSocket pass-through (ARR 3.0+).
- Load balancer — must support WebSocket (Layer 7 with upgrade support or Layer 4 TCP pass-through).
# ── Install WebSocket feature ──
# PowerShell (Server):
Install-WindowsFeature Web-WebSockets
# ── web.config: WebSocket settings ──
# <system.webServer>
# <webSocket enabled="true"
# receiveBufferLimit="4194304"
# pingInterval="00:00:30" />
# </system.webServer>
# ── ASP.NET Core: WebSocket middleware ──
# var builder = WebApplication.CreateBuilder(args);
# var app = builder.Build();
#
# app.UseWebSockets(new WebSocketOptions
# {
# KeepAliveInterval = TimeSpan.FromSeconds(30),
# ReceiveBufferSize = 4096
# });
#
# app.Map("/ws", async context =>
# {
# if (context.WebSockets.IsWebSocketRequest)
# {
# var ws = await context.WebSockets.AcceptWebSocketAsync();
# var buffer = new byte[4096];
# while (ws.State == WebSocketState.Open)
# {
# var result = await ws.ReceiveAsync(buffer, CancellationToken.None);
# if (result.MessageType == WebSocketMessageType.Close)
# {
# await ws.CloseAsync(WebSocketCloseStatus.NormalClosure,
# "Closing", CancellationToken.None);
# }
# else
# {
# // Echo back
# await ws.SendAsync(buffer[..result.Count],
# result.MessageType, result.EndOfMessage,
# CancellationToken.None);
# }
# }
# }
# else
# {
# context.Response.StatusCode = 400;
# }
# });
# ── ARR: Enable WebSocket proxy ──
# In applicationHost.config → system.webServer/proxy:
# <proxy enabled="true" webSocketEnabled="true" />
# ── Connection limits for WebSocket-heavy apps ──
# Increase max connections per server:
# netsh http add iplisten ipaddress=0.0.0.0
# Registry: MaxConnections under
# HKLM\System\CurrentControlSet\Services\HTTP\Parameters
A real-time dashboard used SignalR with WebSocket transport behind ARR. Connections randomly dropped every 2 minutes. The cause: ARR's default connection timeout (120s) was closing idle WebSocket connections. Fix: increased ARR timeout and enabled SignalR's built-in keepalive (ping every 15 seconds) to prevent the connection from appearing idle. Also disabled app pool idle timeout to prevent recycling during quiet hours.
How does SignalR negotiate transport (WebSocket → SSE → Long Polling) and how does IIS configuration affect each?
ASP.NET Core apps can run on IIS in two hosting models via the ASP.NET Core Module (ANCM):
In-Process Hosting (default since ASP.NET Core 3.0):
- The app runs inside the IIS worker process (w3wp.exe).
- Uses
IISHttpServerinstead of Kestrel. - Requests go: HTTP.sys → w3wp.exe → ANCM → App (no inter-process communication).
- Higher performance — no proxy overhead. Benchmarks show ~2x throughput vs out-of-process.
- Limitation: only one app per app pool (the CLR is loaded in w3wp.exe).
Out-of-Process Hosting:
- ANCM launches a separate dotnet.exe process running Kestrel.
- IIS acts as a reverse proxy — forwards requests to Kestrel on a random port.
- Requests: HTTP.sys → w3wp.exe → ANCM → Kestrel (dotnet.exe) → App.
- Supports multiple apps per app pool.
- Better isolation — app crash doesn't crash w3wp.exe.
ANCM handles: process management (start/stop/restart), stdout logging, startup error pages, request forwarding (out-of-process).
Configured in web.config with <aspNetCore> element and hostingModel attribute.
<!-- web.config: In-Process (default, recommended) -->
<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*"
modules="AspNetCoreModuleV2" />
</handlers>
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
hostingModel="InProcess"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT"
value="Production" />
<environmentVariable name="ConnectionStrings__Default"
value="Server=sql;Database=MyDb;" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</configuration>
<!-- Out-of-Process hosting -->
<!-- <aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
hostingModel="OutOfProcess"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout" /> -->
# ── Common ANCM issues and debugging ──
# 1. 502.5 — Process Failure (app crashes on startup)
# Check: stdout logs in .\logs\stdout_*.log
# Common cause: missing .NET runtime, bad connection string
# 2. 500.30 — In-Process Startup Failure
# Check: Event Viewer → Application → Source: IIS AspNetCore Module V2
# Common cause: mismatched runtime version
# 3. Enable detailed errors in Development
# <aspNetCore ...>
# <handlerSettings>
# <handlerSetting name="debugFile" value=".\logs\debug.log" />
# <handlerSetting name="debugLevel" value="FILE,TRACE" />
# </handlerSettings>
# </aspNetCore>
# ── Verify hosting model ──
# In your app, check:
# app.MapGet("/info", (HttpContext ctx) => new
# {
# Server = ctx.Request.Headers["Server"],
# ProcessId = Environment.ProcessId,
# ProcessName = Process.GetCurrentProcess().ProcessName
# // InProcess: "w3wp" | OutOfProcess: "dotnet"
# });
A team deployed an ASP.NET Core 8 app to IIS with out-of-process hosting. Under load, they noticed 50ms of added latency per request compared to running Kestrel directly. Switching to in-process hosting eliminated the proxy hop and reduced P99 latency by 40ms. They had to split two apps that shared an app pool into separate pools because in-process allows only one app per pool.
How does ANCM handle process crashes and restarts? What is the rapidFailProtection interaction?
applicationHost.config is the master configuration file for IIS — it defines sites, app pools, global modules, and server-level settings.
Location: %systemroot%\System32\inetsrv\config\applicationHost.config
Structure:
<configSections>— defines which sections exist and their lock state.<system.applicationHost>— sites, app pools, log configuration.<system.webServer>— global defaults for all sites (modules, handlers, security).<location path="SiteName">— site-specific overrides within applicationHost.config.
IIS Configuration Editor (IIS Manager feature):
- GUI tool to browse and edit any configuration section — even ones not exposed in other IIS Manager panels.
- Shows the effective configuration (merged from all levels).
- Can generate scripts (C#, JS, AppCmd, PowerShell) for any configuration change.
- Useful for advanced settings not exposed in the GUI.
Configuration backup:
- IIS keeps automatic backups in
%systemroot%\System32\inetsrv\config\backup\. - Manual backup:
appcmd add backup "MyBackup". - Configuration history tracks changes (enabled by default).
# ── Backup and restore configuration ──
# Manual backup
%windir%\System32\inetsrv\appcmd add backup "BeforeChanges"
# List backups
%windir%\System32\inetsrv\appcmd list backup
# Restore from backup
%windir%\System32\inetsrv\appcmd restore backup "BeforeChanges"
# ── PowerShell: Read applicationHost.config ──
Import-Module WebAdministration
# List all sites with bindings
Get-ChildItem "IIS:\Sites" | Select-Object Name, ID, State, Bindings
# List all app pools with settings
Get-ChildItem "IIS:\AppPools" | Select-Object Name, State,
@{N="Pipeline";E={$_.managedPipelineMode}},
@{N="Runtime";E={$_.managedRuntimeVersion}},
@{N="StartMode";E={$_.startMode}}
# Read a specific configuration section
Get-WebConfiguration -Filter "/system.webServer/security/requestFiltering" `
-PSPath "IIS:\Sites\MySite"
# ── Export site config for migration ──
# Export a site definition
%windir%\System32\inetsrv\appcmd list site "MySite" /config /xml > site.xml
# Export app pool definition
%windir%\System32\inetsrv\appcmd list apppool "MyPool" /config /xml > pool.xml
# ── Configuration Editor: Generate scripts ──
# IIS Manager → select site → Configuration Editor
# Navigate to system.webServer/httpCompression
# Make changes → click "Generate Script" in Actions pane
# Outputs equivalent PowerShell/AppCmd/C# code
# ── Configuration history ──
# Automatic history stored in:
# %systemroot%\System32\inetsrv\config\configHistory\
# Each folder contains a timestamped snapshot
# Compare with diff tool to find what changed
# ── Validate configuration ──
%windir%\System32\inetsrv\appcmd verify config
# Checks applicationHost.config AND all web.config files for errors
After a server migration, 15 sites needed identical configuration. Instead of configuring each through IIS Manager, the team exported applicationHost.config sections using appcmd and PowerShell, then scripted the import on the new server. They used the Configuration Editor to generate PowerShell scripts for complex settings (ARR, URL Rewrite rules) and version-controlled the scripts for repeatability.
How does IIS Shared Configuration work for web farms? What are the pitfalls?
Server Farms in ARR define groups of backend servers that receive traffic. Combined with health checking, they provide high availability and automatic failover.
Health Check Types:
- URL Test — ARR sends HTTP GET to a specified URL on each server at a configured interval. Checks for status code (200) and optionally matches response body content.
- Live Traffic Test — monitors actual request success/failure rates. If a server's error rate exceeds a threshold, it's marked unhealthy.
Health states:
- Healthy — server receives traffic normally.
- Unhealthy — server is removed from rotation. ARR continues health checking. When the server passes checks again, it's automatically re-added.
- Unavailable — manually marked offline by admin.
Graceful drain:
- Mark a server as "Drain" — no new connections, existing requests complete.
- Used for maintenance windows and deployments.
- When all in-flight requests finish, the server can be taken offline safely.
Failover: If all primary servers are unhealthy, ARR can fail over to a standby farm (disaster recovery).
# ── applicationHost.config: Server Farm with Health Check ──
# <webFarms>
# <webFarm name="ProductionFarm" enabled="true">
# <server address="web01.internal" enabled="true">
# <applicationRequestRouting weight="100" httpPort="80" />
# </server>
# <server address="web02.internal" enabled="true">
# <applicationRequestRouting weight="100" httpPort="80" />
# </server>
# <server address="web03.internal" enabled="true">
# <applicationRequestRouting weight="50" httpPort="80" />
# </server>
# <applicationRequestRouting>
# <healthCheck url="http://YOURSERVER/health"
# interval="00:00:15"
# timeout="00:00:05"
# responseMatch="OK"
# statusMatch="200" />
# <loadBalancing algorithm="LeastCurrentRequest" />
# </applicationRequestRouting>
# </webFarm>
# </webFarms>
# ── PowerShell: Manage server farm ──
Import-Module WebAdministration
# Check server health status
Get-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "webFarms/webFarm[@name='ProductionFarm']/server" `
-Name "." | Select-Object address, enabled
# Drain a server for maintenance
# IIS Manager → Server Farms → ProductionFarm → web01
# Right-click → Set to "Graceful Stop"
# Or via ARR Helper API:
# POST http://arr-server/arr-health/web01/drain
# ── ASP.NET Core health endpoint ──
# builder.Services.AddHealthChecks()
# .AddSqlServer(connectionString, name: "database")
# .AddRedis(redisConnection, name: "redis")
# .AddUrlGroup(new Uri("https://api.external.com/health"),
# name: "external-api");
#
# app.MapHealthChecks("/health", new HealthCheckOptions
# {
# ResponseWriter = async (ctx, report) =>
# {
# ctx.Response.ContentType = "application/json";
# var result = new
# {
# status = report.Status.ToString(),
# checks = report.Entries.Select(e => new
# {
# name = e.Key,
# status = e.Value.Status.ToString(),
# duration = e.Value.Duration
# })
# };
# await ctx.Response.WriteAsJsonAsync(result);
# }
# });
# ── Monitor ARR counters ──
# Performance counters:
# "Web Service\Current Connections" per server
# "ARR\Requests/Sec" — total throughput
# "ARR\Failed Requests" — backend failures
# "ARR\Bytes Sent/Sec" and "Bytes Received/Sec"
A three-server farm had intermittent 502 errors. One server's database connection was timing out, causing 500 responses. ARR's live traffic health check detected the elevated error rate and removed that server from rotation within 30 seconds. The health check URL (/health) verified SQL connectivity — the server was only re-added after the database recovered and the health endpoint returned 200.
How do you implement blue-green deployments using ARR server farms and URL Rewrite rules?
Web Deploy (MSDeploy) is Microsoft's tool for deploying web applications to IIS. It handles file sync, database scripts, configuration transforms, and IIS settings in a single operation.
Deployment methods:
- Web Deploy Package (.zip) — self-contained archive with content, parameters, and IIS settings. Created by Visual Studio or MSBuild.
- Web Deploy Publish — direct push from Visual Studio or CLI to a remote IIS server.
- File sync (
msdeploy -verb:sync) — synchronizes files between source and destination, only transferring changed files.
Providers (what to deploy):
contentPath— file system content.iisApp— IIS application (content + app pool + site settings).dbFullSql/dbDacFx— database scripts or DACPAC.appPoolConfig— app pool settings.recycleApp— recycle the app pool after deployment.
Web Deploy Handler vs Remote Agent:
- Handler (recommended) — runs over HTTPS on port 8172, uses IIS Manager credentials, supports non-admin deployments.
- Remote Agent — runs as a Windows service, requires admin credentials, uses HTTP.
# ── MSBuild: Create a Web Deploy package ──
dotnet publish -c Release -o ./publish
msdeploy -verb:sync `
-source:contentPath="./publish" `
-dest:package="MyApp.zip"
# ── Deploy package to remote IIS ──
msdeploy -verb:sync `
-source:package="MyApp.zip" `
-dest:auto,computerName="https://webserver:8172/msdeploy.axd",`
userName="DeployUser",password="****",authType="Basic" `
-allowUntrusted `
-setParam:name="IIS Web Application Name",value="Default Web Site/MyApp"
# ── CI/CD Pipeline (Azure DevOps YAML) ──
# - task: IISWebAppDeploymentOnMachineGroup@0
# inputs:
# WebSiteName: "MySite"
# Package: "$(Pipeline.Workspace)/drop/MyApp.zip"
# TakeAppOfflineFlag: true
# RemoveAdditionalFilesFlag: true
# XmlTransformation: true
# ── PowerShell: File sync (only changed files) ──
msdeploy -verb:sync `
-source:contentPath="C:\BuildOutput\MyApp" `
-dest:contentPath="D:\Sites\MyApp",computerName="webserver" `
-enableRule:DoNotDeleteRule `
-skip:objectName=filePath,absolutePath="\web\.config$" `
-skip:objectName=dirPath,absolutePath="\logs$"
# Key flags:
# -enableRule:DoNotDeleteRule — don't delete extra files on destination
# -skip — exclude specific files/folders from sync
# -enableRule:AppOffline — put app_offline.htm during deployment
# -whatIf — preview changes without applying
# ── web.config transforms ──
# Web.Release.config:
# <connectionStrings>
# <add name="Default"
# connectionString="Server=prod-sql;Database=MyDb;"
# xdt:Transform="SetAttributes"
# xdt:Locator="Match(name)" />
# </connectionStrings>
A team manually deployed via RDP and file copy — deployments took 30 minutes with frequent missed files. Switching to Web Deploy packages created by CI/CD reduced deployment to 2 minutes. The -enableRule:AppOffline flag showed a maintenance page during deployment, -skip excluded the logs folder, and web.config transforms handled environment-specific settings automatically.
How does Web Deploy compare to Octopus Deploy and Azure DevOps release pipelines for IIS deployment?
HTTP/2 (IIS 10 on Windows Server 2016+):
- Enabled by default for HTTPS connections in IIS 10+.
- Multiplexing — multiple requests/responses over a single TCP connection (no head-of-line blocking at HTTP level).
- Header compression (HPACK) — reduces header overhead for repeated requests.
- Server Push — server proactively sends resources the client will need (CSS, JS).
- Binary framing — more efficient parsing than HTTP/1.1 text format.
- Requirement: TLS 1.2+ with ALPN (Application-Layer Protocol Negotiation). HTTP/2 over plain HTTP is not supported in browsers.
HTTP/3 (IIS on Windows Server 2022+):
- Uses QUIC protocol (UDP-based) instead of TCP.
- Eliminates TCP head-of-line blocking — packet loss on one stream doesn't block others.
- Faster connection setup — 0-RTT or 1-RTT handshake (vs TCP+TLS 3-RTT).
- Better for mobile/lossy networks.
- Requires: Windows Server 2022, TLS 1.3, and UDP port 443 open on firewall.
Negotiation: Client and server negotiate the protocol via ALPN (HTTP/2) or Alt-Svc header (HTTP/3). Falls back gracefully to HTTP/1.1.
# ── Verify HTTP/2 is enabled (default on IIS 10) ──
# Check with browser DevTools → Network tab → Protocol column
# Should show "h2" for HTTPS connections
# ── Registry: Disable HTTP/2 (if needed for debugging) ──
# HKLM\SYSTEM\CurrentControlSet\Services\HTTP\Parameters
# DWORD: EnableHttp2Tls = 0 (disable) | 1 (enable, default)
# DWORD: EnableHttp2Cleartext = 0 (disable, default)
# ── Windows Server 2022: Enable HTTP/3 ──
# Requires TLS 1.3 enabled
# 1. Enable TLS 1.3 (if not already)
New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.3\Server" -Force
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.3\Server" `
-Name "Enabled" -Value 1 -PropertyType DWORD
# 2. Enable HTTP/3 on IIS
# <system.webServer>
# <protocols>
# <add name="h3" /> <!-- or via registry -->
# </protocols>
# </system.webServer>
# Registry: Enable HTTP/3
# HKLM\SYSTEM\CurrentControlSet\Services\HTTP\Parameters
# DWORD: EnableHttp3 = 1
# 3. Open UDP 443 in firewall
New-NetFirewallRule -DisplayName "HTTP/3 QUIC" `
-Direction Inbound -Protocol UDP `
-LocalPort 443 -Action Allow
# 4. IIS sends Alt-Svc header to advertise HTTP/3
# Response header: Alt-Svc: h3=":443"; ma=86400
# ── Verify protocol in use ──
# PowerShell: Test HTTP/2
# curl -v --http2 https://www.example.com 2>&1 | Select-String "HTTP/2"
# ── ASP.NET Core: Protocol settings ──
# builder.WebHost.ConfigureKestrel(options =>
# {
# options.ListenAnyIP(5001, listenOptions =>
# {
# listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
# listenOptions.UseHttps();
# });
# });
A media-heavy site had slow page loads because HTTP/1.1 limited concurrent downloads to 6 per domain. Browsers used domain sharding (cdn1, cdn2, cdn3) as a workaround. After verifying HTTP/2 was active (it was enabled by default but the load balancer was stripping ALPN), they removed domain sharding. HTTP/2 multiplexing loaded 40+ assets over a single connection — page load time dropped by 35%.
How does HTTP/2 Server Push work in IIS? Is it still recommended given browser preload hints?
IIS Shared Configuration allows multiple IIS servers to share a single applicationHost.config from a network share (UNC path). All servers in the farm read the same config.
How it works:
- Export the configuration from one server (including machine keys).
- Place it on a network share (e.g.,
\\fileserver\iisconfig). - Each IIS server points to the share via IIS Manager → Shared Configuration.
- Changes made on the share are picked up by all servers automatically.
Machine keys:
- IIS encrypts sensitive data (passwords, connection strings) using machine-specific keys.
- For shared config, all servers must use the same exported machine keys.
- Keys are exported with a password during the initial export step.
Pitfalls:
- Single point of failure — if the file share is down, IIS can't read config → all sites fail. Mitigation: use DFS Replication or a highly available share.
- Config changes affect all servers instantly — a bad change takes down the entire farm. No staged rollout.
- Machine-specific settings — IP bindings, certificates, and logging paths must be identical or use environment variables.
- Performance — config reads go over the network. Use a fast, low-latency share.
# ── Step 1: Export configuration from master server ──
# IIS Manager → Server node → Shared Configuration
# Or PowerShell:
Export-IISConfiguration -PhysicalPath "\\fileserver\iisconfig" `
-KeyEncryptionPassword (ConvertTo-SecureString "P@ssw0rd" -AsPlainText -Force)
# This exports:
# - applicationHost.config
# - administration.config
# - configEncryption.key (machine keys, encrypted with your password)
# ── Step 2: Enable Shared Configuration on each server ──
# IIS Manager → Shared Configuration → Enable
# Physical path: \fileserver\iisconfig
# Enter the key encryption password
# Or via PowerShell:
Enable-IISSharedConfig -PhysicalPath "\\fileserver\iisconfig" `
-KeyEncryptionPassword (ConvertTo-SecureString "P@ssw0rd" -AsPlainText -Force)
# ── Step 3: Verify ──
# Check that all servers show the same sites/app pools
Get-ChildItem "IIS:\Sites" | Select-Object Name, State
# ── High availability for the share ──
# Option 1: DFS Replication (DFS-R)
# - Config replicated to multiple file servers
# - Each IIS server points to its local DFS namespace
# - Survives file server failure
# Option 2: Scale-Out File Server (SOFS) cluster
# - Continuously available SMB share
# - Built-in failover
# ── Handling machine-specific differences ──
# Use environment variables in applicationHost.config:
# <site name="MySite">
# <application path="/">
# <virtualDirectory path="/"
# physicalPath="%SITE_ROOT%\MySite" />
# </application>
# </site>
# Each server sets SITE_ROOT to its local drive
# ── Rollback strategy ──
# Keep a local backup on each server:
# appcmd add backup "BeforeSharedConfig"
# If shared config fails, disable shared config →
# IIS falls back to local applicationHost.config
A 4-server web farm used Shared Configuration on a simple file share. During a storage maintenance window, the share went offline for 5 minutes. All 4 IIS servers stopped serving requests because they couldn't read applicationHost.config. The fix: migrated to DFS-R with replication to each server's local drive. Each server read from the local DFS replica — surviving share outages. They also implemented a change review process: test config changes on one server first, then export to shared config.
How do you handle SSL certificates across a shared configuration? What about the Central Certificate Store?
The Central Certificate Store (CCS) is an IIS feature that stores SSL certificates on a shared file location (UNC path) instead of each server's local certificate store.
Why CCS?
- Without CCS: you must install certificates on every server individually, keep them in sync, and remember to renew on each server. With 20 servers and 50 certificates — that's 1000 operations.
- With CCS: certificates are PFX files on a share. All servers read from the same location. Add/renew a certificate once — all servers pick it up.
How it works:
- Certificates are stored as .pfx files named by the hostname:
www.example.com.pfx,*.example.com.pfx(wildcard). - All PFX files use the same private key password (configured in CCS settings).
- IIS bindings use SNI (Server Name Indication) to match the hostname to the correct certificate file.
- CCS supports wildcard and SAN certificates.
Requirements:
- IIS 8.0+ (Windows Server 2012+).
- All certificates must be PFX with the same password.
- File naming must match the hostname exactly.
- The share must be accessible to all IIS servers.
# ── Setup Central Certificate Store ──
# 1. Install the CCS feature
Install-WindowsFeature Web-CertProvider
# 2. Create a share for certificates
New-Item -Path "D:\CertStore" -ItemType Directory
New-SmbShare -Name "CertStore" -Path "D:\CertStore" `
-ReadAccess "IIS_Farm_Servers"
# 3. Place PFX files (named by hostname)
# D:\CertStore\www.example.com.pfx
# D:\CertStore\api.example.com.pfx
# D:\CertStore\*.example.com.pfx (wildcard cert)
# 4. Enable CCS on each IIS server
Enable-IISCentralCertProvider `
-CertStoreLocation "\\fileserver\CertStore" `
-UserName "DOMAIN\CertReader" `
-Password "****" `
-PrivateKeyPassword "PfxP@ssw0rd"
# ── Or via IIS Manager ──
# Server node → Centralized Certificates
# Physical path: \fileserver\CertStore
# User name: DOMAIN\CertReader (read access to share)
# Certificate password: PfxP@ssw0rd (password for all PFX files)
# ── Create a binding using CCS ──
# IIS Manager → Site → Bindings → Add
# Type: https
# Host name: www.example.com
# ✅ Require Server Name Indication (SNI)
# ✅ Use Centralized Certificate Store
# (no certificate dropdown — IIS looks up www.example.com.pfx automatically)
# PowerShell:
New-WebBinding -Name "MySite" -Protocol https `
-Port 443 -HostHeader "www.example.com" `
-SslFlags 3 # 1=SNI, 2=CCS, 3=SNI+CCS
# ── Automate certificate renewal ──
# Script to export renewed cert from Let's Encrypt / ACME:
# certbot certonly --standalone -d www.example.com
# openssl pkcs12 -export -out "\\fileserver\CertStore\www.example.com.pfx" `
# -inkey privkey.pem -in fullchain.pem -passout pass:PfxP@ssw0rd
# IIS picks up the new PFX automatically (no restart needed)
A hosting provider managed 200+ websites on a 5-server farm. Certificate renewals were a nightmare — an engineer spent 2 days each month installing updated certs on every server. After implementing CCS with an ACME client that exported PFX files to the share, renewals became fully automated. New sites just needed a PFX file dropped into the share — IIS picked it up via SNI without any binding changes.
How do you handle SAN (Subject Alternative Name) certificates with CCS? What about wildcard certificates?
A production IIS server should be hardened beyond default settings. Key areas:
1. Remove unnecessary features:
- Uninstall unused role services (WebDAV, CGI, ISAPI, FTP if not needed).
- Remove default site, default app pool, IIS default content.
- Remove unused modules (WebDAVModule, CGIModule, ServerSideIncludeModule).
2. HTTP Security Headers:
X-Content-Type-Options: nosniff— prevents MIME sniffing.X-Frame-Options: DENYorSAMEORIGIN— prevents clickjacking.Strict-Transport-Security(HSTS) — forces HTTPS.Content-Security-Policy— controls resource loading.- Remove:
X-Powered-By,Serverheaders (information disclosure).
3. TLS configuration:
- Disable TLS 1.0, TLS 1.1, SSL 2.0, SSL 3.0 — only allow TLS 1.2+.
- Disable weak cipher suites (RC4, DES, 3DES, NULL ciphers).
- Use strong key exchange (ECDHE) and bulk encryption (AES-GCM).
4. Request restrictions:
- Enable Request Filtering — block dangerous extensions, sequences, verbs.
- Set content length limits.
- Disable directory browsing.
- Configure Dynamic IP restrictions.
5. Least privilege:
- Run app pools under custom service accounts (not NetworkService).
- Grant minimum NTFS permissions to web content.
- Disable anonymous auth where not needed.
<!-- web.config: Security hardening -->
<system.webServer>
<!-- Security headers -->
<httpProtocol>
<customHeaders>
<!-- Remove information disclosure headers -->
<remove name="X-Powered-By" />
<!-- Anti-clickjacking -->
<add name="X-Frame-Options" value="SAMEORIGIN" />
<!-- Prevent MIME sniffing -->
<add name="X-Content-Type-Options" value="nosniff" />
<!-- XSS protection -->
<add name="X-XSS-Protection" value="1; mode=block" />
<!-- Force HTTPS for 1 year -->
<add name="Strict-Transport-Security"
value="max-age=31536000; includeSubDomains; preload" />
<!-- Content Security Policy -->
<add name="Content-Security-Policy"
value="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" />
<!-- Referrer Policy -->
<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
<!-- Permissions Policy -->
<add name="Permissions-Policy"
value="camera=(), microphone=(), geolocation=()" />
</customHeaders>
</httpProtocol>
<!-- Remove Server header (requires URL Rewrite) -->
<rewrite>
<outboundRules>
<rule name="Remove Server Header">
<match serverVariable="RESPONSE_Server" pattern=".*" />
<action type="Rewrite" value="" />
</rule>
</outboundRules>
</rewrite>
<!-- Disable directory browsing -->
<directoryBrowse enabled="false" />
</system.webServer>
# ── PowerShell: Disable old TLS versions ──
# Disable TLS 1.0
New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" -Force
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" `
-Name "Enabled" -Value 0 -PropertyType DWORD
# Disable TLS 1.1
New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server" -Force
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server" `
-Name "Enabled" -Value 0 -PropertyType DWORD
# ── Use IISCrypto tool for TLS/cipher suite configuration ──
# Or use the IIS Crypto GUI: https://www.nartac.com/Products/IISCrypto
# Templates: "Best Practices" or "PCI 4.0" for compliance
# ── Remove unused modules ──
# <modules>
# <remove name="WebDAVModule" />
# <remove name="CGIModule" />
# </modules>
A security audit flagged an IIS server for: X-Powered-By header leaking ASP.NET version, TLS 1.0 enabled (PCI DSS violation), directory browsing on the uploads folder, and missing HSTS header. The team implemented the hardening checklist: removed disclosure headers, disabled old TLS versions with IIS Crypto, turned off directory browsing, added HSTS with a 1-year max-age, and set up Content-Security-Policy. The server passed the follow-up audit.
How do you achieve an A+ rating on SSL Labs for an IIS server? What specific cipher suite order is recommended?
PowerShell is the primary automation tool for IIS. Two modules are available:
WebAdministration (legacy, Windows PowerShell 5.1):
- Provides a PSDrive (
IIS:) to navigate sites, app pools, and config as a file system. - Cmdlets:
Get-Website,New-WebSite,New-WebAppPool,Set-WebConfigurationProperty. - Uses WMI/COM under the hood — slower but well-documented.
- Available on all Windows Server versions with IIS.
IISAdministration (modern, PowerShell 5.1+ and 7+):
- Newer, faster, and works with PowerShell 7.
- Cmdlets:
Get-IISSite,New-IISSite,Get-IISAppPool,Start-IISCommitDelay. - Supports commit delay — batch multiple changes into one atomic commit.
- More robust error handling and pipeline support.
Common automation scenarios:
- Provisioning sites and app pools from CI/CD pipelines.
- Bulk certificate renewal and binding updates.
- Configuration drift detection — compare current vs desired state.
- Health monitoring scripts — check app pool state, recycle if needed.
- DSC (Desired State Configuration) for declarative IIS config.
# ── IISAdministration: Create a complete site ──
Import-Module IISAdministration
# Batch changes with commit delay (atomic)
Start-IISCommitDelay
# Create app pool
$pool = New-IISAppPool -Name "MyApp-Pool"
$pool.ManagedPipelineMode = "Integrated"
$pool.ManagedRuntimeVersion = "" # No managed runtime (ASP.NET Core)
$pool.StartMode = "AlwaysRunning"
$pool.ProcessModel.IdleTimeout = [TimeSpan]::Zero
# Create site
New-IISSite -Name "MyApp" `
-PhysicalPath "D:\Sites\MyApp" `
-BindingInformation "*:443:www.myapp.com" `
-Protocol https `
-CertificateThumbprint "ABC123..." `
-CertStoreLocation "Cert:\LocalMachine\My"
# Assign app pool
Set-IISConfigAttributeValue -ConfigElement `
(Get-IISSite "MyApp").Applications["/"] `
-AttributeName "applicationPool" -AttributeValue "MyApp-Pool"
# Commit all changes at once
Stop-IISCommitDelay
# ── WebAdministration: Common operations ──
Import-Module WebAdministration
# List all stopped app pools
Get-ChildItem "IIS:\AppPools" | Where-Object { $_.State -eq "Stopped" }
# Recycle all app pools
Get-ChildItem "IIS:\AppPools" | ForEach-Object { $_.Recycle() }
# Bulk update: set idle timeout for all pools
Get-ChildItem "IIS:\AppPools" | ForEach-Object {
Set-ItemProperty "IIS:\AppPools\$($_.Name)" `
-Name "processModel.idleTimeout" -Value ([TimeSpan]::FromMinutes(0))
}
# ── Health monitoring script ──
# $pools = Get-IISAppPool
# foreach ($pool in $pools) {
# if ($pool.State -ne "Started") {
# Write-Warning "$($pool.Name) is $($pool.State)!"
# Start-IISAppPool -Name $pool.Name
# Send-MailMessage -To "ops@company.com" `
# -Subject "App Pool Restarted: $($pool.Name)" ...
# }
# }
# ── DSC: Desired State Configuration ──
# Configuration IISConfig {
# Import-DscResource -ModuleName xWebAdministration
# Node "WebServer" {
# xWebAppPool MyAppPool {
# Name = "MyApp-Pool"
# State = "Started"
# ManagedPipelineMode = "Integrated"
# }
# xWebsite MySite {
# Name = "MyApp"
# PhysicalPath = "D:\Sites\MyApp"
# ApplicationPool = "MyApp-Pool"
# State = "Started"
# }
# }
# }
A hosting company provisioned new customer sites manually — 45 minutes per site. They built a PowerShell script using IISAdministration with Start-IISCommitDelay that created the app pool, site, bindings, SSL certificate, folder structure, and NTFS permissions in one atomic operation. Provisioning dropped to 30 seconds. The script was integrated into their customer onboarding portal via a REST API.
How does PowerShell DSC compare to Ansible/Chef/Puppet for IIS configuration management?
IIS exposes hundreds of performance counters through Windows Performance Monitor (PerfMon). Key counter categories:
Web Service (per-site):
Current Connections— active connections. Sudden spikes indicate traffic surges or slow responses holding connections.Bytes Sent/Sec+Bytes Received/Sec— bandwidth usage. High values may indicate large responses or missing compression.Get Requests/Sec,Post Requests/Sec— request rate by method.Not Found Errors/Sec(404s) — broken links, scanning attacks, or misconfigured URLs.
W3SVC_W3WP (per-worker process):
Active Requests— requests currently being processed. High values mean the app is slow or thread-starved.Requests/Sec— throughput of the worker process.Request Wait Time— time requests spend in the queue before processing begins.
ASP.NET:
Requests Queued— requests waiting for a thread. Should be near 0. High values = thread starvation.Request Execution Time— how long requests take to complete.Application Restarts— unexpected restarts indicate crashes or config changes.
.NET CLR Memory:
% Time in GC— above 10% indicates memory pressure.Gen 2 Collections— frequent Gen 2 GC means large object churn or memory leaks.Large Object Heap Size— growing LOH suggests large allocations (85KB+).
# ── PowerShell: Read key performance counters ──
# Real-time monitoring of critical counters
$counters = @(
"\Web Service(_Total)\Current Connections",
"\Web Service(_Total)\Get Requests/sec",
"\Web Service(_Total)\Not Found Errors/sec",
"\ASP.NET\Requests Queued",
"\ASP.NET\Request Execution Time",
"\ASP.NET Applications(__Total__)\Requests/Sec",
"\.NET CLR Memory(w3wp)\% Time in GC",
"\.NET CLR Memory(w3wp)\Gen 2 Collections",
"\Process(w3wp)\% Processor Time",
"\Process(w3wp)\Private Bytes",
"\Process(w3wp)\Thread Count"
)
# Sample every 5 seconds, 60 samples
Get-Counter -Counter $counters -SampleInterval 5 -MaxSamples 60 |
Export-Counter -Path "C:\PerfLogs\iis_perf.blg" -Force
# ── Create a Data Collector Set (long-term monitoring) ──
# logman create counter "IIS Monitoring" `
# -c "\Web Service(_Total)\Current Connections" `
# "\ASP.NET\Requests Queued" `
# "\Process(w3wp)\% Processor Time" `
# "\.NET CLR Memory(w3wp)\% Time in GC" `
# -si 10 -f bincirc -max 500 `
# -o "C:\PerfLogs\IIS_Monitor"
# logman start "IIS Monitoring"
# ── Alert when thresholds are exceeded ──
# $sample = Get-Counter "\ASP.NET\Requests Queued"
# $queued = $sample.CounterSamples[0].CookedValue
# if ($queued -gt 100) {
# Send-MailMessage -To "ops@company.com" `
# -Subject "IIS Alert: $queued requests queued!" `
# -SmtpServer "mail.company.com" ...
# # Consider recycling the app pool
# # (Get-IISAppPool "MyApp-Pool").Recycle()
# }
# ── Key thresholds to alert on ──
# Requests Queued > 10 → thread starvation
# % Time in GC > 15% → memory pressure
# Active Requests > 100 → app is slow
# Current Connections > 5000 → possible DDoS
# Request Wait Time > 1000ms → bottleneck
# 404 Errors/Sec > 50 → scanning attack or broken links
# Private Bytes > 2GB → possible memory leak
A production app had intermittent slowness every afternoon. PerfMon data showed Requests Queued spiking to 200+ and % Time in GC jumping to 40% at 2 PM daily. Investigation revealed a scheduled report that loaded millions of rows into memory, triggering Gen 2 GC storms that froze all threads. The fix: moved the report to a separate app pool with its own memory space, and rewrote the query to use streaming (IAsyncEnumerable) instead of loading everything into memory.
How do you integrate IIS performance counters with Application Insights or Prometheus/Grafana?
DebugDiag (Debug Diagnostic Tool) is Microsoft's tool for capturing and analyzing memory dumps of IIS worker processes.
When to use:
- Worker process crashes (Event ID 5011 — app pool disabled due to rapid-fail).
- Memory leaks — w3wp.exe private bytes growing continuously.
- High CPU — one thread consuming 100% CPU.
- Hangs — requests stuck, no response, but process is alive.
Two components:
- DebugDiag Collection — captures dumps based on rules (crash, memory leak, performance).
- DebugDiag Analysis — analyzes dumps and generates HTML reports with stack traces, memory analysis, and recommendations.
Dump types:
- Mini dump — small, contains thread stacks and basic info. Good for crashes.
- Full dump — complete process memory. Required for memory leak analysis. Can be very large (2-8 GB).
Capture triggers:
- First-chance exception (specific type like StackOverflowException).
- Second-chance exception (unhandled — process will crash).
- Memory threshold (e.g., when Private Bytes > 2 GB).
- Manual capture (proactive dump during slow period).
# ── DebugDiag: Create a crash rule ──
# 1. Open DebugDiag Collection
# 2. Add Rule → Crash
# 3. Select "A specific IIS web application pool" → choose your pool
# 4. Configure:
# - Crash type: unhandled exceptions
# - Action: Full user dump
# - Advanced: filter by exception type (optional)
# 5. Set dump location: D:\Dumps\
# 6. Activate rule
# ── DebugDiag: Memory leak rule ──
# 1. Add Rule → Memory and Handle Leak
# 2. Select the w3wp process or app pool
# 3. Configure:
# - Trigger: when Private Bytes > 1.5 GB
# - Auto-create leak rule → tracks allocations
# - Generate dump when threshold hit
# 4. Activate — DebugDiag injects leak tracking
# ── Manual dump with procdump (alternative) ──
# Capture dump when CPU > 90% for 10 seconds:
# procdump -ma -c 90 -s 10 w3wp.exe D:\Dumps\
# Capture dump when private bytes > 2 GB:
# procdump -ma -m 2048 w3wp.exe D:\Dumps\
# Capture dump on specific exception:
# procdump -ma -e 1 -f "System.OutOfMemoryException" w3wp.exe D:\Dumps\
# ── Analyze the dump ──
# 1. Open DebugDiag Analysis
# 2. Add dump file(s)
# 3. Select analysis rule:
# - CrashHangAnalysis — for crashes and hangs
# - MemoryAnalysis — for memory leaks
# - PerfAnalysis — for performance issues
# 4. Start Analysis → generates HTML report
# ── Key things to look for in the report ──
# Crash Analysis:
# - Exception type and message
# - Faulting thread stack trace
# - Module that caused the crash
#
# Memory Analysis:
# - Top memory consumers (by type)
# - Leaked objects and their allocation stacks
# - String duplicates (common leak pattern)
#
# Hang Analysis:
# - Threads blocked on locks (deadlock detection)
# - Threads waiting for external calls (DB, HTTP)
# - Thread pool exhaustion
An app pool crashed every 6-8 hours with no clear pattern. DebugDiag crash rule captured a full dump on the unhandled exception. Analysis revealed a StackOverflowException in a recursive method that processed nested categories — categories with circular parent references caused infinite recursion. The fix: added a depth limit and cycle detection to the recursive method. DebugDiag's report pinpointed the exact method and call chain.
How do you use WinDbg with SOS extension for advanced .NET dump analysis? When is it preferred over DebugDiag?
A Web Garden is an app pool configured with multiple worker processes (maxProcesses > 1). Each worker process (w3wp.exe) handles a portion of requests independently.
How it works:
- HTTP.sys distributes requests among multiple w3wp.exe processes for the same app pool.
- Each process has its own memory space, thread pool, and CLR instance.
- Requests are distributed at the kernel level — very efficient.
When Web Gardens help:
- Application uses single-threaded COM objects that block the entire thread pool.
- Code has a global lock that serializes requests — multiple processes bypass the lock.
- Need to utilize multiple NUMA nodes on high-end servers.
- Application is 32-bit (limited to 4GB) — multiple processes give more total memory.
When Web Gardens hurt (most scenarios!):
- In-process session state is lost — each process has its own memory. Session affinity is not guaranteed.
- In-memory cache is duplicated — N processes = N copies of cached data = wasted memory.
- File locks conflict — multiple processes writing to the same log file.
- Application Initialization runs N times — slower warmup.
Default: maxProcesses = 1 (no Web Garden). This is correct for almost all modern ASP.NET applications.
# ── Configure Web Garden ──
Import-Module WebAdministration
# Set 4 worker processes (Web Garden)
Set-ItemProperty "IIS:\AppPools\MyApp-Pool" `
-Name "processModel.maxProcesses" -Value 4
# Verify
Get-ItemProperty "IIS:\AppPools\MyApp-Pool" `
-Name "processModel.maxProcesses"
# ── Check running worker processes ──
Get-ChildItem "IIS:\AppPools\MyApp-Pool\WorkerProcesses"
# Should show 4 w3wp.exe processes under load
# Or:
Get-Process w3wp | Select-Object Id, StartTime,
@{N="Memory(MB)";E={[math]::Round($_.WorkingSet64/1MB)}},
@{N="CPU(s)";E={[math]::Round($_.CPU,1)}}
# ── When using Web Garden, you MUST use out-of-process state ──
# Session state: SQL Server or Redis
# <sessionState mode="SQLServer"
# sqlConnectionString="Server=sql;Database=ASPState;" />
# Or Redis:
# builder.Services.AddStackExchangeRedisCache(options =>
# {
# options.Configuration = "redis:6379";
# });
# builder.Services.AddSession();
# Cache: Distributed cache (Redis) instead of in-memory
# builder.Services.AddStackExchangeRedisCache(options =>
# {
# options.Configuration = "redis:6379";
# });
# Data protection keys: shared store
# builder.Services.AddDataProtection()
# .PersistKeysToStackExchangeRedis(redis, "DataProtection-Keys");
# ── Better alternative: Multiple app pools ──
# Instead of 4 workers in one pool, use 4 separate pools:
# MyApp-Pool-1, MyApp-Pool-2, MyApp-Pool-3, MyApp-Pool-4
# Each behind ARR load balancing
# Benefit: independent recycling, better isolation
A legacy ASP.NET app used a third-party PDF library that was single-threaded and held a process-wide lock. With one worker process, only one PDF could generate at a time — requests queued up. Setting maxProcesses=4 (Web Garden) allowed 4 concurrent PDF generations because each w3wp.exe had its own lock. Session state was moved to SQL Server to handle the multi-process setup. The better long-term fix was replacing the library with a thread-safe alternative.
How does CPU affinity (NUMA-aware) work with Web Gardens? When does processor affinity help?
Modern IIS deployments should be automated through CI/CD pipelines with zero-downtime strategies.
Deployment strategies:
- App_offline.htm — IIS serves a static maintenance page while files are updated. Simple but causes brief downtime (10-30 seconds).
- Blue-Green with ARR — two identical environments. Deploy to inactive environment, switch traffic via URL Rewrite rule. Instant rollback by switching back.
- Rolling deployment — update one server at a time in a load-balanced farm. ARR health checks route traffic away from the server being updated.
- Canary deployment — route a small percentage of traffic (e.g., 5%) to the new version using ARR weighted routing. Monitor errors, then increase to 100%.
- Shadow deployment — deploy to a new app pool/site, run smoke tests, then switch IIS bindings.
Pipeline steps:
- Build —
dotnet publish, run tests, create Web Deploy package. - Artifact — store package in artifact repository.
- Deploy — push to target server (Web Deploy, PowerShell remoting, or agent).
- Smoke test — hit health endpoint, verify key pages.
- Switch traffic — update ARR routing or DNS.
- Monitor — watch error rates, response times for 15 minutes.
- Rollback — automatic if error rate exceeds threshold.
# ── Azure DevOps YAML Pipeline ──
# trigger:
# branches: { include: [main] }
#
# stages:
# - stage: Build
# jobs:
# - job: BuildApp
# steps:
# - task: DotNetCoreCLI@2
# inputs:
# command: publish
# publishWebProjects: true
# arguments: "-c Release -o $(Build.ArtifactStagingDirectory)"
# - publish: $(Build.ArtifactStagingDirectory)
# artifact: webapp
#
# - stage: Deploy
# jobs:
# - deployment: DeployToIIS
# environment: Production
# strategy:
# runOnce:
# deploy:
# steps:
# - task: IISWebAppDeploymentOnMachineGroup@0
# inputs:
# WebSiteName: "MySite"
# Package: "$(Pipeline.Workspace)/webapp/**/*.zip"
# TakeAppOfflineFlag: true
# RemoveAdditionalFilesFlag: true
# ── PowerShell: Blue-Green deployment script ──
param([string]$NewVersion)
$activeFarm = Get-Content "D:\Deploy\active-farm.txt" # "Blue" or "Green"
$inactiveFarm = if ($activeFarm -eq "Blue") { "Green" } else { "Blue" }
# 1. Deploy to inactive farm
$servers = Get-Content "D:\Deploy\$inactiveFarm-servers.txt"
foreach ($server in $servers) {
msdeploy -verb:sync `
-source:package="D:\Builds\$NewVersion\MyApp.zip" `
-dest:auto,computerName="https://${server}:8172/msdeploy.axd",`
userName="deploy",password="****",authType="Basic" `
-enableRule:AppOffline
}
# 2. Health check on inactive farm
foreach ($server in $servers) {
$health = Invoke-WebRequest "http://${server}:8080/health" -TimeoutSec 30
if ($health.StatusCode -ne 200) {
Write-Error "Health check failed on $server!"
exit 1
}
}
# 3. Switch traffic (update ARR URL Rewrite rule)
$configPath = "MACHINE/WEBROOT/APPHOST"
Set-WebConfigurationProperty -PSPath $configPath `
-Filter "system.webServer/rewrite/rules/rule[@name='Route to Farm']/action" `
-Name "url" -Value "http://${inactiveFarm}Farm/{R:0}"
# 4. Update active farm marker
$inactiveFarm | Set-Content "D:\Deploy\active-farm.txt"
Write-Host "Switched traffic from $activeFarm to $inactiveFarm"
Write-Host "Rollback: run script with -NewVersion to deploy previous version"
A fintech app required zero-downtime deployments with instant rollback. They implemented blue-green with ARR: two server farms ("Blue" and "Green") behind an ARR front-end. The CI/CD pipeline deployed to the inactive farm, ran integration tests against it (using a test hostname), then switched the ARR rule. Rollback was one command — switch the rule back. Deployment went from 15 minutes of downtime to zero, with rollback in under 5 seconds.
How do you handle database schema changes in a blue-green deployment where both versions must work simultaneously?
Kernel-mode caching in HTTP.sys is the fastest response path in the entire IIS stack. Cached responses are served directly from kernel memory — the request never reaches the IIS worker process (w3wp.exe).
How it works:
- A request arrives at HTTP.sys (kernel driver).
- HTTP.sys checks its URI cache — a hash table of cached responses keyed by URI.
- If the URI is cached and valid → response is sent directly from kernel memory. No context switch to user mode. No w3wp.exe involved.
- If not cached → request is queued to the IIS worker process for normal processing. The response may be added to the kernel cache based on caching rules.
What gets kernel-cached:
- Static files (HTML, CSS, JS, images) with
kernelCachePolicyconfigured. - Dynamic responses where IIS Output Caching has
kernelCachePolicyset. - Responses with proper
Cache-Controlheaders.
What CANNOT be kernel-cached:
- Authenticated requests (responses vary by user).
- Requests with query strings (by default — can be enabled).
- Responses > 256 KB (default, configurable).
- Responses with
Set-Cookieheaders. - HTTPS on older Windows versions (Server 2012 R2 and earlier).
Performance impact: Kernel-cached responses can serve 50,000-100,000+ requests/second on a single server — 10x-100x more than user-mode processing.
# ── Check kernel cache status ──
# List all currently cached URIs in HTTP.sys
netsh http show cachestate
# Output example:
# URL: http://www.example.com:80/index.html
# Status code: 200
# HTTP verb: GET
# Cache policy type: Timed
# Cache TTL: 120 seconds
# Cache hit count: 4523
# Cache creation time: 2026-05-30 10:15:00
# ── web.config: Enable kernel caching for dynamic content ──
# <system.webServer>
# <caching enabled="true" enableKernelCache="true">
# <profiles>
# <add extension=".aspx"
# policy="CacheForTimePeriod"
# duration="00:02:00"
# kernelCachePolicy="CacheForTimePeriod"
# varyByQueryString="category,page" />
# </profiles>
# </caching>
# </system.webServer>
# ── Registry: Tune kernel cache limits ──
# HKLM\System\CurrentControlSet\Services\HTTP\Parameters
# MaxCacheResponseSize (DWORD) — max response size to cache (bytes)
# Default: 262144 (256 KB). Increase for larger static files.
# UriMaxUriBytes (DWORD) — max URL length for cache key
# Default: 4096. Increase if you have long URLs.
# ── Performance counters for kernel cache ──
$counters = @(
"\Web Service Cache\Kernel: Current URIs Cached",
"\Web Service Cache\Kernel: URI Cache Hits",
"\Web Service Cache\Kernel: URI Cache Misses",
"\Web Service Cache\Kernel: URI Cache Hits %",
"\Web Service Cache\Kernel: URI Cache Flushes"
)
Get-Counter -Counter $counters
# Target: Kernel URI Cache Hits % > 80%
# If low: check why responses aren't being cached
# Common blockers: authentication, Set-Cookie, large responses
# ── Force flush kernel cache ──
netsh http flush logbuffer
# Or restart HTTP service (affects all sites!):
# net stop http; net start http
# ── ASP.NET Core: Enable kernel caching ──
# Kernel caching works when:
# 1. Response has Cache-Control: public, max-age=N
# 2. No Set-Cookie header
# 3. No Authorization header
# 4. Response is < MaxCacheResponseSize
# app.UseResponseCaching();
# [ResponseCache(Duration = 120, Location = ResponseCacheLocation.Any)]
A news website served the same homepage to millions of visitors. With kernel caching disabled, each request went through the full ASP.NET pipeline — 15ms per request, maxing out at 3,000 req/sec. After enabling kernel caching with a 60-second TTL, the homepage was served from HTTP.sys in under 0.1ms. Throughput jumped to 80,000 req/sec on the same hardware. The cache hit ratio was 98%.
How does HTTP.sys kernel cache interact with CDN edge caching? Can they have conflicting TTLs?
IIS has multiple layers of connection and thread limits that can bottleneck high-traffic applications:
HTTP.sys Queue:
appConcurrentRequestLimit— max concurrent requests per app pool (default: 5000).- When exceeded, HTTP.sys returns 503 Service Unavailable.
- Each app pool has its own request queue in HTTP.sys.
.NET Thread Pool:
minWorkerThreads— minimum threads kept alive (default: number of CPUs).maxWorkerThreads— maximum threads per CPU (default: 32767 on 64-bit).- The thread pool grows slowly — one new thread per 500ms when all existing threads are busy.
- Thread injection rate is the bottleneck — not the max limit. Increase
minWorkerThreadsto pre-create threads.
Connection limits:
maxConnections— max simultaneous connections per site (default: 4294967295 — effectively unlimited).connectionTimeout— idle connection timeout (default: 120 seconds). Lower to free connections faster.
ASP.NET Core Kestrel limits (when using out-of-process):
MaxConcurrentConnections— max simultaneous connections.MaxRequestBodySize— max request body (default: 30 MB).RequestHeadersTimeout— time limit for receiving headers.
# ── Increase min thread pool threads (critical for burst traffic) ──
# machine.config or web.config:
# <configuration>
# <system.web>
# <processModel minWorkerThreads="200"
# minIoThreads="200" />
# </system.web>
# </configuration>
# ASP.NET Core (in Program.cs):
# ThreadPool.SetMinThreads(200, 200);
# ── Application pool queue length ──
Import-Module WebAdministration
# Increase queue length (default 1000)
Set-ItemProperty "IIS:\AppPools\MyApp-Pool" `
-Name "queueLength" -Value 5000
# ── HTTP.sys concurrent request limit ──
# In applicationHost.config:
# <system.webServer>
# <serverRuntime appConcurrentRequestLimit="10000" />
# </system.webServer>
# ── Connection timeout (free idle connections faster) ──
# <system.webServer>
# <limits connectionTimeout="00:01:00"
# maxConnections="10000"
# maxBandwidth="0" />
# </system.webServer>
# ── ASP.NET Core Kestrel tuning ──
# builder.WebHost.ConfigureKestrel(options =>
# {
# options.Limits.MaxConcurrentConnections = 10000;
# options.Limits.MaxConcurrentUpgradedConnections = 1000;
# options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; // 50 MB
# options.Limits.MinRequestBodyDataRate = null; // disable for slow clients
# options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
# });
# ── Monitor thread pool health ──
$counters = @(
"\.NET CLR LocksAndThreads(w3wp)\Current Queue Length",
"\.NET CLR LocksAndThreads(w3wp)\# of current logical Threads",
"\ASP.NET\Requests Queued",
"\Web Service(_Total)\Current Connections"
)
Get-Counter -Counter $counters -SampleInterval 5
# ── Key tuning formula ──
# minWorkerThreads = expected concurrent requests during burst
# Example: if your app handles 500 concurrent requests with avg 200ms response:
# Steady state threads = 500 * 0.2 = 100 threads active
# Set minWorkerThreads = 100-200 (2x steady state)
# This prevents the slow thread injection ramp-up during traffic spikes
An API experienced 503 errors during morning traffic spikes (9 AM). PerfMon showed Requests Queued jumping to 500+. The .NET thread pool had only 8 threads (= CPU count) and was slowly injecting new threads at 1 per 500ms. By the time enough threads were created, requests had timed out. Fix: set minWorkerThreads=200 to pre-create threads. The 503 errors disappeared because the thread pool was ready for the burst.
How does async/await reduce thread consumption in ASP.NET Core? What happens when you block a thread pool thread?
Large file handling in IIS involves several configuration layers and features:
Upload limits:
maxAllowedContentLength(Request Filtering) — maximum request body size in bytes. Default: 30 MB (30000000). IIS returns 404.13 if exceeded.maxRequestLength(ASP.NET) — maximum request size in KB. Default: 4 MB (4096 KB). ASP.NET returns 500 if exceeded.- Both limits must be increased for large file uploads.
Range Requests (partial downloads):
- Client sends
Range: bytes=0-1023header to download a specific byte range. - Server responds with 206 Partial Content and the requested bytes.
- Enables: download resumption (restart interrupted downloads), parallel downloads (multiple chunks simultaneously), video seeking (jump to a timestamp without downloading the whole file).
- IIS supports range requests automatically for static files.
Streaming:
- For uploads: use
Request.BodyReader(ASP.NET Core) to stream directly to disk without buffering in memory. - For downloads: use
FileStreamResultorFile(stream, ...)to stream from disk without loading the entire file into memory. - IIS has a buffering setting — disable for large responses to reduce memory usage.
<!-- web.config: Large file upload configuration -->
<system.webServer>
<!-- IIS Request Filtering limit (bytes) — 2 GB -->
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="2147483648" />
</requestFiltering>
</security>
</system.webServer>
<system.web>
<!-- ASP.NET limit (KB) — 2 GB -->
<!-- executionTimeout: 1 hour for large uploads -->
<httpRuntime maxRequestLength="2097152"
executionTimeout="3600" />
</system.web>
# ── ASP.NET Core: Streaming large file upload ──
# // Disable request body buffering for large files
# [DisableRequestSizeLimit]
# [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)]
# app.MapPost("/upload", async (HttpRequest request) =>
# {
# // Stream directly to disk — no memory buffering
# var boundary = request.GetMultipartBoundary();
# var reader = new MultipartReader(boundary, request.Body);
# var section = await reader.ReadNextSectionAsync();
#
# while (section != null)
# {
# if (ContentDispositionHeaderValue.TryParse(
# section.ContentDisposition, out var cd) && cd.IsFileDisposition())
# {
# var filePath = Path.Combine("D:\Uploads", cd.FileName.Value);
# await using var fileStream = File.Create(filePath);
# await section.Body.CopyToAsync(fileStream);
# }
# section = await reader.ReadNextSectionAsync();
# }
# return Results.Ok("Uploaded");
# });
# ── ASP.NET Core: Streaming large file download ──
# app.MapGet("/download/{file}", (string file) =>
# {
# var path = Path.Combine("D:\Files", file);
# var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
# return Results.File(stream, "application/octet-stream",
# enableRangeProcessing: true); // ← Enables Range/206 support
# });
# ── IIS: Disable response buffering for streaming ──
# <system.webServer>
# <handlers>
# <add name="aspNetCore" path="*" verb="*"
# modules="AspNetCoreModuleV2"
# responseBufferLimit="0" /> <!-- No buffering -->
# </handlers>
# </system.webServer>
# ── Test range requests ──
# Download bytes 0-1023:
# curl -H "Range: bytes=0-1023" https://example.com/largefile.zip -o chunk1
# Resume from byte 5000:
# curl -C 5000 https://example.com/largefile.zip -o resumed.zip
A document management system needed to handle 500MB PDF uploads. The default 30MB limit returned 404.13. After increasing both maxAllowedContentLength and maxRequestLength, uploads worked but w3wp.exe memory spiked to 4GB — ASP.NET was buffering the entire file in memory. Switching to streaming upload (MultipartReader) reduced memory usage to 50MB regardless of file size. They also enabled range requests for downloads so users could resume interrupted downloads.
How do you implement chunked/resumable uploads (tus protocol) with IIS?
When IIS acts as a reverse proxy via ARR, several settings critically impact performance:
Request/Response Buffering:
- Request buffering (default: enabled) — ARR buffers the entire request body before forwarding to the backend. Protects backends from slow clients (slowloris), but adds latency for large uploads.
- Response buffering (default: enabled) — ARR buffers the backend's response before sending to the client. Frees backend connections faster, but adds latency for streaming.
- Disable for streaming scenarios (WebSocket, Server-Sent Events, large file downloads).
Connection pooling:
- ARR maintains a pool of persistent connections to each backend server.
- Reuses TCP connections instead of creating new ones for each request — eliminates TCP handshake and TLS negotiation overhead.
maxConnections— max pooled connections per backend (default: unlimited). Set to prevent overwhelming backends.connectionTimeout— how long an idle connection stays in the pool.
Timeout settings:
timeout— time to wait for the backend to respond (default: 120s). Increase for slow operations.receiveTimeout— time between receiving chunks of the response.- Too low → false 502 errors. Too high → connections held for failing backends.
Keep-Alive: Ensure Connection: Keep-Alive is sent to backends to enable connection reuse.
# ── applicationHost.config: ARR Proxy Settings ──
# <system.webServer>
# <proxy enabled="true"
# preserveHostHeader="true"
# reverseRewriteHostInResponseHeaders="true">
#
# <!-- Connection pooling -->
# <cache enabled="true"
# defaultTtl="120"
# maxConnections="256"
# connectionTimeout="60" />
#
# <!-- Timeouts -->
# <timeout value="00:03:00"
# receiveTimeout="00:01:00" />
#
# <!-- Buffering -->
# <requestBufferLimit="4194304" />
# <!-- 4MB buffer limit. 0 = disable buffering -->
#
# <!-- WebSocket support -->
# <webSocketEnabled="true" />
# </proxy>
# </system.webServer>
# ── PowerShell: Configure ARR proxy settings ──
Import-Module WebAdministration
# Set backend timeout to 3 minutes
Set-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "system.webServer/proxy" `
-Name "timeout" -Value "00:03:00"
# Enable connection pooling with limits
Set-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "system.webServer/proxy/cache" `
-Name "maxConnections" -Value 256
# Preserve original host header
Set-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
-Filter "system.webServer/proxy" `
-Name "preserveHostHeader" -Value $true
# ── Disable buffering for streaming (SSE, WebSocket) ──
# <system.webServer>
# <proxy enabled="true">
# <requestBufferLimit>0</requestBufferLimit>
# <responseBufferLimit>0</responseBufferLimit>
# </proxy>
# </system.webServer>
# ── Monitor ARR performance ──
$counters = @(
"\Web Service(_Total)\Current Connections",
"\HTTP Service Request Queues(*)\CurrentQueueSize",
"\HTTP Service Request Queues(*)\RejectedRequests"
)
Get-Counter -Counter $counters -SampleInterval 5
# ── X-Forwarded headers for backend awareness ──
# ARR automatically adds:
# X-Forwarded-For: client IP
# X-Forwarded-Proto: http or https
# X-ARR-LOG-ID: correlation ID for tracing
# X-Forwarded-Port: original port
#
# Backend reads:
# var clientIp = request.Headers["X-Forwarded-For"];
# var isHttps = request.Headers["X-Forwarded-Proto"] == "https";
A SignalR-based dashboard behind ARR had 2-second delays on real-time updates. Response buffering was enabled — ARR waited to receive the full response before forwarding to the client. For Server-Sent Events, there is no "full response" — it's an infinite stream. Disabling response buffering (responseBufferLimit=0) fixed the latency. They also increased maxConnections to 512 because each SSE client held a persistent backend connection.
How do you troubleshoot 502.3 (Bad Gateway - connection timeout) errors in ARR?
Load testing validates that your IIS server can handle expected traffic. A structured approach:
Test types:
- Baseline test — low load (10-50 users) to establish normal response times and resource usage.
- Load test — expected production load. Verify SLAs are met (response time < 200ms, error rate < 0.1%).
- Stress test — increase load beyond expected until the server degrades. Find the breaking point.
- Endurance/Soak test — sustained load for hours/days. Detect memory leaks, connection leaks, log file growth.
- Spike test — sudden traffic burst (e.g., 10x normal). Test auto-scaling and thread pool ramp-up.
Key metrics to capture:
- Throughput (requests/sec) — how many requests the server handles.
- Response time (P50, P95, P99) — percentile latencies, not averages.
- Error rate — percentage of 4xx/5xx responses.
- CPU, Memory, Disk I/O — server-side resource utilization.
- Thread count, Requests Queued — .NET-specific metrics.
- Connection count — active TCP connections.
Tools: Apache JMeter, k6 (JavaScript-based), Locust (Python), Artillery, Azure Load Testing, WCAT (IIS-specific), bombardier, wrk.
# ── k6: Modern load testing script ──
# // k6 script: load-test.js
# import http from "k6/http";
# import { check, sleep } from "k6";
# import { Rate } from "k6/metrics";
#
# const errorRate = new Rate("errors");
#
# export const options = {
# stages: [
# { duration: "2m", target: 100 }, // Ramp up to 100 users
# { duration: "5m", target: 100 }, // Stay at 100 for 5 min
# { duration: "2m", target: 500 }, // Ramp to 500 users
# { duration: "5m", target: 500 }, // Stay at 500 for 5 min
# { duration: "2m", target: 0 }, // Ramp down
# ],
# thresholds: {
# http_req_duration: ["p(95)<200"], // 95% under 200ms
# errors: ["rate<0.01"], // Error rate < 1%
# },
# };
#
# export default function () {
# const res = http.get("https://www.example.com/api/products");
# check(res, {
# "status is 200": (r) => r.status === 200,
# "response time < 500ms": (r) => r.timings.duration < 500,
# });
# errorRate.add(res.status >= 400);
# sleep(1);
# }
# Run: k6 run load-test.js
# ── bombardier: Quick throughput test ──
# bombardier -c 200 -d 60s -l https://www.example.com/
# -c 200: 200 concurrent connections
# -d 60s: run for 60 seconds
# -l: print latency distribution
# ── wrk: High-performance HTTP benchmark ──
# wrk -t12 -c400 -d30s https://www.example.com/
# -t12: 12 threads
# -c400: 400 connections
# -d30s: 30 second test
# ── Server-side monitoring during load test ──
$counters = @(
"\Processor(_Total)\% Processor Time",
"\Memory\Available MBytes",
"\Process(w3wp)\Private Bytes",
"\.NET CLR Memory(w3wp)\% Time in GC",
"\ASP.NET\Requests Queued",
"\Web Service(_Total)\Current Connections",
"\Web Service(_Total)\Get Requests/sec",
"\PhysicalDisk(_Total)\% Disk Time"
)
# Start monitoring before the test
Get-Counter -Counter $counters -SampleInterval 5 -MaxSamples 600 |
Export-Counter -Path "D:\LoadTest\server-metrics.blg" -Force
# ── Analyze results: key thresholds ──
# ✅ Healthy server under load:
# CPU: < 70% (headroom for spikes)
# Memory: > 20% free (no memory pressure)
# GC Time: < 10% (not spending time collecting garbage)
# Requests Queued: < 10 (threads keeping up)
# Disk Time: < 50% (not I/O bound)
# P95 Response: < 200ms
# Error Rate: < 0.1%
Before a product launch, the team ran a load test simulating 1,000 concurrent users with k6. At 400 users, P95 latency spiked from 100ms to 2 seconds and Requests Queued hit 200. Root cause: minWorkerThreads was default (16) and sync database calls blocked threads. Fix: increased minWorkerThreads to 200 and converted hot-path DB calls to async. Retest: 1,000 users at P95 < 150ms. The load test prevented a launch-day outage.
How do you use Application Insights Profiler to identify hot paths during a load test?
Frequently Asked Questions
The most common IIS interview questions cover the request pipeline (kernel-mode vs user-mode), SSL/TLS certificate binding and HTTPS configuration, application pool recycling and identity, performance tuning (compression, caching, HTTP/2), and troubleshooting with Failed Request Tracing. Our guide covers all of these with real-world scenarios.
We cover 40 in-depth IIS interview questions spanning Basic to Performance levels. Each question includes 6 sections: theory, configuration examples, real-world scenario, key takeaway, common mistake, and follow-up question.
Yes. While Kestrel is the default .NET web server, most enterprise production deployments use IIS as a reverse proxy (via the ASP.NET Core Module). Knowledge of IIS is critical for Windows Server deployments, SSL termination, application pool management, and troubleshooting production issues.
Kestrel is a cross-platform, lightweight web server built into ASP.NET Core. IIS is a full-featured Windows web server that acts as a reverse proxy to Kestrel in modern .NET deployments. IIS adds SSL termination, request filtering, static file serving, application pool isolation, and Windows Authentication. In production, IIS (or Nginx/Apache on Linux) sits in front of Kestrel.
Senior and DevOps interviews focus on application pool recycling strategies, ARR load balancing and reverse proxy, in-process vs out-of-process ASP.NET Core hosting, SSL/TLS hardening (disabling TLS 1.0/1.1, cipher suite ordering), performance tuning (kernel-mode caching, thread pool tuning), security hardening, PowerShell automation (IISAdministration module), crash dump analysis with DebugDiag, and CI/CD zero-downtime deployment strategies.