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

Viewed 134356

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?

12 Answers

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

This answer about allowing concurrent request per session is great but it is missing some important details:

  1. The setting to allow concurrent requests per session is implemented in the newer ASP .NET Session state module which is of type Microsoft.AspNet.SessionState.SessionStateModuleAsync. This setting is supported for any provider who can work with this module.
  2. The older sessionstate module System.Web.SessionState.SessionStateModule does not support this.
  3. Make sure session state usage is thread safe or concurrency issue can occur in the session

Summary to enable this feature:

Allow concurrent request:

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

Make sure newer session state module is used:

<system.webServer>
  <modules>
    <!-- remove the existing Session state module -->
    <remove name="Session" />
    <add name="Session" preCondition="integratedMode" type="Microsoft.AspNet.SessionState.SessionStateModuleAsync, Microsoft.AspNet.SessionState.SessionStateModule, Version=1.1.0.0, Culture=neutral" />
  </modules>
</system.webServer>

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.

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.

For Mono users out there that encountered this issue and found none of the solutions helpful, you are not doing anything wrong.
There is a bug in Mono (Issue #19618) that makes SessionStateBehavior useless on SessionStateModule, so it doesn't matter if you set SessionStateBehavior on Web.config/pages, Application_BeginRequest, or set an attribute on a Controller, or an Action. None will work. I tried.

However, the logic that prevents locking (calling GetItem instead of GetItemExclusive on SessionStateModule) is there with one limitation: The HttpHandler must implement the marker interface IReadOnlySessionState.

So instead of implementing my own SessionStateModule, I took a different (bit hacky) approach.


To your consideration:

// Custom handler that derives from MvcHandler which implements IReadOnlySessionState
public class MvcReadOnlyHandler : MvcHandler, IReadOnlySessionState
{
    public MvcReadOnlyHandler(RequestContext requestContext) : base(requestContext)
    {
    }
}
// Custom RouteHandler that derives from `MvcRouteHandler` which
// returns our very own `MvcReadOnlyHandler`
public class MvcConcurrentRouteHandler : MvcRouteHandler
{
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new MvcReadOnlyHandler(requestContext);
    }
}
// On Global.asax.cs Application_Start, after all the Routes and Areas are registered
// change only the route handler to the new concurrent route handler
foreach (var routeBase in RouteTable.Routes)
{
    // Check if the route handler is of type MvcRouteHandler
    if (routeBase is Route { RouteHandler: MvcRouteHandler _ } route)
    {
         // Replace the route handler
         route.RouteHandler = new MvcConcurrentRouteHandler();
    }
}

Since now the router do implements IReadOnlySessionState there is no locking on the session id

Hopefully when the bug will be fixed my solution will be redundant, but untill then, I hope it will help someone.


Important note: This solution basically makes storing items on the Session unsafe, I don't use this feature so for me it works. You can still add items since ReadOnly does not prevent writing, it is just not locking.
If you want to guarantee safe writing, you can add another extension method MapRoute to RouteCollection to use the new router, in order to register routes that doesnt lock. Like that you can register your routes to new MvcConcurrentRouteHandler router or to the existing one for writing.

Related