How to Troubleshoot Developer Web Apps on IIS

Microsoft Fix Intermediate 14 min read Official Docs Grounded Updated April 20, 2026

Why This Is Happening

You pushed your web app to IIS, hit the URL in your browser, and got slapped with a blank white page, a cryptic HTTP 500 error, or , my personal favorite , the ever-unhelpful "An error occurred on the server" message that tells you absolutely nothing. I've seen this on hundreds of machines across dev teams, staging environments, and production servers. The frustrating truth is that IIS is actually doing its job, it's just doing it silently, hiding the real problem behind a generic error page.

The root causes break down into a surprisingly short list, even though the symptoms feel endless:

  • web.config misconfiguration, A single malformed XML tag or a missing module reference causes HTTP Error 500.19 (error code 0x80070021 or 0x8007000d). IIS can't even load your app at this point.
  • Application pool identity problems, Your app pool is running as ApplicationPoolIdentity or NETWORK SERVICE, but the folder permissions haven't been updated. IIS hits an access denied wall and returns 500.0 or 503.
  • Missing ASP.NET or .NET Core runtime, You deployed a .NET 8 app to a server that only has .NET Framework 4.8. The handler mapping exists, but there's nothing to handle the request. Classic 500.21 or 500.30 errors.
  • Handler mapping conflicts, Two modules fighting over the same request path. You see this most often when PHP and ASP.NET are both installed, or when the .NET Core Hosting Bundle wasn't installed in the right order.
  • URL Rewrite rule errors, A badly written rewrite rule in web.config can cause redirect loops or 404s that look like the file simply doesn't exist.
  • File and folder permission gaps, IIS can read your DLLs but can't write to a temp directory or log folder your app needs at runtime.

Here's why Microsoft's error messages don't help: by default, IIS shows "custom errors" to remote clients and only shows detailed errors to the local machine. If you're testing from another PC, you're seeing the sanitized version. And even on localhost, the default error pages strip out the stack trace. You're basically debugging with your eyes closed.

Who sees this most? Developers moving from IIS Express (Visual Studio's built-in server) to full IIS. The two are more different than Microsoft's marketing suggests. IIS Express runs as you, with your permissions, in your user context. Full IIS runs as a service with its own restricted identity. That gap is where 80% of "it works on my machine" problems live.

Browse all Microsoft fix guides →

The Quick Fix, Try This First

Before going through every step in this guide, do this one thing. It unlocks the real error message and will tell you which section to jump to.

Open your site's web.config file. Inside the <system.web> section, add or update the customErrors element:

<system.web>
  <customErrors mode="Off" />
</system.web>

And inside <system.webServer>, add this to get IIS's own detailed error pages instead of the generic ones:

<system.webServer>
  <httpErrors errorMode="Detailed" />
</system.webServer>

Save the file. Recycle your application pool in IIS Manager (right-click your app pool → Recycle), then refresh the page. You'll now see the actual error, a stack trace, a specific error code like HTTP Error 500.30 - ANCM In-Process Start Failure, or a line number pointing to the broken web.config entry.

Write down that exact error code. If it's 500.19, jump to Step 2. If it's 500.0 or 503, jump to Step 3. If it mentions ANCM or In-Process, you're dealing with a .NET Core hosting issue, Step 4 covers that.

One important note: turn custom errors back on (mode="RemoteOnly") before you go to production. Detailed error pages expose internal paths and stack traces to anyone who visits your site. On a dev box it's fine, but never leave errorMode="Detailed" in a public-facing deployment.

Pro Tip
If you can't modify web.config because the 500.19 error is preventing IIS from loading it at all, navigate directly to the IIS Manager, select your site, double-click Error Pages, then click Edit Feature Settings in the right panel and switch to Detailed errors. This bypasses web.config and forces IIS to show you the raw error, including the exact line in web.config that's broken.
1
Enable Failed Request Tracing to Catch Silent Errors

Some IIS errors never make it to your browser at all, the request fails internally and IIS just returns a blank response or a misleading generic page. Failed Request Tracing (also called FREB, Failed Request Event Buffering) captures everything: every module that touched the request, every status code, every timing measurement. It's the IIS equivalent of attaching a debugger.

First, make sure the FREB feature is installed. Open PowerShell as Administrator and run:

Enable-WindowsOptionalFeature -Online -FeatureName `
  IIS-HttpTracing -All

Or, through the GUI: Control PanelProgramsTurn Windows features on or offInternet Information ServicesWorld Wide Web ServicesHealth and Diagnostics → check Tracing.

Now configure it in IIS Manager. Select your site in the left tree, then in the right Actions panel click Failed Request Tracing.... Check Enable, set the directory to somewhere you can easily access (like C:\inetpub\logs\FailedReqLogFiles), and set Max number of trace files to 50.

Then double-click Failed Request Tracing Rules from the site's Features view. Click Add... in the right panel. Select All content (*), set the status codes to 400-999, and set Verbosity to Verbose. Click OK.

Now reproduce your error. Navigate to the FREB log folder and open the newest fr######.xml file in Internet Explorer or Edge (the XSL stylesheet renders it properly in those browsers, it won't look right in Chrome). You'll see a color-coded waterfall of every IIS event. Red entries are where the failure occurred. The MODULE_SET_RESPONSE_ERROR_STATUS event will tell you exactly which module triggered the error and why.

If you see the failure in ManagedPipelineHandler, your ASP.NET app itself is throwing the error. If it's in StaticFileModule or ProtocolSupportModule, the problem is in IIS's own handling before your app even runs.

2
Fix web.config Errors (HTTP Error 500.19)

HTTP Error 500.19 with sub-status codes like 0x8007000d or 0x80070021 means IIS found your web.config but can't parse or apply it. The error page will show you the exact line number, that's your starting point.

The most common cause: you referenced an IIS module or handler that isn't installed on this machine. For example, if your web.config has:

<add name="RewriteModule" ... />

...but the URL Rewrite 2.1 module isn't installed, IIS refuses to load the entire config. You need to either install the missing module or remove the reference.

To check which modules are currently installed, run this in PowerShell:

Get-WebConfiguration system.webServer/globalModules | 
  Select-Object name

For the URL Rewrite module specifically, download it from the IIS official site and install it. Restart IIS after:

iisreset /restart

Another common 500.19 trigger: incorrect XML in web.config. Run this quick validation in PowerShell to catch malformed XML before IIS does:

[xml](Get-Content "C:\inetpub\wwwroot\YourApp\web.config")

If PowerShell throws a terminating error, it's bad XML. The error message will include the line and character position. Common culprits are unescaped ampersands in connection strings (& must be written as &amp; in XML), unclosed tags, and copy-pasted smart quotes instead of straight quotes.

Sub-status 0x80070021 specifically means "This configuration section cannot be used at this path." That happens when a section is locked at the server level. Fix it by unlocking it in IIS Manager: navigate to the server root (not your site) → Configuration Editor → find the locked section → click Unlock Section in the right panel. Alternatively, at the server level in applicationHost.config, change overrideModeDefault="Deny" to overrideModeDefault="Allow" for the relevant section.

When the fix works, IIS will load your site normally. If you still see a 500.19, compare your web.config against a known-good minimal config and add sections back one at a time to isolate the problem element.

3
Resolve Application Pool Identity and Permission Errors

This is the single most common category of IIS problem for developers moving from IIS Express to full IIS. Your app pool runs as a built-in account, either ApplicationPoolIdentity (the default, and the right choice), NETWORK SERVICE, or LOCAL SERVICE. When the folders your app needs to read, write, or execute aren't accessible to that account, you get 500.0, 503.7, or, the most confusing one, a silent blank page.

First, confirm which identity your app pool is using. In IIS Manager, click Application Pools, select your pool, then Advanced Settings in the right panel. Look at Identity under Process Model.

If it's ApplicationPoolIdentity, the actual Windows account name is IIS AppPool\YourAppPoolName. To grant your app folder access, open a Command Prompt as Administrator and run:

icacls "C:\inetpub\wwwroot\YourApp" /grant "IIS AppPool\YourAppPoolName":(OI)(CI)RX /T

That grants Read & Execute recursively. If your app writes files (logs, uploads, temp files), grant Modify on those specific subdirectories:

icacls "C:\inetpub\wwwroot\YourApp\logs" /grant "IIS AppPool\YourAppPoolName":(OI)(CI)M /T

Don't just give IIS_IUSRS full control on everything, that's the lazy fix and it creates a real security gap. Be specific about which folders need write access.

Also check the Load User Profile setting in your app pool's Advanced Settings. Some apps require the user profile to be loaded (it provides access to the TEMP and TMP environment variables). Set it to True if your app is crashing on startup trying to access a temp path.

To verify the fix worked: open Event Viewer (eventvwr.msc), navigate to Windows Logs → Security, and look for Event ID 4656 or 5145 with "Access Denied" in the description. If those are gone after your permission change, you've solved it.

4
Fix .NET Core and ASP.NET Runtime Errors (500.30, 500.31, 500.32)

If your app is built on .NET Core or .NET 5+, IIS doesn't run it natively, it uses the ASP.NET Core Module (ANCM) as a proxy. When ANCM can't start your app process, you get a 500.30 (ANCM In-Process Start Failure), 500.31 (ANCM Failed to Find Native Dependencies), or 500.32 (ANCM Failed to Load dll). These are among the most opaque IIS errors because the real error is happening in your app's startup code, not in IIS itself.

First, check whether the correct .NET runtime is installed on the server:

dotnet --list-runtimes

You need to see a runtime version that matches or exceeds what your app targets. If it's missing, download and install the correct .NET Hosting Bundle (not just the Runtime, the Hosting Bundle includes the ANCM module). After installation:

iisreset /restart

If the runtime is present but you're still getting 500.30, the failure is in your app's startup code. The fastest way to see the real error is to temporarily enable stdout logging. In your web.config, find the aspNetCore element and set:

<aspNetCore processPath="dotnet" 
            arguments=".\YourApp.dll"
            stdoutLogEnabled="true" 
            stdoutLogFile=".\logs\stdout"
            hostingModel="inprocess">

Create the logs folder manually and grant the app pool identity write access on it. Then reproduce the error and check the stdout_xxxxxxxx.log file, it will contain the full exception from your app's Program.cs startup, including missing environment variables, failed database connections, or misconfigured services.

One specific gotcha: if you're using In-Process hosting (hostingModel="inprocess"), your app pool must be set to No Managed Code under the Managed Pipeline Mode. If it's set to an actual .NET CLR version, ANCM will conflict with it. Fix it in IIS Manager → Application Pools → your pool → Basic Settings → set .NET CLR Version to No Managed Code.

When everything's correct, your app will start and the 500.30 disappears. Remember to set stdoutLogEnabled="false" again, leaving it on fills your disk with log files surprisingly fast.

5
Debug Handler Mapping and Module Errors (404, 500.21, HTTP 500.0)

When IIS doesn't know what to do with a request, because no handler is mapped to that file extension or path, it returns a 404 even if the file physically exists. Or it returns 500.21 ("Handler 'PageHandlerFactory-Integrated' has a bad module 'ManagedPipelineHandler'"), which sounds alarming but usually means ASP.NET isn't registered properly with IIS.

To fix the classic ASP.NET registration issue, run this from an elevated Command Prompt. Find your ASP.NET version first:

dir /b C:\Windows\Microsoft.NET\Framework64\

Then register it with IIS:

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\aspnet_regiis.exe -iru

Follow it with:

iisreset /restart

For handler mapping issues more broadly, go to IIS Manager, select your site, and double-click Handler Mappings. Check that there's an entry that covers your file type. For an ASP.NET MVC or Web API app with extensionless URLs, you need the ExtensionlessUrlHandler-Integrated-4.0 entry to be present and mapped to *. (asterisk dot). If it's missing, you can add it back via PowerShell:

Add-WebConfiguration -Filter "system.webServer/handlers" `
  -PSPath "IIS:\Sites\YourSiteName" `
  -Value @{
    name="ExtensionlessUrlHandler-Integrated-4.0"
    path="*."
    verb="GET,HEAD,POST,DEBUG"
    type="System.Web.Handlers.TransferRequestHandler"
    resourceType="Unspecified"
    requireAccess="Script"
    preCondition="integratedMode,runtimeVersionv4.0"
  }

For PHP apps, handler conflicts are common when both PHP and ASP.NET handlers are active. Open Handler Mappings, sort by Path, and look for duplicate entries matching *.php. Remove any that point to the wrong FastCGI executable. Check that your PHP handler points to the correct php-cgi.exe path for your installed PHP version.

After adjusting handler mappings, always do an iisreset /restart and test with a simple static file first to confirm IIS itself is responding, then test your dynamic content.

Advanced Troubleshooting

Reading Event Viewer for IIS Errors

Event Viewer is where IIS logs the things it won't show you in the browser. Open it with eventvwr.msc. The two places to look are Windows Logs → Application and Applications and Services Logs → Microsoft → Windows → IIS-Configuration → Administrative.

In the Application log, filter by Source = ASP.NET, W3SVC-WP, or Application Error. Event ID 1309 is an unhandled ASP.NET exception with a full stack trace. Event ID 5011 means IIS couldn't start the worker process, usually a permissions or port conflict. Event ID 2268 from source WAS (Windows Activation Service) means the app pool failed to start, often because the identity account's password expired (yes, this happens on service accounts set with fixed passwords).

Group Policy and Locked-Down Developer Environments

On domain-joined developer machines, Group Policy can silently override IIS settings. A common one: GPO disabling the ability to run scripts, which causes your ASP.NET handlers to fail even though the handler mapping is correct. To check what policies are applied, run:

gpresult /H C:\gpreport.html

Open the resulting HTML file and search for "IIS" and "scripts." If you find restrictive policies, talk to your AD admin, trying to work around GPO locally is a losing battle and can create security issues.

Registry: Forcing IIS to Respect Custom Application Pool Settings

In some edge cases, particularly after failed IIS updates or corrupted installations, the applicationHost.config gets out of sync with the registry entries that IIS uses at startup. The config file lives at:

C:\Windows\System32\inetsrv\config\applicationHost.config

Open it in a text editor run as Administrator. Search for your site name and verify the physicalPath, bindingInformation, and applicationPool attributes are correct. A corrupted or truncated applicationHost.config causes IIS to start in a degraded state where some sites simply don't load.

If applicationHost.config is actually corrupted, IIS keeps automatic backups. Recover from one:

cd C:\Windows\System32\inetsrv\config\backup
dir
copy [most-recent-backup-folder]\applicationHost.config `
     C:\Windows\System32\inetsrv\config\applicationHost.config

Network-Level and Binding Issues

If your app works on localhost but not from other machines on the network, it's almost always a firewall rule or a binding issue. Check that port 80 (or 443) is open in Windows Defender Firewall:

netsh advfirewall firewall show rule name="World Wide Web Services (HTTP Traffic-In)"

Also check whether another process has grabbed port 80 before IIS could bind to it:

netstat -ano | findstr :80

Cross-reference the PID with Task Manager. Skype used to notoriously grab port 80 on developer machines. Teams and various VPN clients do this too. If IIS isn't winning the port binding race at startup, the fix is to change the conflicting app's port or configure IIS to start earlier via the service start order in services.msc.

When to Call Microsoft Support
If you've gone through every step here and you're still seeing failures, particularly if Event Viewer shows Event ID 5059 (WAS cannot start worker process) repeatedly, if your applicationHost.config is regenerating incorrect entries after every iisreset, or if IIS stops responding entirely and won't restart, it's time to escalate. These symptoms usually indicate a deeper installation corruption or a bug that requires a hotfix. Contact Microsoft Support with your Event Viewer exports, the FREB trace files, and the output of appcmd list site /processModel.userName:*. The more data you bring, the faster they can diagnose it.

Prevention & Best Practices

Most IIS developer headaches are repeatable, the same mistakes show up on the same types of projects, made by the same well-intentioned engineers who just didn't know how IIS worked under the hood. Here's how to avoid coming back to this guide every time you deploy.

Use Web Deploy (MSDeploy) instead of manual file copies. Manual xcopy deployments frequently leave behind stale assemblies or don't update web.config correctly. Web Deploy handles file locking, app pool recycling, and permission propagation automatically. Set it up once and your deployments become repeatable:

msdeploy -verb:sync -source:package='C:\build\MyApp.zip' `
  -dest:auto,computerName='localhost',`
  authType='NTLM' -setParam:name='IIS Web Application Name',`
  value='Default Web Site/MyApp'

Never develop directly against your live IIS installation. Use IIS Express via Visual Studio for daily development. Reserve full IIS for staging and production. The mental model mismatch between the two causes most of the permission errors this guide covers.

Version-control your applicationHost.config changes. The applicationHost.config is not version-controlled by default, which means IIS configuration drift silently accumulates across environments. Use IIS's built-in export or PowerShell's WebAdministration module to export site configs as scripts and commit those to your repo.

Set up health monitoring from day one. IIS has a built-in health monitoring system under <healthMonitoring> in web.config. Configure it to log to the Application event log, at minimum, log Application Lifetime Events and All Errors. This gives you a baseline audit trail that makes post-incident debugging much faster.

Quick Wins
  • Set your app pool to recycle daily at a fixed off-peak time (3 AM) rather than on memory limits, memory-triggered recycles happen at random production moments and are hard to correlate with logs
  • Always create a dedicated app pool per site, sharing app pools means one app's crash takes down all apps in that pool
  • Enable HTTP/2 on your IIS 10+ bindings: netsh http add sslcert ... enablehttp2=yes, it's off by default on many IIS installs and provides a noticeable performance gain
  • Schedule regular exports of your IIS configuration: %windir%\system32\inetsrv\appcmd list config > C:\backup\iis-config.xml, run this via Task Scheduler weekly so you always have a clean restore point

Frequently Asked Questions

Why does my ASP.NET app work in Visual Studio but give a 500 error in IIS?

This almost always comes down to the difference between IIS Express (which runs as you, with your full user permissions and environment variables) and full IIS (which runs as a restricted service account). Check three things in order: first, that the app pool identity has read access to your application folder; second, that any environment variables your app reads at startup (connection strings, API keys) are set at the machine or IIS level, not just your user environment; third, that the app pool's .NET version matches what your project targets. With those three aligned, the behavior will match.

IIS keeps returning 403.14 Forbidden, Directory Listing Denied. How do I fix it?

A 403.14 means IIS received a request for a directory (not a file), couldn't find a default document in that directory, and doesn't have Directory Browsing enabled. The correct fix depends on your app type. For an ASP.NET MVC or Web API app, this usually means your URL routing isn't set up, add a <modules runAllManagedModulesForAllRequests="true" /> entry in web.config to ensure the routing module intercepts all requests. For a static site, either add an index.html as the default document (IIS Manager → your site → Default DocumentAdd), or enable Directory Browsing if you intentionally want folder listings. Never enable Directory Browsing on a production app, it exposes your file structure.

My app pool keeps stopping by itself. What's causing it?

An app pool that stops automatically almost always means the worker process crashed hard enough that IIS hit its Rapid Fail Protection threshold. By default, IIS stops an app pool if it fails 5 times within 5 minutes. Open Event Viewer and look in Windows Logs → Application for Event ID 5002 or 1000 (Application Error), these will show you the crash reason and faulting module. Common culprits are unhandled exceptions in app startup code, OutOfMemoryException under load, or a native DLL dependency (like a 32-bit COM component in a 64-bit app pool) throwing an access violation. Temporarily set the app pool's Rapid Fail Protection to False in Advanced Settings to prevent it from shutting down while you diagnose, then re-enable it once you've fixed the root cause.

How do I run a .NET Core web app on IIS, what's the minimal setup needed?

Three things are required and all three must be present. First, install the correct .NET Hosting Bundle for your target framework (e.g., the .NET 8 Hosting Bundle for a .NET 8 app), this installs both the runtime and the ASP.NET Core Module for IIS. Second, your app pool must have its .NET CLR Version set to No Managed Code, this is counterintuitive but correct, because .NET Core manages its own CLR. Third, your web.config must contain the <aspNetCore> element pointing to your app's DLL with processPath="dotnet". When you publish with dotnet publish, the correct web.config is generated automatically, if you're deploying manually and web.config is missing or wrong, that's the source of your problem.

IIS is returning HTTP 503 Service Unavailable. What does that mean?

A 503 from IIS means the request arrived at IIS but couldn't be forwarded to a worker process, the application pool isn't running. Open IIS Manager, click Application Pools, and look at the Status column. If your pool shows Stopped, right-click and start it, then immediately check Event Viewer for why it stopped. The most common causes: the app pool identity's password expired (if you're using a custom service account), the worker process crashed and Rapid Fail Protection kicked in, or a port conflict prevented the process from binding. A 503 is never random, IIS always has a reason, and Event Viewer will have it documented within a few seconds of the failure.

Can I troubleshoot IIS without restarting it, I have other live sites on the same server?

Yes, and you should avoid full iisreset commands on shared servers whenever possible, it drops all active connections across every site. Instead, recycle only the specific app pool for your site: in IIS Manager, right-click your app pool and select Recycle, or from PowerShell: Restart-WebAppPool -Name "YourAppPoolName". To restart just your site without touching others: Stop-WebSite -Name "YourSiteName"; Start-WebSite -Name "YourSiteName". For web.config changes specifically, IIS detects and applies them automatically without any restart, the app pool recycles itself when it detects the file change, and that's scoped to just your application.

Related Microsoft Fix Guides

H
Sai Kiran Pandrala
Our team includes certified Microsoft engineers, Azure architects, and system administrators with 10+ years of enterprise IT experience. Every guide is written from hands-on troubleshooting, not guesswork. We test every fix before publishing.