Deployment and Diagnostics

This section documents ASP.NET Web Forms on .NET Framework 4.8.1, the last and permanent version of Web Forms — the page life cycle and postback model, ViewState and control state, server controls, validation controls, master pages and themes, data-bound controls, and the provider-based security model — as described by the official documentation at Microsoft Learn and the ASP.NET previous-versions archive, which are the reference these pages are written and verified against.

Web Forms receives security fixes only and has no forward path onto modern .NET (.NET Framework 4.8.1 is Microsoft’s last version of .NET Framework; Web Forms itself never shipped on .NET Core/.NET 5+). It remains supported for existing applications running on Windows but is not recommended for new development — see Choosing an ASP.NET Framework and Migrating to Modern ASP.NET for what that means in practice.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Running a Web Forms application in production is inseparable from IIS — this page covers the deployment and operational-diagnostics concerns specific to that combination, on top of the ordinary "watch the logs" practices that apply to any web application.

IIS application pools

Each IIS application pool is an isolated worker process (w3wp.exe) hosting one or more applications, with its own recycling schedule, identity, and CLR settings:

Setting Why it matters for Web Forms

Integrated pipeline

The default and effectively only mode used today — see HTTP Pipeline, Handlers, and Configuration for classic-vs-integrated behavior.

32-bit vs. 64-bit

"Enable 32-Bit Applications" must match what any native-dependency assemblies were built for; a mismatch throws a BadImageFormatException at load time, not at compile time.

.NET CLR version

.NET Framework 4.x (Web Forms always uses "v4.0" here regardless of the specific 4.x version targeted — 4.0 through 4.8.1 share one CLR) vs. the legacy "v2.0" pool setting, relevant only for ancient ASP.NET 1.1/2.0-3.5 applications still needing the old CLR.

Recycling

Scheduled/idle-timeout/memory-threshold recycling drops all in-process state —  InProc session, Application, in-memory Cache — which is exactly why out-of-process session state matters for any application that cannot tolerate that.

Web Deploy (MSDeploy) and Visual Studio publishing profiles

Web Deploy (msdeploy.exe) synchronizes a built application, IIS site/app-pool settings, and optionally databases, from a build machine (or CI agent) to a target server, either directly over the Web Management Service or by producing a portable package:

msdeploy.exe -verb:sync -source:contentPath="C:\src\MyApp\obj\Release\Package\PackageTmp" ^
    -dest:contentPath="MyApp",computerName="https://webserver:8172/msdeploy.axd",userName=deploy,password=%DEPLOY_PW%,authType=basic

A publish profile (.pubxml, created and edited through Visual Studio’s Publish dialog, or hand-edited) captures a named deployment target’s settings — destination, credentials reference, whether to precompile, which web.config transform to apply — so a repeatable msbuild /p:DeployOnBuild=true /p:PublishProfile=Production can run from CI without Visual Studio installed.

Precompiled vs. updatable deployment

As introduced in HTTP Pipeline, Handlers, and Configuration, aspnet_compiler (invoked automatically by a Web Deploy publish, or run standalone) produces either an updatable precompiled site (markup .aspx/.ascx files still deployed, tweakable in place without recompiling; code-behind is precompiled into assemblies) or a fully non-updatable precompiled site (everything, including markup, compiled to assemblies — nothing readable/editable remains on disk). Fully precompiled is the safer default for anything beyond a small internal tool: it removes first-request compilation latency, avoids leaking source-adjacent markup on the server, and prevents an accidental production hotfix made by editing a file directly on the server (a workflow the Web Site Project model, see Getting Started, made easy but which bypasses source control entirely).

<customErrors> and error pages

<system.web>
  <customErrors mode="RemoteOnly" defaultRedirect="~/Errors/Generic.aspx">
    <error statusCode="404" redirect="~/Errors/NotFound.aspx" />
    <error statusCode="500" redirect="~/Errors/ServerError.aspx" />
  </customErrors>
</system.web>

mode="RemoteOnly" (the default) shows the detailed ASP.NET error/stack-trace page to requests from the local machine only, and the friendly customErrors page to everyone else — mode="On" forces friendly pages everywhere (including locally, useful when testing the error pages themselves), mode="Off" disables the feature entirely and should never be set in production, since it leaks stack traces (and potentially connection strings, in an unhandled exception message) to end users.

Page and application Trace

Page.Trace/<trace> in web.config gives per-request diagnostic output — request/response details, control-tree size and ViewState contribution per control (relevant to measuring ViewState weight), session/application state dumps, and custom timed messages — either inline at the bottom of the page or collected centrally:

<system.web>
  <trace enabled="true" requestLimit="40" pageOutput="false" localOnly="true" />
</system.web>
protected void Page_Load(object sender, EventArgs e)
{
    Trace.Write("OrderPage", "Loading order " + Request.QueryString["id"]);
}

With pageOutput="false", trace output is collected application-wide and viewed at ~/Trace.axd instead of appended to every page — the practical choice for anything beyond debugging a single page locally, since it does not alter the page’s own rendered output.

Health monitoring

<healthMonitoring> is ASP.NET’s built-in event-based monitoring system — application lifecycle events, unhandled exceptions, Membership/Forms-authentication failures, and custom WebBaseEvent-derived events — are routed to one or more configured providers (SQL Server, email, the Windows event log, or a custom provider):

<system.web>
  <healthMonitoring enabled="true">
    <providers>
      <add name="SqlWebEventProvider" type="System.Web.Management.SqlWebEventProvider"
           connectionStringName="HealthMonitoring" buffer="false" />
    </providers>
    <rules>
      <add name="All Errors Default" eventName="All Errors" provider="SqlWebEventProvider" />
    </rules>
  </healthMonitoring>
</system.web>

This predates, and covers roughly the same ground as, structured application logging in later frameworks — most current Web Forms applications either lean on this built-in system for baseline coverage or replace it outright with a conventional logging library (log4net, NLog, Serilog) called directly from Application_Error and other code.

ELMAH

ELMAH (Error Logging Modules and Handlers) is the de facto standard third-party error-logging package for System.Web applications — a drop-in HttpModule (NuGet: elmah) that captures every unhandled exception automatically, with zero code changes, and exposes a browsable log (~/elmah.axd) plus pluggable storage (SQL Server, SQLite, Azure Table Storage, XML files, …​):

<system.webServer>
  <modules>
    <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
  </modules>
</system.webServer>

ELMAH and <healthMonitoring> are not mutually exclusive but overlap significantly in the "log unhandled exceptions somewhere reviewable" use case — most teams pick one rather than running both.

Performance counters

ASP.NET registers a set of Windows Performance Counters under the ASP.NET Apps v4.0.30319 and ASP.NET v4.0.30319 categories — Requests/Sec, Requests Queued, Requests Timed Out, % Managed Processor Time, Errors Total/Sec, Sessions Active, Cache Total Hit Ratio — viewable live via Performance Monitor (perfmon.exe) or captured for trend analysis by any monitoring agent that reads Windows performance counters. Requests Queued climbing under load is the classic early signal of thread-pool starvation, frequently caused by blocking synchronous I/O in code that should have been made asynchronous.

The classic "works on my machine" web.config differences

The most common source of "it worked in dev, broke in production" for Web Forms specifically: a web.config <compilation debug="true"> left on in production (major performance cost — disables batch compilation and some JIT optimizations, and leaks stack traces regardless of customErrors), a machineKey that is auto-generated per-server rather than explicitly shared across a farm (breaking ViewState/Forms-auth validation intermittently depending on which server handles a given request — see Security), and connection strings or <appSettings> pointing at a developer’s local database because a Web.Release.config transform was never actually wired up or tested. Verifying the deployed, post-transform web.config on the target server — not just the source-controlled one — after every release catches most of these before they become an incident.