[asp.net] I just discovered why all ASP.Net websites are slow, and I am trying to work out what to do about it

I just discovered that every request in an ASP.Net web application gets a Session lock at the beginning of a request, and then releases it at the end of the request!

In case the implications of this are lost on you, as it was for me at first, this basically means the following:

  • Anytime an ASP.Net webpage is taking a long time to load (maybe due to a slow database call or whatever), and the user decides they want to navigate to a different page because they are tired of waiting, THEY CAN'T! The ASP.Net session lock forces the new page request to wait until the original request has finished its painfully slow load. Arrrgh.

  • Anytime an UpdatePanel is loading slowly, and the user decides to navigate to a different page before the UpdatePanel has finished updating... THEY CAN'T! The ASP.net session lock forces the new page request to wait until the original request has finished its painfully slow load. Double Arrrgh!

So what are the options? So far I have come up with:

  • Implement a Custom SessionStateDataStore, which ASP.Net supports. I haven't found too many out there to copy, and it seems kind of high risk and easy to mess up.
  • Keep track of all requests in progress, and if a request comes in from the same user, cancel the original request. Seems kind of extreme, but it would work (I think).
  • Don't use Session! When I need some kind of state for the user, I could just use Cache instead, and key items on the authenticated username, or some such thing. Again seems kind of extreme.

I really can't believe that the ASP.Net Microsoft team would have left such a huge performance bottleneck in the framework at version 4.0! Am I missing something obvious? How hard would it be to use a ThreadSafe collection for the Session?

This question is related to asp.net performance session iis architecture

The answer is


If your page does not modify any session variables, you can opt out of most of this lock.

<% @Page EnableSessionState="ReadOnly" %>

If your page does not read any session variables, you can opt out of this lock entirely, for that page.

<% @Page EnableSessionState="False" %>

If none of your pages use session variables, just turn off session state in the web.config.

<sessionState mode="Off" />

I'm curious, what do you think "a ThreadSafe collection" would do to become thread-safe, if it doesn't use locks?

Edit: I should probably explain by what I mean by "opt out of most of this lock". Any number of read-only-session or no-session pages can be processed for a given session at the same time without blocking each other. However, a read-write-session page can't start processing until all read-only requests have completed, and while it is running it must have exclusive access to that user's session in order to maintain consistency. Locking on individual values wouldn't work, because what if one page changes a set of related values as a group? How would you ensure that other pages running at the same time would get a consistent view of the user's session variables?

I would suggest that you try to minimize the modifying of session variables once they have been set, if possible. This would allow you to make the majority of your pages read-only-session pages, increasing the chance that multiple simultaneous requests from the same user would not block each other.


If you are using the updated Microsoft.Web.RedisSessionStateProvider(starting from 3.0.2) you can add this to your web.config to allow concurrent sessions.

<appSettings>
    <add key="aspnet:AllowConcurrentRequestsPerSession" value="true"/>
</appSettings>

Source


For ASPNET MVC, we did the following:

  1. By default, set SessionStateBehavior.ReadOnly on all controller's action by overriding DefaultControllerFactory
  2. On controller actions that need writing to session state, mark with attribute to set it to SessionStateBehavior.Required

Create custom ControllerFactory and override GetControllerSessionBehavior.

    protected override SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, Type controllerType)
    {
        var DefaultSessionStateBehaviour = SessionStateBehaviour.ReadOnly;

        if (controllerType == null)
            return DefaultSessionStateBehaviour;

        var isRequireSessionWrite =
            controllerType.GetCustomAttributes<AcquireSessionLock>(inherit: true).FirstOrDefault() != null;

        if (isRequireSessionWrite)
            return SessionStateBehavior.Required;

        var actionName = requestContext.RouteData.Values["action"].ToString();
        MethodInfo actionMethodInfo;

        try
        {
            actionMethodInfo = controllerType.GetMethod(actionName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
        }
        catch (AmbiguousMatchException)
        {
            var httpRequestTypeAttr = GetHttpRequestTypeAttr(requestContext.HttpContext.Request.HttpMethod);

            actionMethodInfo =
                controllerType.GetMethods().FirstOrDefault(
                    mi => mi.Name.Equals(actionName, StringComparison.CurrentCultureIgnoreCase) && mi.GetCustomAttributes(httpRequestTypeAttr, false).Length > 0);
        }

        if (actionMethodInfo == null)
            return DefaultSessionStateBehaviour;

        isRequireSessionWrite = actionMethodInfo.GetCustomAttributes<AcquireSessionLock>(inherit: false).FirstOrDefault() != null;

         return isRequireSessionWrite ? SessionStateBehavior.Required : DefaultSessionStateBehaviour;
    }

    private static Type GetHttpRequestTypeAttr(string httpMethod) 
    {
        switch (httpMethod)
        {
            case "GET":
                return typeof(HttpGetAttribute);
            case "POST":
                return typeof(HttpPostAttribute);
            case "PUT":
                return typeof(HttpPutAttribute);
            case "DELETE":
                return typeof(HttpDeleteAttribute);
            case "HEAD":
                return typeof(HttpHeadAttribute);
            case "PATCH":
                return typeof(HttpPatchAttribute);
            case "OPTIONS":
                return typeof(HttpOptionsAttribute);
        }

        throw new NotSupportedException("unable to determine http method");
    }

AcquireSessionLockAttribute

[AttributeUsage(AttributeTargets.Method)]
public sealed class AcquireSessionLock : Attribute
{ }

Hook up the created controller factory in global.asax.cs

ControllerBuilder.Current.SetControllerFactory(typeof(DefaultReadOnlySessionStateControllerFactory));

Now, we can have both read-only and read-write session state in a single Controller.

public class TestController : Controller 
{
    [AcquireSessionLock]
    public ActionResult WriteSession()
    {
        var timeNow = DateTimeOffset.UtcNow.ToString();
        Session["key"] = timeNow;
        return Json(timeNow, JsonRequestBehavior.AllowGet);
    }

    public ActionResult ReadSession()
    {
        var timeNow = Session["key"];
        return Json(timeNow ?? "empty", JsonRequestBehavior.AllowGet);
    }
}

Note: ASPNET session state can still be written to even in readonly mode and will not throw any form of exception (It just doesn't lock to guarantee consistency) so we have to be careful to mark AcquireSessionLock in controller's actions that require writing session state.


I prepared a library based on links posted in this thread. It uses the examples from MSDN and CodeProject. Thanks to James.

I also made modifications advised by Joel Mueller.

Code is here:

https://github.com/dermeister0/LockFreeSessionState

HashTable module:

Install-Package Heavysoft.LockFreeSessionState.HashTable

ScaleOut StateServer module:

Install-Package Heavysoft.LockFreeSessionState.Soss

Custom module:

Install-Package Heavysoft.LockFreeSessionState.Common

If you want to implement support of Memcached or Redis, install this package. Then inherit the LockFreeSessionStateModule class and implement abstract methods.

The code is not tested on production yet. Also need to improve error handling. Exceptions are not caught in current implementation.

Some lock-free session providers using Redis:


After struggling with all available options, I ended up writing a JWT token based SessionStore provider (the session travels inside a cookie, and no backend storage is needed).

http://www.drupalonwindows.com/en/content/token-sessionstate

Advantages:

  • Drop-in replacement, no changes to your code are needed
  • Scale better than any other centralized store, as no session storage backend is needed.
  • Faster than any other session storage, as no data needs to be retrieved from any session storage
  • Consumes no server resources for session storage.
  • Default non-blocking implementation: concurrent request won't block each other and hold a lock on the session
  • Horizontally scale your application: because the session data travels with the request itself you can have multiple web heads without worrying about session sharing.

Marking a controller's session state as readonly or disabled will solve the problem.

You can decorate a controller with the following attribute to mark it read-only:

[SessionState(System.Web.SessionState.SessionStateBehavior.ReadOnly)]

the System.Web.SessionState.SessionStateBehavior enum has the following values:

  • Default
  • Disabled
  • ReadOnly
  • Required

Unless your application has specially needs, I think you have 2 approaches:

  1. Do not use session at all
  2. Use session as is and perform fine tuning as joel mentioned.

Session is not only thread-safe but also state-safe, in a way that you know that until the current request is completed, every session variable wont change from another active request. In order for this to happen you must ensure that session WILL BE LOCKED until the current request have completed.

You can create a session like behavior by many ways, but if it does not lock the current session, it wont be 'session'.

For the specific problems you mentioned I think you should check HttpContext.Current.Response.IsClientConnected. This can be useful to to prevent unnecessary executions and waits on the client, although it cannot solve this problem entirely, as this can be used only by a pooling way and not async.


Just to help anyone with this problem (locking requests when executing another one from the same session)...

Today I started to solve this issue and, after some hours of research, I solved it by removing the Session_Start method (even if empty) from the Global.asax file.

This works in all projects I've tested.


I started using the AngiesList.Redis.RedisSessionStateModule, which aside from using the (very fast) Redis server for storage (I'm using the windows port -- though there is also an MSOpenTech port), it does absolutely no locking on the session.

In my opinion, if your application is structured in a reasonable way, this is not a problem. If you actually need locked, consistent data as part of the session, you should specifically implement a lock/concurrency check on your own.

MS deciding that every ASP.NET session should be locked by default just to handle poor application design is a bad decision, in my opinion. Especially because it seems like most developers didn't/don't even realize sessions were locked, let alone that apps apparently need to be structured so you can do read-only session state as much as possible (opt-out, where possible).


Examples related to asp.net

RegisterStartupScript from code behind not working when Update Panel is used You must add a reference to assembly 'netstandard, Version=2.0.0.0 No authenticationScheme was specified, and there was no DefaultChallengeScheme found with default authentification and custom authorization How to use log4net in Asp.net core 2.0 Visual Studio 2017 error: Unable to start program, An operation is not legal in the current state How to create roles in ASP.NET Core and assign them to users? How to handle Uncaught (in promise) DOMException: The play() request was interrupted by a call to pause() ASP.NET Core Web API Authentication Could not load file or assembly 'CrystalDecisions.ReportAppServer.CommLayer, Version=13.0.2000.0 WebForms UnobtrusiveValidationMode requires a ScriptResourceMapping for jquery

Examples related to performance

Why is 2 * (i * i) faster than 2 * i * i in Java? What is the difference between spark.sql.shuffle.partitions and spark.default.parallelism? How to check if a key exists in Json Object and get its value Why does C++ code for testing the Collatz conjecture run faster than hand-written assembly? Most efficient way to map function over numpy array The most efficient way to remove first N elements in a list? Fastest way to get the first n elements of a List into an Array Why is "1000000000000000 in range(1000000000000001)" so fast in Python 3? pandas loc vs. iloc vs. at vs. iat? Android Recyclerview vs ListView with Viewholder

Examples related to session

What is the best way to manage a user's session in React? Spring Boot Java Config Set Session Timeout PHP Unset Session Variable How to kill all active and inactive oracle sessions for user Difference between request.getSession() and request.getSession(true) PHP - Session destroy after closing browser Get Current Session Value in JavaScript? Invalidating JSON Web Tokens How to fix org.hibernate.LazyInitializationException - could not initialize proxy - no Session How can I get session id in php and show it?

Examples related to iis

ASP.NET Core 1.0 on IIS error 502.5 CS1617: Invalid option ‘6’ for /langversion; must be ISO-1, ISO-2, 3, 4, 5 or Default Publish to IIS, setting Environment Variable IIS Manager in Windows 10 The page cannot be displayed because an internal server error has occurred on server The service cannot accept control messages at this time NuGet: 'X' already has a dependency defined for 'Y' Changing project port number in Visual Studio 2013 System.Data.SqlClient.SqlException: Login failed for user "This operation requires IIS integrated pipeline mode."

Examples related to architecture

Single Page Application: advantages and disadvantages Dilemma: when to use Fragments vs Activities: What is the technology behind wechat, whatsapp and other messenger apps? Design Documents (High Level and Low Level Design Documents) A potentially dangerous Request.Form value was detected from the client Is Django for the frontend or backend? How should a model be structured in MVC? When to Redis? When to MongoDB? I just discovered why all ASP.Net websites are slow, and I am trying to work out what to do about it When is it appropriate to use C# partial classes?