38

The Problem:

When you use session in an ASP.NET site, it causes dramatic delays (multiples of 500ms) when loading multiple requests at nearly the same time.

More Specifically, My Problem

Our website uses session exclusively for its SessionId. We use this key to look up a db table that has user info, etc. This seemed to be a good design since it stored minimal session data. Unfortunately, the SessionId will change if you store nothing in session, so we store Session["KeepId"] = 1;. This is enough to convince SessionId to not change.

On a seemingly unrelated note, the site serves customized product images through a controller action. This action generates an image if needed then redirects to the cached image. This means that one web page may contain images or other resources, sending dozens of requests through the ASP.NET pipeline.

For whatever reason, when you (a) send multiple requests at the same time and (b) have session involved in any way then you will end up with random 500ms delays about 1/2 the time. In some cases these delays will be 1000ms or more, always increasing in intervals of ~500ms. More requests means longer wait times. Our page with dozens of images can wait 10+ seconds for some images.

How to Reproduce the Problem:

  1. Create an empty ASP.NET MVC Web Application
  2. Create an empty controller action:

    public class HomeController : Controller
    {
      public ActionResult Test()
      {
        return new EmptyResult();
      }
    }
    
  3. Make a test.html page with a few img tags that hit that action:

    <img src="Home/Test?1" />
    <img src="Home/Test?2" />
    <img src="Home/Test?3" />
    <img src="Home/Test?4" />
    
  4. Run the page & watch the fast load times in firebug:

    Each image loads reasonably fast

  5. Do one of the follow (both have the same result):

    • Add an empty Session_Start handler to your Global.asax.cs

      public void Session_Start() { }
      
    • Put something in session

      public class HomeController : Controller
      {
        public ActionResult Test()
        {
          HttpContext.Current.Session["Test"] = 1;
          return new EmptyResult();
        }
      }
      
  6. Run the page again & notice the occasional/random delays in the responses.

    Some requests experience long delays

What I Know So Far

My Question

How can I use session but avoid these delays? Does anyone know of a fix?

If there's not a fix, I guess we'll have to bypass session & use cookies directly (session uses cookies).

Community
  • 1
  • 1
bendytree
  • 13,095
  • 11
  • 75
  • 91
  • Question: Does it happen only for multiple requests from the same session? (in other words, concurrent requests each from different sessions are OK) – Andrew Barber Dec 01 '11 at 22:16
  • What session state mode are you using? InProc, ASP.NET Session State Server, SQL Server? – rsbarro Dec 01 '11 at 22:19
  • thanks andrew - after brief testing, requests from different sessions appear OK. rsbarro - we're using InProc – bendytree Dec 01 '11 at 22:47
  • What happens if you switch to StateServer instead of InProc? – rsbarro Dec 02 '11 at 01:26
  • As Gats mentioned, the problem appears to be ASP.NET's locking on session. This locking also applies to StateServer & SqlServer, so those probably won't help. I just tried SqlServer & it had almost identical delays. – bendytree Dec 02 '11 at 15:41

6 Answers6

23

As Gats mentioned, the problem is that ASP.NET locks session so each request for the same session must run in serial.

The annoying part is if I ran all 5 requests in serial (from the example), it'd take ~40ms. With ASP.NET's locking it's over 1000ms. It seems like ASP.NET says, "If session is in use, then sleep for 500ms & try again."

If you use StateServer or SqlServer instead of InProc, it won't help - ASP.NET still locks session.

There are a few different fixes. We ended up using the first one.

Use Cookies Instead

Cookies are sent in the header of every request, so you should keep it light & avoid sensitive info. That being said, session uses cookies by default to remember who is who by storing a ASPNET_SessionId string. All I needed is that id, so there's no reason to endure ASP.NET's session locking when it's just a wrapper around an id in a cookie.

So we avoid session completely & store a guid in the cookie instead. Cookies don't lock, so the delays are fixed.

Use MVC Attributes

You can use session, but avoid the locks on certain requests by making session read-only.

For MVC 3 applications, use this attribute on your controller to make session read-only (it doesn't work on a specific action):

[SessionState(SessionStateBehavior.ReadOnly)]

Disable Session for Some Routes

You can also disable session through MVC routing, but it's a bit complicated.

Disable Session on a Specific Page

For WebForms, you can disable session for certain aspx pages.

Community
  • 1
  • 1
bendytree
  • 13,095
  • 11
  • 75
  • 91
  • Good answer, I was going to make a response saying to use SessionState Disabled for the action(s), you can still easily read the session id directly from the cookie. Didn't realize this was the OP's answer. So remember you can still get the session ID even if session state is disabled since the cookie will still always be sent and can be read as a normal cookie. – Chris Marisic Dec 02 '11 at 16:08
  • 1
    `As Gats mentioned, the problem is that ASP.NET locks session so each request must run in serial.` You should add "...each request **for the same session** must run in serial.."; otherwise you give the wrong impression. – Icarus Dec 02 '11 at 16:13
  • Also, the link Gats provided is *SO OLD* (2000) that I'd rather link this: http://msdn.microsoft.com/en-us/library/ms178581.aspx – Icarus Dec 02 '11 at 16:15
  • 1
    I'm SO OLD Icarus.. and that ain't changed much in the last 10 years :) – Gats Dec 04 '11 at 00:28
  • ASP.NET refuses and even actively removes the "ASP.NET_SessionId" cookie from the response when session state is read-only. So you'd actually have to hit a non-read-only page at least once to get the cookie to the client and then hope the client sends it back every time. Crazy. – Triynko Nov 03 '16 at 21:21
14

I took a look at the ASP.NET framework. The class where session state is managed is defined here : http://referencesource.microsoft.com/#System.Web/State/SessionStateModule.cs,114

Here is how it works (simplified code) :

LOCKED_ITEM_POLLING_INTERVAL = 500ms;
LOCKED_ITEM_POLLING_DELTA = 250ms;

bool GetSessionStateItem() {
    item = sessionStore.GetItem(out locked);

    if (item == null && locked) {
        PollLockedSession();
        return false;
    }
    return true;
}

void PollLockedSession() {
    if (timer == null) {
        timer = CreateTimer(PollLockedSessionCallback, LOCKED_ITEM_POLLING_INTERVAL);
    }
}

void PollLockedSessionCallback() {
    if (DateTime.UtcNow - lastPollCompleted >= LOCKED_ITEM_POLLING_DELTA) {             
          isCompleted = GetSessionStateItem();
          lastPollCompleted = DateTime.UtcNow;

          if(isCompleted) {
              ResetPollTimer();
          }
    } 
}

Summary : if the session item cannot be retrieved because it is locked by another thread, a timer will be created. It will regularly pool the session to try to fetch the item again (every 500ms by default). Once item has been successfully retrieved, the timer is cleared. Additionally, there is a check to make sure that there is a given delay between GetSessionStateItem() calls (LOCKED_ITEM_POLLING_DELTA = 250 ms by default).


It's possible to change the default value of LOCKED_ITEM_POLLING_INTERVAL by creating the following key in the registry (this will affect all websites running on the machine):

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ASP.NET
SessionStateLockedItemPollInterval (this is a DWORD)

Another way (which is a hack) is to change the value by reflection :

Type type = typeof(SessionStateModule);
FieldInfo fieldInfo = type.GetField("LOCKED_ITEM_POLLING_INTERVAL",
   BindingFlags.NonPublic | BindingFlags.Static);
fieldInfo.SetValue(null, 100); //100ms

DISCLAIMER : exact consequences of lowering this value are unknown. It might increase thread contention on server, create potential deadlocks etc... A better solution is to avoid to use session state or decorate your controller with SessionStateBehavior.ReadOnly attribute as other users suggested.

tigrou
  • 4,236
  • 5
  • 33
  • 59
2

There are a few ways to speed up session in ASP.NET. First of all a few key points:

  1. Session is thread safe which means it doesn't play nice with concurrent client requests that need to access it. If you need asyncronous processes to access session type data, you can use some other form of temporary storage like cache or your database (for async progress tracking this is necessary). Otherwise the users session info will lock on the context of the request to prevent issues with altering it. Hence why request 1 is fine, then the following requests have a delay. PS this is absolutely necessary to garantee the integrity of the data in session... like user orders in ecommerce.

  2. Session is serialized in and out on the server. A cookie is used to maintain session although it's a different type of cookie which means I wouldn't expect much difference in performance.

So my favourite way to speed up session requests is always to use state server. It takes the in process problems away and also means that in testing you can rebuild your project without having to log in again to your site. It also makes sessions possible in simple load balancing scenarios. http://msdn.microsoft.com/en-us/library/ms972429.aspx

PS I should add that technically out of process state service is supposed to be slower. In my experience it is faster and easier to develop with, but I'd imagine that depends on how it's being used.

Gats
  • 3,452
  • 19
  • 20
  • thanks Gats - the thread safe behavior explains a lot. After some testing, the thread safety (understandably) applies to StateServer & SqlServer, so neither would help me. The issue isn't so much performance as much as the very slow locking mechanism. When requests use session but don't block each other it takes ~9ms which is plenty fast. – bendytree Dec 02 '11 at 15:38
  • Yep the thread safety does apply to all implementations of session state. If you have your own custom sql or cache based session variables then it will not be locked beyond the duration of the GET on those values. I have used this in the past to provide async progress for long running processes where I need secure session behaviour with error reporting while the first request is completing. This required a seperate store out of session that was secured by a unique guid that was passed back intermittently. State server tips were just tips for development, not suggested solution :) – Gats Dec 04 '11 at 00:19
1

I just wanted to add a little detail to the great answer of @bendytree. When using WebForms you cannot only disable session state completely

<%@ Page EnableSessionState="False" %>

but also set it to readonly:

<%@ Page EnableSessionState="ReadOnly" %>

This solved the described problem in my case. See documentation

Stefan Ortgies
  • 295
  • 1
  • 2
  • 13
0

just wondering if someone can chime in my issue that seems related to the above.

We are using Redis as session state provider and are seeing the exact same behaviour: enter image description here

Note that we have already reduced the LOCKED_ITEM_POLLING_INTERVAL to 100ms and we have also decorated the Search controller with the [SessionState(SessionStateBehavior.ReadOnly)] attribute. Still, the issue persists.

Any idea what else we could try? The orange lines are REDIS commands, first one being a GET and then all the subsequent EVAL.

Rok
  • 1,482
  • 3
  • 18
  • 37
0
Since problem is related to parallel calls. First check if that can be reduced specially if its hitting some API's ( calling some stored procs usually ). Try to combine the results into single DB call, optimized with a single return result.  So that single call from the web page can avoid causing session locking issues.

Another option to be tried via these settings
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>

or 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>

And lastly Code from Microsoft for Custom session-state module implementation that stores session information in memory using a Hashtable

https://learn.microsoft.com/en-us/dotnet/api/system.web.sessionstate.sessionstateutility?redirectedfrom=MSDN&view=netframework-4.8.1