ASP.NET Core Hosting Bundle on IIS: Install, Verify, and Fix the 500.19 / 500.30 Duo

The ASP.NET Core Hosting Bundle installs three things: the .NET runtime, the ASP.NET Core shared framework, and the AspNetCoreModuleV2 IIS module that reverse-proxies requests to the out-of-process Kestrel server. Most “it works locally but 500s on IIS” failures trace to one of those three being absent or mismatched.

Install Order and Verification

Install the bundle after IIS, or the module registration step has no IIS to register with. If you install IIS later, repair the bundle. Then verify all three components independently rather than assuming:

# 1. runtimes present
dotnet --list-runtimes

# 2. module registered globally
Get-WebGlobalModule | Where-Object { $_.Name -like 'AspNetCoreModule*' } |
  Select-Object Name, Image

# 3. handler present in the site's config
Get-WebConfiguration -PSPath "IIS:\Sites\MyApp" -Filter "system.webServer/handlers/*" |
  Where-Object { $_.Name -like 'aspNetCore' }

dotnet --list-runtimes must show a Microsoft.AspNetCore.App entry matching your target framework. A machine with only Microsoft.NETCore.App will fail every Core web app. If the module is missing, restart the Windows Process Activation Service — not just W3SVC:

net stop was /y
net start w3svc

Application Pool and web.config

Three settings in the app pool and one in web.config account for the bulk of configuration errors.

SettingRequired valueConsequence if wrong
App pool .NET CLR versionNo Managed CodeCLR load failures, module never loads
Pipeline modeIntegratedHandler mapping failures
IdentityApplicationPoolIdentity (default) or a low-privilege service accountFile/registry access errors, or excessive rights
32-bit applicationsfalse unless you have 32-bit native depsMixed-bitness load failures
web.config hostingModelinprocess for performance, outofprocess for isolation500.30 with inprocess/outofprocess mismatch
Import-Module WebAdministration
Set-ItemProperty "IIS:\AppPools\MyApp" managedRuntimeVersion ""
Set-ItemProperty "IIS:\AppPools\MyApp" managedPipelineMode Integrated
Set-ItemProperty "IIS:\AppPools\MyApp" enable32BitAppOnWin64 $false

Reading 500.19 vs 500.30 Correctly

These are unrelated faults that get conflated constantly.

  • 500.19 — Configuration Error. IIS could not parse web.config. The response names the offending section. Root causes: a <system.webServer> section locked at server level, a duplicate section handler, or a 32/64-bit module mismatch. The fix is almost always a missing appcmd unlock config or a duplicate module registration, not anything in your application.
  • 500.30 — In-Process Start Failure. The module loaded but the app failed to start. This is your application, not IIS. Read the actual exception from the Windows Application log or by running the app standalone against the same DLL — that is the fastest path to the real error.
# surface the real .NET exception rather than the generic 500 page
Get-WinEvent -LogName Application -MaxEvents 20 |
  Where-Object ProviderName -like '*IIS AspNetCore Module*' |
  Select-Object TimeCreated, Message | Format-List

Enable stdout logging in web.config for an app that starts fine standalone but dies under IIS — it captures the startup exception to a file you can read without a debugger:

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

Create the logs folder yourself — the module will not create it, and a missing folder silently produces no log. Turn stdout logging off once resolved; it prevents log rotation and grows without bound.

Deployment and Permissions

The app pool identity needs read and execute on the deployment folder and write access only where the application genuinely writes. Applying this correctly is the difference between a working deployment and a stream of 500.30s on first request:

$acl = "IIS AppPool\MyApp"
icacls "C:\inetpub\MyApp" /grant "${acl}:(OI)(CI)(RX)" /T /C
icacls "C:\inetpub\MyApp\logs" /grant "${acl}:(OI)(CI)(M)" /T /C
icacls "C:\inetpub\MyApp\App_Data" /grant "${acl}:(OI)(CI)(M)" /T /C

Publish with Web Deploy or dotnet publish, then confirm the deployed target framework matches the installed runtime — a version skew between the build SDK and the server runtime produces startup failures that look like configuration problems. The deployment pipeline mechanics are covered in our ASP.NET Core on IIS hosting guide.

Checklist Before You Go Live

  1. dotnet --list-runtimes shows the matching Microsoft.AspNetCore.App version.
  2. AspNetCoreModuleV2 appears in Get-WebGlobalModule.
  3. App pool set to No Managed Code, Integrated pipeline, 32-bit disabled.
  4. App pool identity has RX on the site root, M on write paths only.
  5. stdoutLogEnabled is false in production.
  6. HTTPS binding and HSTS configured — steps in our IIS HTTPS bindings and certificates walkthrough.
  7. Application Pool recycling configured so a recycle does not drop in-flight requests.

Hosting It

A single low-traffic ASP.NET Core site is comfortable on 2 vCPU / 4 GB with NVMe storage. InterServer Windows VPS supplies licensed Windows Server with full IIS control and no restricted handlers, and code TRYINTERSERVER brings the first month to $0.01. If your build pipeline needs hourly provisioning or you want the option of a licensed Windows image on fast NVMe, Vultr and Database Mart are worth comparing.

Tuning the Reverse Proxy to Kestrel

The module forwards requests to Kestrel over a local loopback port. Two defaults are worth changing on a busy site: the request body limit, because IIS caps uploads at 30 MB regardless of your application’s own limits, and the shutdown timeout, because a graceful recycle needs long enough for in-flight requests to drain.

# raise the IIS-level upload cap to 100 MB
Set-WebConfigurationProperty -PSPath "IIS:\Sites\MyApp" `
  -Filter "system.webServer/security/requestFiltering/requestLimits" `
  -Name maxAllowedContentLength -Value 104857600

# confirm the app pool shutdown timeout gives in-flight requests time to finish
Get-ItemProperty "IIS:\AppPools\MyApp" |
  Select-Object shutdownTimeLimit, startMode

A mismatch between the IIS limit and your Kestrel MaxRequestBodySize produces confusing 413 responses — the request is rejected before your middleware ever sees it. Set the IIS limit slightly higher than the application limit and let the application produce the meaningful error. Recycling behaviour and overlap settings are covered in our IIS application pool recycling guide, which matters more for in-process hosting where a recycle restarts your managed code.

Leave a Comment