We recently ran into an issue where several of our ajax requests were taking an inordinate amount of time to execute. Digging a little deeper into the queries being executed, I was expecting return times in the order of 200ms, not the several seconds I was seeing. Installing XHGui only furthered my confusion: session_start() was the culprit with incredibly high run times.

I started exploring how we were storing sessions, the default file store. My first thought was that perhaps there was so many files in the session directory that directory access was slowed (easily fixed with some options on session.save_path). But we only had a few active sessions and fewer than 100 files. No dice.

After some head pounding, I realized the issue: PHP locks the session file for the duration of script execution. The first ajax request took the expected amount of time, but locked the user’s session file for the duration of the request. The second ajax request was blocked on session_start() until the first call had finished. The third ajax request blocked on the first two, and was left with the incredibly high run length that drew me in in the first place.

You can try it out yourself like this: Open a basic HTML file including jQuery, and add the following: $(document).ready(function() { $.ajax({ type: “GET”, url: “/destination.php?q=1”, async : true }).done(function( msg ) { }); $.ajax({ type: “GET”, url: “/destination.php?q=2”, }).done(function( msg ) { }); $.ajax({ type: “GET”, url: “/destination.php?q=3”, }).done(function( msg ) { }); }); Then create destination.php is quite simple: <?php session_start(); sleep(1);

In your browser’s developer tools you should see three requests fired off almost simultaneously. The first will take about a second to execute, the second will take two seconds, and the third three.

I couldn’t simply replace our session datastore (as attractive as that is) with something like APC or memcache, as we’d lose all our user data if the server restarted. Implementing a system that uses memory but falls back on a persistent store would take more time than I had. I also couldn’t rip out session usage here: it was needed for authentication, and in some cases helped craft the response.

Luckily PHP has a solution: session_write_close();. This writes any changes you’ve made to the session file, then closes it and releases the lock. In my case, I wasn’t ever writing new data to the file, so I could start the session, then call session_write_close() immediately. This released the lock, and allowed the other ajax requests to actually execute asynchronously. You can try it out yourself by modifying destination.php to call session_write_close() immediately after session_start().

If you’re writing your own code, closing out sessions early should be pretty easy. If you’re dealing with a framework, you may want to hack in some code to close early on ajax calls.

Comments »

Weblog: www.juds.com.ua
Tracked: Jul 31, 06:49
If calling session_write_close() immediately after calling session_start(), why call session_start() at all? Or did you mean immediately after doing something with the session data?
#1 Anonymous on 2013-07-24 04:09 (Reply)

Locking the session is a good thing (the two memcache session handlers also do it and you might be better off if you database solution also does it), because it will help you with really bad race conditions.

Remember: The session data is (in most cases) more or less just a serialized array. At the start of the request, the data is fetched and unserialized. Then you change something and at the end of the request, the array is serialized again.

If your background requests change the session state at all, without locking, it would be completely non-deterministic in what order the changes would be saved (if at all - after all, the one request that doesn't change the session data might end up finishing after the request that did).
#2 Philip Hofstetter (Homepage) on 2013-07-24 06:09 (Reply)

The reason to call session_start() is to get $_SESSION populated - with whatever session data you might need (like the ID of the logged in user)
#3 Philip Hofstetter (Homepage) on 2013-07-24 06:10 (Reply)

It is a very old well known "problem" (more than 10 years) which occurs in the past with HTML frames (more popular than ajax 10 years ago)
#4 Arnapou (Homepage) on 2013-07-24 07:20 (Reply)

You can reacquire the session with @session_start() if you need to modify the $_SESSION data. $_SESSION is still available after you closed it with session_write_close(), but any change you make to $_SESSION will be lost.
If you do such things, use a class which modifies $_SESSION and permits any change to the session variables after you closed the session for writing. Can take you a long time to realize why some data in the session might not be available,
#5 Jürgen Henge-Ernst on 2013-07-24 09:55 (Reply)

I was also surprised to discover this after many years of php development - in a case where the web app was making an http call to itself (in a webservice debugger page) and reusing the the same session, it just got stuck.
Since the app is built on a framework, there is little chance to avoid the initial session-locking call, but adding a session_write_close() in my code fixed the problem.
#6 gggeek on 2013-07-25 10:23 (Reply)

Check this interactive PHP reference as well:
I can't code without it!
#7 Mark (Homepage) on 2013-07-26 02:12 (Reply)

You can use https://github.com/nicolasff/phpredis#php-session-handler for storing sessions in memory and configure redis to persist data depending on your requirements. I use this in production on several sites for last 6 months and I haven't experienced any problems.
#8 Slaven Bacelic (Homepage) on 2013-07-28 05:51 (Reply)

This was super helpful. We've got a page of statistics that makes 8 async requests (throttled down to 2 at a time) once the initial page is loaded. The full time to load all packets was 10.5 seconds, and went down to 7.5 seconds. Sweet.
#9 Jon on 2013-08-01 16:28 (Reply)

For larger installations at least, I'd suggest using repcached - a dropin for memcached which adds 2 server master-master replication, such that either server can go down and the app keeps running using the other cache. It's most appropriate in situations where you have multiple servers anyway.

I explored this solution for a slightly different reason - If you use file based session storage on a GFS storage system, locking on the session directory becomes a limiting factor.
#10 mc0e on 2013-10-02 06:44 (Reply)

Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.
Standard emoticons like :-) and ;-) are converted to images.

Hi, I’m Paul Reinheimer, a developer working on the web.

I co-founded WonderProxy which provides access to over 200 proxies around the world to enable testing of geoip sensitive applications. We've since expanded to offer more granular tooling through Where's it Up

My hobbies are cycling, photography, travel, and engaging Allison Moore in intelligent discourse. I frequently write about PHP and other related technologies.