A redirect loop on IIS is almost never the rewrite rule you blame first. It is a disagreement between the rule the request entered through and the hostname, scheme or path the app thinks it should serve. Browsers report ERR_TOO_MANY_REDIRECTS after roughly 20 hops, leaving an IIS log full of 301s that each look correct in isolation.
The scenarios below cover the loops that appear most often on hosted Windows Server 2016 to 2025 instances: HTTP-to-HTTPS loops behind a load balancer, www and non-www rules fighting each other, trailing-slash rules duplicating, and the interaction between URL Rewrite and ASP.NET Cores HTTPS redirection middleware. Each has a concrete diagnostic command and a fix.
Confirm the loop is at the HTTP layer, not the app
Start with the log, not the browser. The current u_exYYMMDD.log in %SystemDrive%\inetpub\logs\LogFiles\W3SVC1 records status, cs-uri-stem and cs(Host) for every hop:
Select-String -Path "C:\inetpub\logs\LogFiles\W3SVC1\u_ex*.log" -Pattern " 301 " | Select-Object -Last 40. If the same URI returns 301 with the same host repeatedly, the loop is a rewrite rule. If the URI alternates between two paths, it is a canonicalisation rule oscillating — most often one hop stripping a trailing slash and another adding it back.
If the log shows a single request and the loop happens inside the application, the cause is code: an ASP.NET Core app calling UseHttpsRedirection() while TLS terminates at a reverse proxy that speaks HTTP to Kestrel. Every request looks like plain HTTP to the app, which redirects to HTTPS, which the proxy forwards again as HTTP. There is nothing in the IIS log to see because IIS is not issuing the redirect.
Loop one: HTTPS redirect behind a load balancer
This is the most common cause where a cloud load balancer or front-end Nginx terminates TLS. IIS receives HTTP on port 80, a rule matching {HTTPS} equal to OFF redirects to https, and the balancer decrypts it and forwards HTTP again.
The fix is to trust the forwarded header. The URL Rewrite condition should check HTTP_X_FORWARDED_PROTO rather than {HTTPS}: add that server variable to the allowed list in applicationHost.config and match on it. In ASP.NET Core, set ForwardedHeaders.XForwardedProto in ForwardedHeadersOptions.
- Check what the balancer actually sends:
curl -sI https://your-domain.tld/ | grep -i forwarded. - If X-Forwarded-Proto is absent, fix it at the proxy before touching IIS.
- Never trust the header blindly: restrict the trusted proxy IPs in IIS with
<ipSecurity>or in the middleware known proxies list. - For Cloudflare, use the
CF-Visitorheader or the origin certificate plus Full (Strict) mode, not Flexible.
Loop two: www and non-www rules that disagree
Two rules, one adding www and one removing it, is more common than it should be. Query the merged configuration: %windir%\system32\inetsrv\appcmd.exe list config "Default Web Site" /section:system.webServer/rewrite/rules /xml, then read every rule whose redirect action matches on hostname.
Keep exactly one canonical-host rule per site, and order it first. A working pattern for non-www canonical, which is what this site uses, matches HTTP_HOST beginning with www\. and redirects to https://{C:1}{REQUEST_URI} with a status of 301, where the capture group is the host without www. Disable or delete the opposite rule rather than leaving it commented out in a different section of the file.
Loop three: trailing slash and trailing dot rules
A rule appending a trailing slash combined with one removing it produces a two-hop oscillation; the log shows the URI alternating between /contact and /contact/. The same happens with a trailing dot in the hostname, because example.com. and example.com are different values to a regex.
Test the three variants directly and watch the status codes without a browser cache in the way:
curl -sI http://your-domain.tld/contact, then curl -sI https://your-domain.tld/contact, then curl -sI https://your-domain.tld/contact/. Any request returning 301 to a URL you just requested is the offending pair. Use the -L flag with --max-redirs 5 to see the chain explicitly and stop before curl gives up.
URL Rewrite inheritance and the duplicate rule trap
Rules defined at the server level in applicationHost.config merge with rules in each sites web.config. If both define a redirect with the same name, IIS throws HTTP Error 500.19 — but if the names differ, you get two redirects executing in sequence and a loop. Check the effective configuration rather than the file you edited: appcmd list config -section:system.webServer/rewrite/rules at server level, then the same command scoped to the site.
Failed Request Tracing is the cleanest diagnostic. Enable it with appcmd set config "Default Web Site" /section:system.webServer/tracing /traceFailedRequestsLogging.enabled:true /traceFailedRequestsLogging.directory:"C:\inetpub\logs\FailedReqLogs", then add a rule for 301 and 302. The XML log names the rule that fired and the substitution applied.
Redirects issued by the application, not IIS
If Failed Request Tracing shows a single hop and the browser still loops, application code is redirecting. In ASP.NET Core the culprits are UseHttpsRedirection(), UseHsts() with a port mismatch, or AddRedirectToHttpsPermanent. In classic ASP.NET it is RequireHttpsAttribute as a global filter.
The fix is the same in all three: make the application proxy-aware. Forwarded headers first, then HTTPS redirection, then routing. Drop HSTS outside production entirely, because a stale max-age header keeps a browser redirecting for months after the server config is fixed.
A short checklist that resolves most cases
Work through these in order and you will find the loop within fifteen minutes on almost any instance:
- Read the IIS log for repeating 301/302 on the same URI — confirms HTTP-layer loop.
- List every redirect rule at server and site scope with
appcmd. - Test with
curl -Iagainst http, https, www and non-www. - Check
X-Forwarded-Protoif a proxy or load balancer sits in front. - Enable Failed Request Tracing for 301 and read the rule name.
- Clear the browser HSTS state in development via
chrome://net-internals/#hsts.
When you test, use curl from a clean client: browsers cache permanent redirects aggressively by design, so the tool you test with can hide the fix. Our guide to HTTPS bindings and certificate setup on IIS covers the correct binding and SNI configuration, which removes the most common reasons a redirect rule misfires in the first place.
If the loop appeared right after a certificate renewal or an SNI binding change rather than a rule edit, check the binding state first: a binding that no longer matches the expected hostname sends every request to the default site, which redirects to the hostname the client already used. Our reference on Windows Server web hosting with IIS covers verifying binding order from the command line.



