🖥️ 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.

40Questions
5Levels
6Answer Sections
240Total Answers
Showing 40 of 40 questions
0 of 40 viewed
01 How does IIS process a request? Explain the architecture — kernel mode, user mode, and the ASP.NET Core Module. basic

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.exe process 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 IISHttpServer instead 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).

IIS uses HTTP.sys (kernel) for fast request intake and w3wp.exe (user mode) for processing. ASP.NET Core in-process hosting runs inside w3wp.exe for best performance. Out-of-process adds a reverse proxy hop. Always verify your hosting model in web.config.
⚠️ Common Mistake
// ❌ Deploying with OutOfProcess unintentionally // web.config: hostingModel="OutOfProcess" // Every request: Client → HTTP.sys → w3wp → Kestrel → app // Extra HTTP hop adds ~1-3ms latency per request
// ✅ Use InProcess for best performance (default in .NET 6+) // web.config: hostingModel="InProcess" // Request flow: Client → HTTP.sys → w3wp (app runs here) // No extra hop — direct processing inside IIS worker process // ~3× faster than OutOfProcess in benchmarks
🔁 Follow-Up Question

What is the difference between HTTP.sys and Kestrel? Can you use HTTP.sys without IIS?

02 How do you configure SSL/TLS in IIS? Explain certificate binding, HTTPS redirect, HSTS, and TLS hardening. intermediate

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.

Bind certificates with SNI for multi-site hosting. Always redirect HTTP→HTTPS via URL Rewrite. Enable HSTS to prevent downgrade attacks. Disable TLS 1.0/1.1 via registry or IIS Crypto. Verify your configuration with SSL Labs or nmap --script ssl-enum-ciphers.
⚠️ Common Mistake
// ❌ Leaving TLS 1.0 enabled — fails PCI DSS compliance // Default Windows Server allows TLS 1.0/1.1 // Attackers can force downgrade to exploitable protocols (POODLE, BEAST) // No HSTS header — users can be redirected to HTTP via MITM
// ✅ Hardened configuration: // 1. Disable TLS 1.0/1.1 via registry or IIS Crypto // 2. Enable only TLS 1.2 + 1.3 // 3. Prefer ECDHE cipher suites (forward secrecy) // 4. Add HSTS: Strict-Transport-Security: max-age=31536000; includeSubDomains // 5. Redirect all HTTP → HTTPS with 301 // 6. Test with: ssllabs.com/ssltest → target A+ rating
🔁 Follow-Up Question

What is SNI and why was it introduced? What happens if a client does not support SNI?

03 How do application pools work in IIS? Explain recycling, identity, idle timeout, and rapid-fail protection. intermediate

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).

Each app pool is an isolated w3wp.exe. Schedule recycling during off-peak hours (not the 29-hour default). Use ApplicationPoolIdentity for least privilege. Set idle timeout to 0 for production APIs. Enable overlapped recycling and Application Initialization for zero-downtime restarts.
⚠️ Common Mistake
// ❌ Using default settings in production // - 29-hour recycle drifts to peak hours // - 20-minute idle timeout kills the app during quiet periods // - First request after restart is slow (cold start) // - Running as LocalSystem — security nightmare
// ✅ Production-ready configuration: // 1. Schedule recycling: 3 AM (low traffic window) // 2. Idle timeout: 0 (always running) or Suspend action // 3. Start mode: AlwaysRunning + Application Initialization // 4. Identity: ApplicationPoolIdentity (least privilege) // 5. Memory-based recycling: recycle if private memory > 2GB // 6. Overlapped recycling: enabled (zero-downtime restarts)
🔁 Follow-Up Question

What is Application Initialization in IIS? How does it ensure zero-downtime during app pool recycling?

04 How do you optimise IIS performance? Explain compression, kernel-mode caching, HTTP/2, and connection limits. advanced

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 dynamicCompressionBeforeCache carefully — 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%.

Enable static compression always (free after first request). Enable dynamic compression for API responses (trade CPU for bandwidth). Use kernel-mode caching for static/anonymous content. HTTP/2 is automatic over HTTPS on IIS 10+. Tune queue length for bursty workloads. Monitor with performance counters.
⚠️ Common Mistake
// ❌ Compression disabled + no cache headers // Every response sent uncompressed — wastes bandwidth // No Cache-Control headers — browser re-downloads assets every visit // Kernel cache not utilised — every static file hits w3wp.exe // Result: slow pages, high bandwidth costs, high CPU
// ✅ Optimised configuration: // 1. Static compression: enabled (CSS, JS, SVG — cached on disk) // 2. Dynamic compression: enabled (JSON, HTML — Gzip/Brotli) // 3. Static content clientCache: max-age=30 days // 4. Kernel cache profiles: .css, .js, .jpg with CacheForTimePeriod // 5. HTTP/2: automatic over HTTPS on IIS 10+ // Result: 60-70% bandwidth reduction, 10× faster static file serving
🔁 Follow-Up Question

What is the difference between kernel-mode caching and IIS output caching? When does kernel cache not work?

05 How do you troubleshoot IIS issues? Explain Failed Request Tracing, common HTTP errors, and w3wp crash analysis. advanced

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.config locking.
  • 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 procdump or 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.

Enable Failed Request Tracing for 500-599 errors and slow requests (safe for production). Always enable stdoutLogEnabled temporarily when debugging ASP.NET Core startup failures. Know the common error codes: 500.19 (bad config), 502.5 (app won't start), 503 (pool stopped). Use Event Viewer + DebugDiag for w3wp crashes.
⚠️ Common Mistake
// ❌ Leaving stdoutLogEnabled="true" in production permanently // Stdout logs grow unbounded — fills disk, degrades performance // Also exposes sensitive data (connection strings in error stack traces) // web.config: stdoutLogEnabled="true" ← left from debugging
// ✅ Enable stdout logging ONLY for troubleshooting, then disable // web.config: stdoutLogEnabled="false" (production default) // // For persistent diagnostics, use: // 1. Failed Request Tracing (zero overhead when no matches) // 2. Structured logging (Serilog → Seq/ELK) // 3. Application Insights or OpenTelemetry // 4. Event Viewer for crash notifications
🔁 Follow-Up Question

How do you configure proactive health monitoring in IIS? What is Application Request Routing (ARR) and how does it enable load balancing?

06 IIS vs IIS Express vs Kestrel — when should you use each web server? basic

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.

Use Kestrel for development and cross-platform scenarios. Use IIS in production on Windows for SSL, Windows Auth, app pool isolation, and request filtering. IIS Express is for development only when IIS-specific features are needed. Never expose Kestrel directly to the internet without a reverse proxy.
⚠️ Common Mistake
// ❌ Exposing Kestrel directly to the internet // No request filtering, no connection limits, no SSL hardening builder.WebHost.UseKestrel(); // Accessible on public IP without any protection
// ✅ Kestrel behind IIS reverse proxy (production) // IIS handles: SSL termination, request filtering, connection limits, // Windows Auth, kernel-mode caching // web.config: hostingModel="InProcess" // Or on Linux: Nginx/Apache as reverse proxy
🔁 Follow-Up Question

Can Kestrel handle production traffic without a reverse proxy? What are the security risks?

07 What is the difference between a Site, Application, and Virtual Directory in IIS? basic

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.config and 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.

Sites define bindings (domain:port). Applications provide app pool isolation within a site. Virtual Directories map URLs to physical folders without isolation. Use separate Applications when you need different app pools, .NET versions, or fault isolation.
⚠️ Common Mistake
// ❌ Running everything under one app pool // Site root "/" and "/api" in the same pool // API crash takes down the main website // API memory leak affects website performance
// ✅ Separate applications with dedicated app pools // "/" → MyWebsitePool (CMS) // "/api" → ApiPool (REST API — isolated) // "/admin" → AdminPool (admin panel — isolated) // Each can crash/recycle independently
🔁 Follow-Up Question

How does web.config inheritance work across Sites, Applications, and Virtual Directories?

08 How do IIS bindings work? Explain IP addresses, ports, host headers, and wildcard certificates. basic

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):

  1. Exact IP + exact port + exact host header (most specific)
  2. Exact IP + exact port + no host header
  3. Wildcard IP (*) + exact port + exact host header
  4. 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.

Bindings route requests to sites using IP + port + host header. Use host headers to run multiple sites on the same IP:port. Use SNI for multiple HTTPS sites on the same IP:443. Wildcard certificates simplify multi-subdomain deployments. The most specific binding wins.
⚠️ Common Mistake
// ❌ Two sites with identical bindings — only one starts // Site A: *:80:www.example.com // Site B: *:80:www.example.com ← conflict! // IIS refuses to start Site B — "binding already in use" // Also: no host header on *:80 = catch-all steals all traffic
// ✅ Unique bindings per site // Site A: *:80:www.example.com (HTTP) // Site A: *:443:www.example.com (HTTPS + SNI) // Site B: *:80:api.example.com (different host header) // Site B: *:443:api.example.com (different host header) // Each site has unique host header — no conflicts
🔁 Follow-Up Question

What is the difference between "All Unassigned" (*) and a specific IP in bindings? When would you use each?

09 How does IIS logging work? Explain W3C format, log fields, and log management. basic

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 IP
  • cs-method — GET, POST, etc.
  • cs-uri-stem — URL path; cs-uri-query — query string
  • sc-status — HTTP status code; sc-substatus — IIS substatus
  • sc-bytes — response size; time-taken — milliseconds
  • cs(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.

Configure W3C logging with all useful fields (especially time-taken and substatus). Store logs on a separate drive, set up automatic cleanup, and ship to a centralised log system (ELK, Splunk). Remember time-taken is in milliseconds and includes network time. Logs are in UTC by default.
⚠️ Common Mistake
// ❌ Default logging with no management // Logs on C: drive — fills up, crashes the server // No cleanup — years of logs accumulate // Missing useful fields (time-taken, substatus) // No centralized logging — troubleshooting requires RDP to each server
// ✅ Production logging setup: // 1. Logs on separate drive: D:\Logs\IIS // 2. W3C format with all fields including time-taken, substatus // 3. Daily rollover // 4. Scheduled cleanup: delete logs older than 30 days // 5. Ship to ELK/Splunk for centralized analysis and alerting // 6. Monitor disk space with health checks
🔁 Follow-Up Question

What is Enhanced Logging in IIS 8.5+? How do you log custom request/response headers?

10 What is Request Filtering in IIS? How does it protect against common web attacks? basic

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 limitsmaxQueryString (default 2048 bytes).
  • Content lengthmaxAllowedContentLength (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.

Request Filtering is your first line of defense — it blocks attacks before they reach your code. Configure URL/query/content length limits, block dangerous extensions (.config, .exe), deny path traversal sequences (".."), and hide sensitive segments (.git, bin). Returns 404 substatus codes for blocked requests.
⚠️ Common Mistake
// ❌ Default request filtering — missing critical rules // .git folder accessible: https://example.com/.git/HEAD // web.config downloadable via crafted URL // TRACE verb enabled — allows XST (Cross-Site Tracing) attacks // No content length limit — 2GB upload DoS possible
// ✅ Hardened request filtering: // 1. Hidden segments: .git, bin, App_Data, node_modules // 2. Blocked extensions: .config, .cs, .exe, .bat, .csproj // 3. Denied sequences: "..", "web.config" // 4. Blocked verbs: TRACE, TRACK // 5. Content limit: 50MB (or whatever your app needs) // 6. Double-encoding: blocked (allowDoubleEscaping="false")
🔁 Follow-Up Question

What is the difference between Request Filtering and URL Rewrite for security? Can they work together?

11 How does URL Rewrite work in IIS? Explain rules, conditions, and common patterns. intermediate

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.

Use Redirect (301) for permanent URL changes and domain canonicalization — clients and search engines update their links. Use Rewrite for internal routing — clean URLs mapped to actual handlers. Always set stopProcessing="true" on redirects to avoid double processing. Test rules with the URL Rewrite test tool in IIS Manager.
⚠️ Common Mistake
// ❌ Using 302 (temporary) instead of 301 (permanent) for canonical redirect // Search engines don't transfer link equity with 302 // Users' bookmarks keep pointing to the old URL <action type="Redirect" url="https://www.example.com/{R:1}" redirectType="Found" /> <!-- 302 = temporary -->
// ✅ Use 301 for permanent changes — SEO-friendly <action type="Redirect" url="https://www.example.com/{R:1}" redirectType="Permanent" /> <!-- 301 = permanent --> // Search engines transfer link equity, browsers cache the redirect
🔁 Follow-Up Question

How do outbound rules work? When would you rewrite URLs in the response body?

12 What is Application Initialization in IIS? How does it eliminate cold-start delays? intermediate

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:

  1. IIS starts a new worker process.
  2. The new process receives the warmup request and completes initialization.
  3. Only after warmup completes does IIS route real traffic to the new process.
  4. The old process finishes existing requests and shuts down.
  5. 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.

Use Application Initialization (AlwaysRunning + preloadEnabled) to eliminate cold starts. Configure a warmup URL that triggers expensive initialization (EF Core, caches, connections). Combine with overlapped recycling for zero-downtime restarts. Use remapManagedRequestsTo for a loading page during warmup.
⚠️ Common Mistake
// ❌ Default settings — cold start on every recycle // App pool: startMode = OnDemand (waits for first request) // First request after recycle: 5-15 seconds (DI + EF Core + cache) // Users hit timeout errors during peak-hour recycling
// ✅ Zero-downtime with Application Initialization // App pool: startMode = AlwaysRunning // Site: preloadEnabled = true // web.config: applicationInitialization with warmup URLs // Overlapped recycling: new worker warms up before old one shuts down // Result: zero cold-start delay for users
🔁 Follow-Up Question

How does IIS Application Initialization compare to ASP.NET Core's IHostedService for warmup?

13 How does Windows Authentication work in IIS? Compare Negotiate, NTLM, and Kerberos. intermediate

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.

Use Negotiate (Kerberos first, NTLM fallback) for Windows Auth. Configure SPNs when using a custom service account. For the double-hop problem, set up Kerberos Constrained Delegation in AD. Always disable Anonymous Auth when enabling Windows Auth.
⚠️ Common Mistake
// ❌ Using NTLM only — no delegation support // <providers> // <add value="NTLM" /> <!-- Kerberos skipped --> // </providers> // Double-hop fails: Web Server cannot pass credentials to SQL Server // Each request re-authenticates (no ticket caching) — slower
// ✅ Use Negotiate — Kerberos first with NTLM fallback // <providers> // <add value="Negotiate" /> // <add value="NTLM" /> // </providers> // + Register SPNs: setspn -S HTTP/hostname DOMAIN\Account // + Configure Constrained Delegation for double-hop // Kerberos: ticket-based, delegation, faster (cached)
🔁 Follow-Up Question

What is Kerberos Constrained Delegation vs Resource-Based Constrained Delegation? When do you use each?

14 How does IIS handle static files? Explain MIME types, client caching, and ETags. intermediate

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.config under <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: etag on 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.

Register MIME types for all file formats you serve (.json, .woff2, .webp, .svg). Set aggressive Cache-Control max-age for versioned assets (CSS/JS with hashes). Use short or no cache for HTML. Disable ETags on web farms or use consistent generation. A 304 response saves bandwidth but still costs a round trip.
⚠️ Common Mistake
// ❌ Missing MIME type — IIS returns 404.3 // .woff2 font fails to load → text renders in fallback font // .webp images fail → broken images on the page // No MIME type registered = IIS refuses to serve the file
// ✅ Register all modern MIME types // <staticContent> // <mimeMap fileExtension=".woff2" mimeType="font/woff2" /> // <mimeMap fileExtension=".webp" mimeType="image/webp" /> // <mimeMap fileExtension=".json" mimeType="application/json" /> // </staticContent> // Always test after deployment: check browser DevTools Network tab
🔁 Follow-Up Question

What is the difference between IIS kernel-mode caching and output caching? When is each used?

15 How does IIS handle CORS? Explain cross-origin configuration and common pitfalls. intermediate

CORS (Cross-Origin Resource Sharing) controls which external domains can make AJAX requests to your IIS-hosted API.

How CORS works:

  • Browser sends an Origin header 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.

Use the IIS CORS Module (IIS 10+) or ASP.NET Core CORS middleware — both handle preflight properly. Avoid the custom headers approach (doesn't handle OPTIONS). Never use Allow-Origin: * with Allow-Credentials: true (browsers block it). Specify exact origins in production.
⚠️ Common Mistake
// ❌ Allow all origins with credentials — security vulnerability // <add name="Access-Control-Allow-Origin" value="*" /> // <add name="Access-Control-Allow-Credentials" value="true" /> // Browsers block this combination — and if they didn't, // any site could make authenticated requests to your API
// ✅ Specific origins with the IIS CORS Module // <cors enabled="true" failUnlistedOrigins="true"> // <add origin="https://www.example.com" // allowCredentials="true" /> // </cors> // Only www.example.com can make credentialed requests // failUnlistedOrigins blocks all other origins
🔁 Follow-Up Question

What is the difference between CORS and CSP (Content Security Policy)? How do they complement each other?

16 How does web.config inheritance work in IIS? Explain hierarchy, overrides, and section locking. intermediate

IIS configuration uses a hierarchical inheritance model. Settings flow downward and can be overridden at each level:

  1. machine.config — .NET framework defaults (C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config).
  2. applicationHost.config — IIS server-level settings (C:\Windows\System32\inetsrv\config). Defines sites, app pools, global modules.
  3. root web.config — .NET root-level config.
  4. Site web.config — site root.
  5. 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 block in applicationHost.config. This approach kept the section locked for all other sites while allowing the specific app to configure it.

IIS config inherits downward: applicationHost.config → site web.config → subfolder web.config. Locked sections (Deny) cannot be overridden — unlock with blocks for specific sites. A 500.19 error with "cannot be used at this path" means a locked section. Use Get-WebConfiguration to see the effective merged config.
⚠️ Common Mistake
// ❌ Trying to enable Windows Auth in web.config when section is locked // web.config: // <windowsAuthentication enabled="true" /> // Result: 500.19 — "This configuration section cannot be used at this path" // The section is locked in applicationHost.config (overrideModeDefault="Deny")
// ✅ Ask the server admin to unlock the section for your site // In applicationHost.config: // <location path="MySite" overrideMode="Allow"> // <system.webServer> // <security> // <authentication> // <windowsAuthentication /> // </authentication> // </security> // </system.webServer> // </location> // Now web.config can configure windowsAuthentication
🔁 Follow-Up Question

How do you troubleshoot which level of config is causing a problem? What tools show merged configuration?

17 How do you configure custom error pages in IIS? Explain httpErrors vs customErrors. intermediate

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.

Use httpErrors (not customErrors) for error pages — it handles all content types. Use File responseMode for static error pages (fastest). Use ExecuteURL for dynamic error pages. Never use Redirect — it loses the status code and hurts SEO. Set existingResponse="Auto" to respect application-generated error responses.
⚠️ Common Mistake
// ❌ Using Redirect mode for 404 — loses status code // <error statusCode="404" path="/errors/not-found" // responseMode="Redirect" /> // User sees: 302 → /errors/not-found (200 OK) // Google indexes your error page as real content! // Original URL is lost in the address bar
// ✅ Use File or ExecuteURL — preserves status code // <error statusCode="404" path="/errors/404.html" // responseMode="File" /> // User sees: 404 Not Modified with a friendly error page // Google correctly treats it as a missing page // Original URL stays in the address bar
🔁 Follow-Up Question

How does existingResponse="PassThrough" interact with ASP.NET Core exception handling middleware?

18 How do IP restrictions and Dynamic IP Security work in IIS? intermediate

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.0 with subnet mask 255.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.

Use static IP restrictions for known bad actors or to whitelist admin panels. Use Dynamic IP Security for automated DDoS/brute-force protection. Enable enableProxyMode when behind a load balancer. Set denyAction="AbortRequest" to silently drop malicious connections. Monitor 403.501 entries in IIS logs for dynamic blocks.
⚠️ Common Mistake
// ❌ No proxy mode behind a load balancer // <dynamicIpSecurity enableProxyMode="false"> // All requests show the load balancer's IP (e.g., 10.0.0.1) // Rate limiting blocks the LOAD BALANCER — all users blocked!
// ✅ Enable proxy mode to read X-Forwarded-For // <dynamicIpSecurity enableProxyMode="true"> // IIS reads X-Forwarded-For header for the real client IP // Rate limiting correctly targets individual clients // Ensure the LB sets X-Forwarded-For properly
🔁 Follow-Up Question

How does Dynamic IP Security interact with a CDN like Cloudflare? What additional configuration is needed?

19 What are IIS modules? Explain native vs managed modules and the request pipeline. intermediate

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.config under <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.config under <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.

IIS is fully modular — every feature is a pluggable module. Remove modules you don't use (WebDAV, CGI, ISAPI) to reduce attack surface and improve performance. Native modules are faster but need server-level registration. Managed modules can be deployed in web.config. The 405 error with PUT/DELETE often means WebDAVModule is interfering.
⚠️ Common Mistake
// ❌ WebDAVModule blocks PUT/DELETE for Web API // PUT /api/products/42 → 405 Method Not Allowed // WebDAV intercepts the verb before your API handler runs // All installed modules run by default even if you don't use them
// ✅ Remove WebDAV module and handler // <modules> // <remove name="WebDAVModule" /> // </modules> // <handlers> // <remove name="WebDAV" /> // </handlers> // PUT/DELETE now reach your Web API handler correctly // Audit and remove other unused modules too
🔁 Follow-Up Question

What is the difference between preCondition="managedHandler" and no precondition? When does a managed module run for static files?

20 How does Output Caching work in IIS? Explain user-mode vs kernel-mode caching. intermediate

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%.

Kernel-mode caching is the fastest path — responses never leave HTTP.sys. Use it for anonymous, public content. User-mode caching supports vary-by parameters for dynamic content. Set appropriate TTLs — short for frequently changing data, long for static content. Monitor kernel cache hit ratios with performance counters. Use netsh http show cachestate to inspect cached entries.
⚠️ Common Mistake
// ❌ Caching authenticated/personalized responses // User A's dashboard gets cached → User B sees User A's data! // Kernel cache ignores cookies and auth headers // NEVER cache content that varies by user identity
// ✅ Only cache anonymous, public content in kernel mode // <add extension=".aspx" kernelCachePolicy="DontCache" /> // Use user-mode cache with varyByHeaders="Cookie" for personalized content // Or better: cache at the application layer where you control identity // Public pages → kernel cache | Private pages → no cache or app-level cache
🔁 Follow-Up Question

How does IIS output caching interact with CDN caching? Can they conflict?

21 How does Application Request Routing (ARR) work? Explain load balancing and reverse proxy. advanced

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 Host header, adds X-Forwarded-For, X-ARR-LOG-ID for 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.

ARR turns IIS into a powerful L7 load balancer and reverse proxy. Use server farms with health checks for high availability. Use Weighted Round Robin for gradual rollouts (canary). Enable client affinity only if your app requires sticky sessions (it hurts load distribution). SSL offloading at ARR reduces CPU load on backend servers.
⚠️ Common Mistake
// ❌ Enabling client affinity (sticky sessions) by default // <affinity useCookie="true" /> // One server handles all requests from a client // If that server goes down, user loses their session // Uneven load distribution — some servers overloaded, others idle
// ✅ Use distributed session (Redis/SQL) + no affinity // <affinity useCookie="false" /> // Any server can handle any request // Failed server = seamless failover (session is in Redis) // Even load distribution across all servers // Only use affinity as a last resort for legacy apps
🔁 Follow-Up Question

How do you configure ARR for blue-green deployments with zero-downtime switchover?

22 How does WebSocket support work in IIS? What are the configuration requirements? advanced

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 Module handles the HTTP → WebSocket upgrade handshake.
  • After upgrade, the connection is persistent — no HTTP request/response overhead.

How the handshake works:

  1. Client sends HTTP GET with Upgrade: websocket and Connection: Upgrade headers.
  2. IIS validates the request and returns 101 Switching Protocols.
  3. 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 webSocket settings 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.

Install the WebSocket feature separately — it's not enabled by default. WebSocket connections are persistent, so plan for connection limits and memory. Configure keepalive pings to prevent proxy/LB timeouts. Disable app pool idle timeout for WebSocket apps. ARR 3.0+ supports WebSocket proxy — enable webSocketEnabled="true".
⚠️ Common Mistake
// ❌ Default app pool settings with WebSocket app // Idle timeout: 20 minutes → kills all WebSocket connections when quiet // Regular recycling: 29 hours → abruptly drops all connections // No keepalive → ARR/LB closes "idle" connections after 2 minutes
// ✅ Configure for persistent connections // App pool: idle timeout = 0 (never idle out) // App pool: regular time interval = 0 (disable periodic recycling) // Or: use overlapped recycling + client reconnect logic // WebSocket keepalive: ping every 15-30 seconds // ARR: increase connection timeout to match your needs
🔁 Follow-Up Question

How does SignalR negotiate transport (WebSocket → SSE → Long Polling) and how does IIS configuration affect each?

23 How do you configure IIS with ASP.NET Core? Explain in-process vs out-of-process hosting. advanced

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 IISHttpServer instead 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.

Use in-process hosting (default) for best performance — no proxy overhead, ~2x throughput. Use out-of-process only when you need multiple apps per pool or process isolation. Always enable stdoutLogEnabled for troubleshooting. Check Event Viewer for ANCM startup errors (500.30, 502.5). Set environment variables in web.config, not system-wide.
⚠️ Common Mistake
// ❌ Two ASP.NET Core apps in one app pool with InProcess // Both apps try to load into w3wp.exe // Second app fails with 500.30 — only one InProcess app per pool // No error message clearly explains the conflict
// ✅ One app per app pool with InProcess hosting // App1 → AppPool1 (InProcess) — runs inside w3wp.exe // App2 → AppPool2 (InProcess) — separate w3wp.exe // Or: use OutOfProcess if you must share a pool // Each InProcess app gets its own isolated worker process
🔁 Follow-Up Question

How does ANCM handle process crashes and restarts? What is the rapidFailProtection interaction?

24 How do you manage applicationHost.config? Explain the IIS Configuration Editor and schema. advanced

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.

applicationHost.config is the IIS master config — always back up before changes. Use the Configuration Editor to explore hidden settings and generate scripts. Export site/pool configs with appcmd for migration. Use configuration history to track changes. Validate config with appcmd verify config before restarting IIS.
⚠️ Common Mistake
// ❌ Editing applicationHost.config directly without backup // One typo = all sites go down (500.19 errors globally) // No way to know what was changed or roll back // File is locked while IIS is running — edits may corrupt
// ✅ Always backup before editing // appcmd add backup "BeforeChanges" // Use IIS Manager or PowerShell instead of direct editing // Test with: appcmd verify config // Keep backups in source control for audit trail // Use configuration history to diff changes
🔁 Follow-Up Question

How does IIS Shared Configuration work for web farms? What are the pitfalls?

25 How do Server Farms and health checking work in IIS with ARR? advanced

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.

Configure both URL-based and live traffic health checks for comprehensive monitoring. Use responseMatch to validate the health endpoint actually checks dependencies (not just returns 200). Implement graceful drain for zero-downtime maintenance. Use LeastCurrentRequest algorithm for uneven workloads. Monitor ARR performance counters for farm health.
⚠️ Common Mistake
// ❌ Health check only verifies HTTP 200 (shallow check) // /health returns 200 even when database is down // Server stays in rotation, serving 500 errors to users // ARR thinks the server is healthy because /health returns 200
// ✅ Deep health check that verifies all dependencies // /health checks: database, Redis, external APIs // Returns 503 if ANY critical dependency is down // ARR removes server from rotation immediately // Use responseMatch="OK" to verify response body too // app.MapHealthChecks("/health") with AddSqlServer, AddRedis
🔁 Follow-Up Question

How do you implement blue-green deployments using ARR server farms and URL Rewrite rules?

26 How does Web Deploy (MSDeploy) work? Explain automated deployments to IIS. advanced

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.

Use Web Deploy for repeatable, automated IIS deployments. Create packages with MSBuild/dotnet publish for consistency. Use the Handler (not Remote Agent) for secure HTTPS deployments. Enable AppOffline rule to show a maintenance page during deploy. Skip logs and user-uploaded content. Use -whatIf to preview before applying.
⚠️ Common Mistake
// ❌ Deploying without AppOffline — users see errors during file copy // Files are partially updated — DLL mismatch errors (500) // web.config changes mid-request — app pool crashes // Users see yellow screen of death during the 30-second window
// ✅ Use AppOffline for graceful deployment // msdeploy -verb:sync ... -enableRule:AppOffline // 1. Drops app_offline.htm → IIS returns 503 to all requests // 2. Syncs all files safely (no in-flight requests) // 3. Removes app_offline.htm → app restarts with new code // Users see a "maintenance" page instead of errors
🔁 Follow-Up Question

How does Web Deploy compare to Octopus Deploy and Azure DevOps release pipelines for IIS deployment?

27 How do you configure HTTP/2 and HTTP/3 in IIS? What are the requirements and benefits? advanced

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%.

HTTP/2 is enabled by default on IIS 10+ for HTTPS. Verify it's working with browser DevTools (look for "h2"). Remove HTTP/1.1 workarounds like domain sharding and CSS sprites. For HTTP/3, you need Windows Server 2022, TLS 1.3, and UDP 443 open. HTTP/3 is best for mobile/lossy networks. Always ensure your load balancer supports the protocol end-to-end.
⚠️ Common Mistake
// ❌ Load balancer terminates TLS without ALPN → HTTP/2 breaks // Client → LB (TLS termination, no ALPN) → IIS (HTTP/1.1) // Browser can't negotiate HTTP/2 because ALPN is stripped // All HTTP/1.1 limitations apply: 6 connections per domain
// ✅ Ensure ALPN is preserved end-to-end // Client → LB (TLS with ALPN: h2) → IIS (HTTP/2) // Or: TLS passthrough at LB, IIS handles TLS + HTTP/2 // Verify with: curl -v --http2 https://site.com // Look for "ALPN: h2" in the TLS handshake
🔁 Follow-Up Question

How does HTTP/2 Server Push work in IIS? Is it still recommended given browser preload hints?

28 How does IIS Shared Configuration work for web farms? What are the pitfalls? experienced

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:

  1. Export the configuration from one server (including machine keys).
  2. Place it on a network share (e.g., \\fileserver\iisconfig).
  3. Each IIS server points to the share via IIS Manager → Shared Configuration.
  4. 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.

Shared Configuration ensures consistency across web farms but creates a dependency on the file share. Use DFS-R or SOFS for high availability. Always keep local backups. Test config changes on one server before exporting. Use environment variables for machine-specific paths. Have a rollback plan — disable shared config to fall back to local config.
⚠️ Common Mistake
// ❌ Shared config on a single file share, no HA // File share goes down → ALL IIS servers stop working // No local backup → can't recover quickly // Config changes are instant across all servers — no testing
// ✅ DFS-R for resilient shared configuration // Each server reads from its LOCAL DFS replica // File share failure → servers keep running on cached replica // Change process: test on one server → export to share → DFS replicates // Keep local backup: appcmd add backup "PreChange"
🔁 Follow-Up Question

How do you handle SSL certificates across a shared configuration? What about the Central Certificate Store?

29 What is the IIS Central Certificate Store (CCS)? How does it simplify SSL management? experienced

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.

Use CCS for multi-server environments — certificates managed in one place. Name PFX files by hostname. All PFX files must share the same password. Combine with SNI for IP-address-independent SSL. Automate with ACME/Let's Encrypt exporting to the share. CCS eliminates per-server certificate management.
⚠️ Common Mistake
// ❌ Installing certificates on each server individually // 5 servers × 50 certs = 250 manual installations // Forgot to renew on server 3 → SSL warnings for some users // Load balancer sends users to random servers — inconsistent certs
// ✅ Central Certificate Store — one location for all certs // Drop www.example.com.pfx on the share → all 5 servers serve it // Renew once → all servers pick up the new cert automatically // Combine with ACME automation for zero-touch renewals // No per-server management needed
🔁 Follow-Up Question

How do you handle SAN (Subject Alternative Name) certificates with CCS? What about wildcard certificates?

30 What is an IIS security hardening checklist? Explain key steps to secure a production server. experienced

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: DENY or SAMEORIGIN — prevents clickjacking.
  • Strict-Transport-Security (HSTS) — forces HTTPS.
  • Content-Security-Policy — controls resource loading.
  • Remove: X-Powered-By, Server headers (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.

Harden IIS before going to production: remove disclosure headers (X-Powered-By, Server), disable old TLS (< 1.2), add security headers (HSTS, CSP, X-Frame-Options), restrict requests (filtering, IP security), and use least-privilege app pool identities. Use IIS Crypto tool for TLS configuration. Run a security scanner (SSL Labs, SecurityHeaders.com) to verify.
⚠️ Common Mistake
// ❌ Default IIS installation exposed to the internet // X-Powered-By: ASP.NET → attacker knows the tech stack // TLS 1.0 enabled → vulnerable to POODLE, BEAST attacks // No HSTS → users can be downgraded to HTTP (MITM) // Directory browsing on → attackers see file structure
// ✅ Hardened IIS production server // Remove X-Powered-By and Server headers // TLS 1.2+ only with strong cipher suites // HSTS with includeSubDomains and preload // Request Filtering + Dynamic IP Security // Custom service account with minimum permissions // Regular security scans: SSL Labs grade A+
🔁 Follow-Up Question

How do you achieve an A+ rating on SSL Labs for an IIS server? What specific cipher suite order is recommended?

31 How do you automate IIS management with PowerShell? Explain the WebAdministration and IISAdministration modules. experienced

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.

Use IISAdministration (not WebAdministration) for new scripts — it's faster and supports PowerShell 7. Use Start-IISCommitDelay for atomic multi-step changes. Automate routine tasks: provisioning, certificate renewal, health checks. Use DSC for declarative configuration management across server farms. Version-control all IIS automation scripts.
⚠️ Common Mistake
// ❌ Making changes without commit delay — partial state on error // New-IISSite "MyApp" ... ← succeeds // Set-IISAppPool ... ← fails (typo in name) // Site exists but has wrong app pool — inconsistent state // Each cmdlet commits immediately by default
// ✅ Use commit delay for atomic changes // Start-IISCommitDelay // New-IISSite "MyApp" ... // Set-IISAppPool ... // Stop-IISCommitDelay ← all changes commit together // If any step fails, nothing is committed // Rollback is automatic — no partial configuration
🔁 Follow-Up Question

How does PowerShell DSC compare to Ansible/Chef/Puppet for IIS configuration management?

32 How do you monitor IIS with performance counters? Which counters matter most? experienced

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.

Monitor these counters: Requests Queued (< 10), % Time in GC (< 10%), Active Requests, Current Connections, and Private Bytes. Use Data Collector Sets for long-term trending. Set alerts on threshold breaches. Correlate spikes across counters to find root causes. Separate heavy workloads into their own app pools to isolate impact.
⚠️ Common Mistake
// ❌ No monitoring — find out about problems from users // "The site is slow" → no data to diagnose // No historical baseline → can't tell if current behavior is normal // Memory leak grows for weeks → app pool crashes at 3 AM
// ✅ Proactive monitoring with alerts // Data Collector Set running 24/7 with key counters // Alerts: Requests Queued > 10, GC Time > 15%, Private Bytes > 2GB // Weekly review of trends to catch slow degradation // Baseline during normal load → detect anomalies early
🔁 Follow-Up Question

How do you integrate IIS performance counters with Application Insights or Prometheus/Grafana?

33 How do you analyze IIS crash dumps with DebugDiag? Explain memory dump analysis workflow. experienced

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.

Use DebugDiag for crash analysis — set up rules before the crash happens. For memory leaks, use the memory leak rule to track allocations over time. For hangs, take 2-3 manual dumps 30 seconds apart and compare thread states. Always capture full dumps for memory analysis (mini dumps lack heap data). DebugDiag's HTML reports provide actionable recommendations.
⚠️ Common Mistake
// ❌ Restarting the app pool without capturing a dump // "The app pool crashed again, let's just restart it" // No dump = no root cause analysis // The same crash will happen again — and again // You're treating symptoms, not the disease
// ✅ Configure DebugDiag rules BEFORE the crash // Crash rule → captures dump automatically on unhandled exception // Memory leak rule → captures dump when Private Bytes > threshold // Analyze the dump → find the root cause → fix the code // Prevent future crashes instead of firefighting
🔁 Follow-Up Question

How do you use WinDbg with SOS extension for advanced .NET dump analysis? When is it preferred over DebugDiag?

34 What is a Web Garden in IIS? When should you use multiple worker processes? experienced

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.

Web Gardens (maxProcesses > 1) are rarely needed for modern ASP.NET apps. They help only when the app has process-level bottlenecks (single-threaded COM, global locks). For most apps, keep maxProcesses=1 and scale with ARR + multiple servers instead. If you use Web Gardens, switch to distributed session/cache (Redis/SQL). Consider multiple app pools behind ARR as a better alternative.
⚠️ Common Mistake
// ❌ Enabling Web Garden with in-process session state // maxProcesses = 4 with default InProc session // User logs in on process 1 → next request goes to process 3 // Session not found → user logged out randomly // Shopping cart items disappear between page loads
// ✅ If you must use Web Garden, use distributed state // Session: SQL Server or Redis (shared across all processes) // Cache: Redis or NCache (not in-memory) // Data Protection: shared key ring (Redis or file share) // But first ask: do you REALLY need a Web Garden? // Modern async code rarely needs multiple worker processes
🔁 Follow-Up Question

How does CPU affinity (NUMA-aware) work with Web Gardens? When does processor affinity help?

35 How do you integrate IIS deployments with CI/CD pipelines? Explain zero-downtime strategies. experienced

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:

  1. Builddotnet publish, run tests, create Web Deploy package.
  2. Artifact — store package in artifact repository.
  3. Deploy — push to target server (Web Deploy, PowerShell remoting, or agent).
  4. Smoke test — hit health endpoint, verify key pages.
  5. Switch traffic — update ARR routing or DNS.
  6. Monitor — watch error rates, response times for 15 minutes.
  7. 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.

Use blue-green or rolling deployments for zero downtime. Automate everything — build, deploy, health check, switch, monitor, rollback. App_offline.htm is acceptable for non-critical apps. Blue-green gives instant rollback. Rolling deployments work well with ARR health checks. Always run smoke tests before switching traffic. Monitor error rates after deployment — auto-rollback if thresholds are exceeded.
⚠️ Common Mistake
// ❌ Manual RDP deployment to production // 1. RDP to server → 2. Stop site → 3. Copy files → 4. Start site // 15 minutes of downtime per deployment // Forgot to copy the config transform → app crashes // No rollback plan — previous version was overwritten // Deployed on Friday afternoon → spent the weekend debugging
// ✅ Automated CI/CD with blue-green deployment // Push to main → build → package → deploy to inactive farm // Health check passes → switch ARR routing (zero downtime) // Errors detected → auto-rollback in 5 seconds // Previous version is still running on the other farm // Audit trail: who deployed what, when, from which commit
🔁 Follow-Up Question

How do you handle database schema changes in a blue-green deployment where both versions must work simultaneously?

36 How does kernel-mode caching work in HTTP.sys? When does it bypass IIS entirely? performance

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:

  1. A request arrives at HTTP.sys (kernel driver).
  2. HTTP.sys checks its URI cache — a hash table of cached responses keyed by URI.
  3. If the URI is cached and valid → response is sent directly from kernel memory. No context switch to user mode. No w3wp.exe involved.
  4. 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 kernelCachePolicy configured.
  • Dynamic responses where IIS Output Caching has kernelCachePolicy set.
  • Responses with proper Cache-Control headers.

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-Cookie headers.
  • 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%.

Kernel-mode caching is the single most impactful IIS performance optimization. Use netsh http show cachestate to verify it's working. Monitor Kernel URI Cache Hits % — target above 80%. Ensure responses don't have Set-Cookie or Authorization headers for cacheable content. Increase MaxCacheResponseSize if you have large static files. Kernel cache is flushed on app pool recycle.
⚠️ Common Mistake
// ❌ Setting Cache-Control: private on public content // Response header: Cache-Control: private, no-cache // HTTP.sys sees "private" → skips kernel cache // Every request goes to w3wp.exe → 15ms instead of 0.1ms // Server handles 3,000 req/sec instead of 80,000
// ✅ Use Cache-Control: public for anonymous content // Response header: Cache-Control: public, max-age=60 // HTTP.sys caches the response in kernel memory // Subsequent requests served in 0.1ms from kernel // Server handles 80,000+ req/sec for cached content // Only use "private" for user-specific responses
🔁 Follow-Up Question

How does HTTP.sys kernel cache interact with CDN edge caching? Can they have conflicting TTLs?

37 How do you tune IIS connection limits and thread pool settings for high traffic? performance

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 minWorkerThreads to 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.

The #1 high-traffic fix: increase minWorkerThreads to match your expected concurrent load. The default (CPU count) is too low for I/O-heavy apps. Lower connectionTimeout to free idle connections. Increase queueLength only as a buffer — queued requests mean you're already overloaded. Use async/await everywhere to reduce thread consumption. Monitor thread count and Requests Queued.
⚠️ Common Mistake
// ❌ Default minWorkerThreads (= CPU count, e.g., 8) // 9 AM spike: 500 concurrent requests arrive // Thread pool has 8 threads → 492 requests queued // New thread injected every 500ms → 246 seconds to reach 500 threads! // Most requests time out → 503 errors → users see "Service Unavailable"
// ✅ Pre-create threads for expected load // ThreadPool.SetMinThreads(200, 200); // 9 AM spike: 500 concurrent requests arrive // 200 threads ready immediately → only 300 need to be created // Thread pool ramps up in ~150 seconds (not 246) // Plus async/await: each thread handles many requests concurrently // Result: 0 timeouts, 0 503 errors
🔁 Follow-Up Question

How does async/await reduce thread consumption in ASP.NET Core? What happens when you block a thread pool thread?

38 How does IIS handle large file uploads and downloads? Explain range requests and streaming. performance

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-1023 header 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 FileStreamResult or File(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.

Increase both maxAllowedContentLength (IIS) and maxRequestLength (ASP.NET) for large uploads. Use streaming (MultipartReader) to avoid buffering entire files in memory. Enable range processing for large file downloads (enableRangeProcessing: true). Set responseBufferLimit=0 for streaming responses. Increase executionTimeout for long-running uploads.
⚠️ Common Mistake
// ❌ Buffered upload — loading 500MB file into memory // var file = Request.Form.Files[0]; // Buffers entire file in RAM // var bytes = new byte[file.Length]; // 500MB byte array allocated // w3wp.exe memory: 500MB per concurrent upload // 8 simultaneous uploads = 4GB memory → OutOfMemoryException
// ✅ Streaming upload — constant memory regardless of file size // var reader = new MultipartReader(boundary, request.Body); // await section.Body.CopyToAsync(fileStream); // Stream to disk // w3wp.exe memory: ~50MB regardless of file size // 8 simultaneous uploads = still ~50MB total // Works for any file size — even 10GB+
🔁 Follow-Up Question

How do you implement chunked/resumable uploads (tus protocol) with IIS?

39 How do you optimize reverse proxy performance with ARR? Explain buffering, timeouts, and connection pooling. performance

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.

Disable response buffering for streaming (SSE, WebSocket, long-polling). Set appropriate backend timeouts — 120s default is often too short for report generation, too long for APIs. Use connection pooling to reduce TCP/TLS overhead. Set maxConnections to prevent overwhelming backends. Always set preserveHostHeader=true so backends see the correct domain.
⚠️ Common Mistake
// ❌ Response buffering enabled for SSE/streaming // Backend sends: data: {"price": 42.50}\n\n (immediately) // ARR buffers: "Is there more coming?" (waits...) // Client sees: nothing for 2 seconds (buffer timeout) // Real-time dashboard shows stale data
// ✅ Disable buffering for streaming responses // responseBufferLimit = 0 // Backend sends: data: {"price": 42.50}\n\n // ARR forwards: immediately to client // Client sees: real-time price update in <50ms // Also works for WebSocket and chunked transfer encoding
🔁 Follow-Up Question

How do you troubleshoot 502.3 (Bad Gateway - connection timeout) errors in ARR?

40 How do you benchmark and load test an IIS server? Explain tools, methodology, and key metrics. performance

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.

Always load test before production launches and after major changes. Use k6 or JMeter for realistic user simulation. Monitor server-side metrics during tests (PerfMon). Focus on percentile latencies (P95, P99) — averages hide outliers. Run soak tests to find memory leaks. Set pass/fail thresholds: P95 < 200ms, error rate < 0.1%, CPU < 70%.
⚠️ Common Mistake
// ❌ Testing from a single machine on the same network // Load generator and IIS on the same server → unrealistic latency // Network is not the bottleneck → misses real-world network issues // Single client IP → Dynamic IP Security blocks the test // Testing only happy path → doesn't test error handling under load
// ✅ Realistic load testing setup // Load generators on separate machines (or cloud: Azure Load Testing) // Test from multiple geographic locations // Simulate realistic user behavior (think time, varied URLs) // Include error scenarios (404s, timeouts, large uploads) // Whitelist load test IPs in Dynamic IP Security // Capture both client-side and server-side metrics
🔁 Follow-Up Question

How do you use Application Insights Profiler to identify hot paths during a load test?

Frequently Asked Questions

Written and reviewed by the FreeBytes Editorial Team · Last updated: June 2026