When a Single ASP.NET Client makes Concurrent Requests for Writeable Session Variables

The design of ASP.NET includes the valuable session variables that enable the application to keep track of individual sessions. Unfortunately the ASP.NET pipeline will not process requests belonging to the same session concurrently but queues them, and executes them serially. MVC locks session variables to ensure thread safety and concurrency control, but takes a conservative approach to polling for these locks. Do you always need this level of thread safety? If not, what can you do to improve performance when you need to use writeable session variables?

Problem: Is there a good way to improve performance under concurrent requests from a single client in ASP.NET MVC when using writable session variables?

Currently,  as of version 5.1, ASP.NET MVC will queue concurrent requests from a single client when writable (i.e. not read-only) session variables are used (see The Downsides of ASP.NET Session State). This queuing occurs because MVC locks session variables to ensure thread safety. Unfortunately, the lock is implemented such that the session is only polled every 500ms to check if it is free (see Storing Anything in ASP.NET Session Causes 500ms Delays). As a result, performance can be very poor when multiple requests are made simultaneously from the same client through means such as asynchronous AJAX requests.

To avoid queuing, session can be turned off or set to read-only for specific controllers—possibly automatically if not otherwise specified (see SessionStateBehavior Enumeration). In some cases, allowing a controller to write to session may be desirable or necessary. If this is the case, there are at least two ways that performance may be increased without forgoing the use of writeable session variables. Both of these solutions that we will describe will work on a project-wide scale and will not require session variables to be used differently outside of the thread safety considerations related to Solution 2.

Solution 1: Decrease the Lock Polling Interval

Because the polling interval of the session lock is defined as a static constant within the SessionStateModule class (Microsoft .NET Framework 4.6.2 Reference Source), it may be modified through the use of reflection (Storing Anything in ASP.NET Session Causes 500ms Delays). LOCKED_ITEM_POLLING_INTERVAL is the interval at which polling is performed with a default value of 500ms. LOCKED_ITEM_POLLING_DELTA is the minimum time between polling checks with a default value of 250ms. Code to change these values is provided in Figure 1 below. It may be placed in the project’s Global.asax file, though it isn’t required that it should be placing there.

Figure 1. Code to Change Session Lock Polling Interval

The particular values used here are somewhat arbitrary; however, in general, values closer in duration to the length of time it takes a typical request to be processed are likely desirable. Setting the values too low may lead to an undesirably high usage of CPU resources. Experimentation may need to be performed if performance is initially unsatisfactory.

Although requests will still queue after these modifications are made, performance may increase significantly, especially when many quickly processed requests are made simultaneously.

Solution 2: Implement a Lockless SessionStateStoreProvider

MVC allows for the use of custom session state store providers through the inheritance of the SessionStateStoreProviderBase class (MSDN: Implementing a Session-State Store Provider, MSDN: Session-State Modes). A simple way to use this to our advantage is to use reflection to wrap the pre-existing in-memory session state store provider, InProcSessionStateStore, in a simple class that uses calls to InProcSessionStateStore methods for most of its method implementations. When we encounter locks in either of the methods that are used to retrieve a session, GetItem and GetItemExclusive, we immediately release them using ReleaseItemExclusive and then again attempt to retrieve the now-unlocked session. As a result, session becomes effectively lockless. An implementation of the described class is provided in Figure 2 below.

Figure 2. Lockless Session State Store Provider

Additionally, the XML in Figure 3 below must be placed in the system.web element of the Web.config file of the project in order for the custom session state store provider to be used. The “type” attribute of the LocklessInProcSessionStateStore provider should be the fully qualified class name of the wrapper class. The values provided for the “cookieless” and “timeout” attributes may be changed.

Figure 3. XML Configuration for Lockless Session State Store Provider

This solution will allow for even better performance than that provided by Solution 1; however, it gives up thread safety. If this solution is to be used, each use of session will need to be carefully considered in order to avoid typical multi-threading pitfalls such as race conditions. As such, if it is implemented in an existing project, it may introduce subtle and not-so-subtle bugs.

Example Performance Comparison:

Below are some images of the network tab in Google Chrome’s developer console  in Figures 4, 5, and 6 as examples of the behavior exhibited by implementations with No Solution, Solution 1, and Solution 2 on a page that performs 7 concurrent asynchronous AJAX requests upon loading. The small green bars on the right side of each image represent active client-side AJAX requests over time. The images show an approximately common horizontal time scale. Note that the scales are different because the latency of the standard session is so high.  the actual performance differences are highly circumstantial and variable in nature. Both solutions provide a dramatic improvement, but the first solution, Decreasing the Lock Polling Interval, is a safer solution at a minor cost in performance.

Figure 4. Concurrent Request Performance for No Solution (standard session)

Figure 5. Concurrent Request Performance for Solution 1 (session lock polling interval decreased)

Figure 6. Concurrent Request Performance for Solution 2 (session lock disabled)