As a .NET programmer you have to keep certain rules in mind when developing high performance ASP.NET applications, and/or optimizing your existing ASP.NET website. Here are a few. A lot of information is available on this subject, so in this post I share valuable articles about ASP.NET performance I frequently pass on to customers, so they can improve their ASP.NET web applications.
ASP.NET performance is mostly about memory: avoid Large Object Heap fragmentation, don't push static files through the managed pipeline (runAllManagedModulesForAllRequests), cache wisely and isolate every website in its own application pool. On shared web servers, HighDensityWebHosting and gcTrimCommitOnLowMemory lower the memory usage per site.
I originally wrote this post in 2013 for ASP.NET on IIS 7 and later. The principles still apply to ASP.NET on .NET Framework 4.8 and Windows Server IIS 10.
Tips to fix the performance killers in your ASP.NET application
One important aspect of performance and programming (not only in .NET...) is memory: memory allocation (memory addressing) and memory usage.
The Dangers of the Large Object Heap
Whenever I need to explain why a customer's website uses a lot of memory, I find this one of the best information resources available. .NET manages memory in different regions, called heaps. Small objects (85,000 bytes or less) live in the generation 0, 1 and 2 heaps, where the garbage collector compacts the survivors so free space is always in one large block. Large objects go to the Large Object Heap (LOH), which is not compacted by default. That makes it prone to fragmentation, and when the garbage collector reaches its limits:
the runtime can exhaust memory in a way that is surprising and confusing to any developer who is not aware of how .NET chooses to lay out objects in memory.
Do read on at Simple Talk: The Dangers of the Large Object Heap.
Another great article and explanation is Large Object Heap Uncovered (an old, recovered MSDN article), explaining the LOH inner workings: what qualifies an object as a large object, how large objects are collected and what performance implications they impose. The current Microsoft documentation is The large object heap on Windows systems.
Do you want to monitor the Large Object Heap in, for example, Zabbix? Have a look at how to monitor .NET CLR Garbage Collected heap from your web application.
Back to Basics: Dynamic Image Generation, ASP.NET Controllers, Routing, IHttpHandlers, and runAllManagedModulesForAllRequests
Another important aspect of performance is what type of content you push through the .NET pipeline. Setting a wildcard ASP.NET scriptmapping (back in the good old IIS 6.0 days), or runAllManagedModulesForAllRequests on IIS 7+, pushes everything through ASP.NET. Even generated images or documents. You can imagine this slows down the .NET process and increases memory usage. For this, a basic understanding of the .NET process and request pipeline is required.
Scott Hanselman, a Microsoft programmer and author of several ASP.NET books, wrote an excellent article on, basically, how not to push your images and documents through the whole request pipeline. "RAMMFAR" means that all managed modules run for all requests: PNGs, PDFs, everything, including static files. His advice:
If you can let IIS handle a request before ASP.NET sees it, that's better.
So, unless your application really needs it, make sure it is turned off in your web.config:
<system.webServer>
<modules runAllManagedModulesForAllRequests="false" />
</system.webServer>
Like he says, the article is long but full of info. Read it all on Scott Hanselman's blog.
.NET Baby Steps: Part VII - Caching
Caching is the art of saving information in-process (mostly memory) for later use. The website can reuse the cached information without performing the same, earlier performed operation again. This saves a lot of computing time and information is available faster.
On the other hand, you must think about what information needs to be cached and what not. For instance, you don't want to aggressively cache information for logged in users, so it becomes available to users who are not logged in, just because your caching logic or policy is wrong.
New in .NET 4.0 is the System.Runtime.Caching namespace, and Robert MacLean has a nice article about it. The older System.Web.Caching was not extensible (memory only) and was part of ASP.NET, which made it awkward to use outside web applications. System.Runtime.Caching solves both issues. Read on about how to use it in .NET Baby Steps: Part VII - Caching, and be sure to check out his other baby steps posts.
Global.asax: Application_End and Session_End
A feature of the global.asax file is to fire an event at the end of a session or application. For example, when a visitor leaves your website or when the application pool is recycled. Unfortunately, you never know for sure if the event fired. This might keep objects and variables alive, filling up important memory space.
One example of this behavior: Session_OnEnd or Session_End events in global.asax won't fire if you store ASP.NET sessions out of proc (in State Server or SQL Server). The Session_End event is only supported when the session-state mode is InProc, which is the default. For StateServer and SQLServer modes the event in Global.asax is ignored, and for Custom mode it depends on the session-state store provider. So pay attention to this when you move your sessions from InProc to State Server or SQL Server: code in Session_End will silently stop running.
Sources: SessionStateModule.End Event on Microsoft Learn, and the original MSDN blog post Session_OnEnd or Session_End won't fire if you store ASP.NET sessions out of proc (Wayback Machine).
Moving sessions out of proc is required for a web garden, web farm or load balanced setup. See how to configure SQLServer sessionState for Umbraco.
Use with care!
Fix the 3 silent performance killers for IIS / ASP.NET apps
Mike Volodarsky, lead developer of LeanSentry and a former Microsoft programmer for IIS 7.0 and ASP.NET, writes about fixing the three silent performance killers for IIS and ASP.NET apps:
If you could double your IIS/ASP.NET application performance by making just a few small tweaks, would you do it?
The three points he outlines to improve are:
- Handled exceptions & Response.Redirect
- LINQ to SQL & non-compiled queries
- Memory allocation & "% Time In GC"
Watching for and fixing these three low-hanging issues can make a big difference in the performance of your ASP.NET application, with a minimal amount of work. Read on at Fix the 3 silent performance killers for IIS / ASP.NET apps.
Tips to improve ASP.NET application performance
An oldie, but most tips still apply: 20 Tips to Improve ASP.NET Application Performance.
Understanding and troubleshooting unmanaged memory usage in .NET
Writing C# every day, we forget that we are in a privileged world. Underneath the abstraction of the CLR lies native C++ code that handles memory the old fashioned way: blocks of memory without a type controlling access to them. Writing outside their bounds, or using a block after it was freed, can lead to spectacular crashes and memory leaks that the garbage collector can't help you with. Red Gate, known for its ANTS profilers, explained this in Understanding and troubleshooting unmanaged memory usage in .NET (Wayback Machine).
ASP.NET Performance, Troubleshooting, and Debugging
Of course, Microsoft has documentation on ASP.NET performance, troubleshooting and debugging. Some of the subjects covered are:
- ASP.NET Performance Overview
- ASP.NET Tracing Overview
- ASP.NET Health Monitoring Overview
- ASP.NET Troubleshooting and Debugging
One major subject in performance is, as mentioned earlier in this article, caching. You'll find the documentation here: ASP.NET Performance, Troubleshooting, and Debugging and specific for caching: ASP.NET Caching.
Keep an eye on your ASP.NET web applications in production: monitor IIS application pools in Zabbix and continue with ASP.NET web application monitoring in Zabbix, part 2.
Ensure Security Isolation for Web Sites
The recommendation for isolating websites in a shared hosting environment is consistent with all general security isolation recommendations for Internet Information Services 7 (IIS 7) and above. In particular, it is recommended to:
- Use one application pool per website.
- Use a dedicated user account as an identity for the application pool.
- Configure anonymous user identity to use the application pool identity.
- Ensure that FastCGI impersonation is enabled in the php.ini file.
Recommended documentation: Ensure Security Isolation for Web Sites and Application Pool Identities.
With one application pool per website, recycling becomes important too. Learn how to set IIS application pool recycle defaults to specific times instead of a regular time interval.
ASP.NET Partial Trust does not guarantee application isolation
ASP.NET lets administrators host applications in partial trust modes such as medium trust, and to configure custom partial trust levels through policy files (see How To: Use Medium Trust in ASP.NET 2.0). For years, partial trust was used to isolate applications on shared web servers. Microsoft updated its guidance though: running an ASP.NET application in partial trust does not guarantee complete isolation from other applications in the same process or on the same server.
The recommended way to isolate ASP.NET applications from each other is running them in separate, low-privileged processes: individual application pools, as described above.
Still running into partial trust issues? Here is how to use MySQL Connector/NET 6.5 in partial trust.
HighDensityWebHosting
Since ASP.NET 4.5, efforts have been made to make the ASP.NET Framework more performant in shared hosting environments, for example if you are the administrator of a server that hosts several small websites. One of these improvements lies in the GC, or Garbage Collection.
Tune GC for high-density web hosting: GC can impact a site's memory consumption, but it can be tuned to enable better performance. You can tune or configure GC for better CPU performance (slow down the frequency of collections) or lower memory consumption (more frequent collections to free up memory sooner). To enable the GC tuning, add the HighDensityWebHosting setting to the runtime node in the Aspnet.config file in the C:\Windows\Microsoft.NET\Framework64\v4.0.30319 folder, to achieve smaller memory consumption (working set) per site:
<configuration>
<!-- ... -->
<runtime>
<performanceScenario value="HighDensityWebHosting" />
<!-- ... -->
</runtime>
</configuration>
As that same administrator hosting several small('ish) websites in a shared hosting environment, you can optimize performance and increase site capacity by adding the following gcTrimCommitOnLowMemory setting to the runtime node in the same Aspnet.config file:
<gcTrimCommitOnLowMemory enabled="true" />
This setting is recommended only for shared web hosting scenarios. With it enabled, the garbage collector starts trimming its committed memory when the system memory load reaches 90%.
If you want to enable both settings for 64-bit and 32-bit .NET using PowerShell, run the following as Administrator:
# https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/optimization-for-shared-web-hosting
# https://learn.microsoft.com/en-us/archive/msdn-magazine/2012/april/clr-an-overview-of-performance-improvements-in-net-4-5
$frameworks = @{
"x64" = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Aspnet.config"
"x86" = "C:\Windows\Microsoft.NET\Framework\v4.0.30319\Aspnet.config"
}
foreach ($arch in $frameworks.Keys) {
$config = $frameworks[$arch]
# Back up Aspnet.config to $env:TEMP\x64 or $env:TEMP\x86
$backupDir = Join-Path $env:TEMP $arch
if (!(Test-Path $backupDir)) {
New-Item -ItemType Directory -Path $backupDir | Out-Null
}
Copy-Item $config (Join-Path $backupDir "Aspnet.config")
$xml = New-Object xml
$xml.Load($config)
# Make sure the <runtime> node exists
$runtime = $xml.SelectSingleNode("configuration/runtime")
if ($null -eq $runtime) {
$runtime = $xml.DocumentElement.AppendChild($xml.CreateElement("runtime"))
}
# Not registered yet
if ($null -eq $runtime.SelectSingleNode("performanceScenario")) {
$node = $xml.CreateElement("performanceScenario")
$node.SetAttribute("value", "HighDensityWebHosting")
$runtime.AppendChild($node) | Out-Null
}
if ($null -eq $runtime.SelectSingleNode("gcTrimCommitOnLowMemory")) {
$node = $xml.CreateElement("gcTrimCommitOnLowMemory")
$node.SetAttribute("enabled", "true")
$runtime.AppendChild($node) | Out-Null
}
$xml.Save($config)
}
# Checks
foreach ($config in $frameworks.Values) {
Select-String -Path $config -Pattern "HighDensityWebHosting", "gcTrimCommitOnLowMemory" |
Select-Object Path, LineNumber, Line
}
First, the PowerShell code copies both Aspnet.config files to a backup directory in your $env:TEMP folder. Secondly, it reads the C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Aspnet.config and C:\Windows\Microsoft.NET\Framework\v4.0.30319\Aspnet.config XML configuration files, and adds both optimization options to the runtime node, unless they are already present.
Last but not least, Select-String checks whether the strings HighDensityWebHosting and gcTrimCommitOnLowMemory are present in the ASP.NET configuration files.
Did you use an earlier version of this script? It placed <performanceScenario> directly under <configuration> instead of under <runtime>. Remove that node from both Aspnet.config files and run the script again.
You can monitor ASP.NET garbage collection using Zabbix: Monitor .NET CLR Garbage Collected heap from your web application. But there are many more relevant Performance Counters, see (Get-Counter -ListSet "*").Counter.
Looking for more server-side performance? Tune Windows Server TCP/IP and IIS for an optimized network performance.