PHP Sessions Can Hurt Your WordPress Performance

Many of the projects that we onboard come with crippling performance issues that make their WordPress sites slow, resource-hungry and incapacitated under load. Today we’ll look at another common problem that we often see – PHP Sessions.

What Are PHP Sessions

Sessions in PHP allow you to store arbitrary data about a particular request, and pass that data along to subsequent requests by the same visitor. This allows developers to store things like shopping cart items, authentication sessions and many other things.

By default, that data is stored on disk on the web server with a unique session identifier. This identifier is passed on with every request using an HTTP cookie (or GET/POST variables) called PHPSESSID, which allows PHP to link future requests by the same visitor to an existing session.

For a more in-depth explanation with examples, read the documentation in the PHP manual.

The Problems with Sessions

Remember, a PHP session will generate a unique identifier linking a visitor to their session data. Think about that for a moment.

It means that when using sessions, each visitor’s identifier will force all their requests to be unique, and this creates a huge problem for full page caching plugins and services, because they simply can’t serve the same cached data, if the requests were unique.

The Impact of PHP Sessions on WordPress Performance
The Impact of PHP Sessions on WordPress Performance

So if you’re calling session_start() early on every single request, best case scenario is your caching solution will simply ignore and never cache these requests. Worst case — it will cache the requests individually, meaning if 1000 visitors opened your home page, you now have 1000 different cached copies of the same page. This can lead to other more valuable cached data being evicted to make room for this nonsense.

Always Monitor Your WordPress Caching Efficiency
Always Monitor Your WordPress Caching Efficiency

Although, come to think of it, an even worse case scenario would be if your caching plugin did cache these requests under the same cache key, completely ignoring the existence of a session. This would be highly insecure, since there’s a risk of one user receiving a cached page which was generated for a different user.

File-based Session & Locking

Another problem with sessions in PHP is that by default they use the local filesystem to store all the session data.

This means that sessions is a big no-no for WordPress projects spanned across multiple web servers, unless you have the same IPs always hit the same web server with IP hashing at the load balancer level. But that’s kind of a hack, because you might need to take the server offline for a hardware upgrade, taking down all the session data with it.

One might also assume that disk IO may eventually become a bottleneck, but session files are often way too small to generate that kind of load on the disks, especially SSDs, and on hardware with plenty of room for disk page cache in memory.

Exclusive Locks

But there is another gotcha! See, when you call session_start() in PHP, the process running your code will obtain an exclusive lock on that session file, meaning any other process will have to wait for that lock to be released, before obtaining a similar lock.

So here’s a simple scenario. Let’s assume you have this silly backup plugin and you hit that button in your wp-admin which fires a request that takes 30 minutes to load, and along with that request you passed on your session identifier.

This will spawn a PHP process, which will acquire an exclusive lock to your session file, and start backing things up for 30 minutes…

While you wait, you decide to visit a completely different page on your site, and again, your session identifier is passed along with that request, spawning another PHP process with the same session. Now this process will try to acquire an exclusive lock for the same file, but it’ll have to wait for the first process to release it.

Exclusive Locking with PHP Sessions
Exclusive Locking with PHP Sessions

At this point you’re a bit frustrated since your home page is not loading, so you hit refresh, spawning a third PHP process with the same session identifier, which will also wait for the first process to release the lock.

And now you’re even more frustrated, so you hit that refresh button a few more times, spawning a fourth, fifth, and sixth PHP processes, all waiting for that backup request to release the lock.

Suppose your web server is configured for up to 8 simultaneous PHP processes, it will take you only a couple more “tries” to render that server completely unresponsive for everyone, until that backup task is done.

You won’t see this often, especially on smaller sites, but when you do see it, it’s not very obvious and hard to debug.

WordPress Does Not Use PHP Sessions

It’s worth noting that WordPress Core does not use native PHP sessions. Instead, it heavily relies on cookies for authentication, and stores any additional information about an authenticated session in the database.

This means that if you are seeing a PHPSESSID cookie on your WordPress site, it’s coming from your theme or a plugin, and unfortunately there are way too many WordPress plugins using PHP sessions, and what’s even more sad is that most of them are doing in wrong.

It’s fairly simple to find out what may be initializing a PHP session by searching your codebase:

$ cd /path/to/wp-content
$ grep -r 'session_start'

Note that there are some plugins which justify the use of sessions and initiate them at the right time and at the right place. Others such as WooCommerce chose to implement their own session handlers at the database level with persistent object caching.

But most other plugins will simply call session_start() the first opportunity they get, often times during init or plugins_loaded, hurting cachability and performance.

Debugging Response Headers in Google Chrome
Debugging Response Headers in Google Chrome

It’s not always easy to spot legitimate uses of sessions. We suggest making a few requests to pages you would expect to be served from cache, using an incognito browser window or cURL from the command line. Look for the PHPSESSID cookie in the response header:

$ curl -v http://example.org > /dev/null
< Set-Cookie: PHPSESSID=...; path=/

Solutions

When you find such a plugin, the best thing you can do for your WordPress performance is not use it.

If, however, it provides some functionality that is essential to your business, and there’s no alternative available, it would be a good idea to get in touch with the plugin author and asking them to address these problems.

If you’re a developer working on such a plugin, and you can’t think of a way to provide the same features without the use of sessions, the basic rule to keep in mind is: initialize sessions only when necessary and as late as possible. You can also look into using session_write_close() if necessary.

Moving Sessions to a Database

If you’re working with a multi-server environment, or experiencing the exclusive file locking problem, you can offload all your PHP sessions to a different place with session_set_save_handler().

The database is often a good option, and we wrote a small mu-plugin called WPDB PHP Sessions which uses the WordPress $wpdb wrapper to store all session data in MySQL. It also includes basic garbage collection which runs hourly via wp-cron.

The plugin is meant to run on the Pressjitsu hosting platform, but you can make it work on any self-hosted WordPress install by changing the enable flag in the configuration array.

You can also try storing sessions data in the WordPress options table, but it can quickly get out of hand. Using an in-memory key-value store such as Redis or Memcached for sessions will often give you a great performance boost on high-traffic WordPress sites.

It is always important to remember, though, that most solutions (including WPDB PHP Sessions) are not thread-safe and can result in race conditions unless there are explicit locks, which come with their own set of issues (as seen in the default file-backed PHP session handler). However, in many cases, a single unique session will not be used in enough parallel executions (especially not in the browser) to cause any race conditions.

It’s not only about PHP Sessions

As we have outlined above, the main problem with sessions is that they create a unique cookie for every visitor. Even if a plugin does not use sessions, it can still create unique identifiers and pass them on as a cookie on every page load causing the same effect, so watch out for those too!

We’ve noticed some e-commerce, single sign-on, behavior tracking, and A/B testing plugins doing this wrong.

Even WordPress itself is a bit guilty here, storing a commentator’s first name, e-mail address and URL in a cookie after submitting a comment, and reading it later with PHP. Mark Jaquith’s Cache Buddy plugin attempts to address that and some other things with a JavaScript approach.

Finally, some JavaScript-based libraries and services will also set cookies with unique identifiers, and for better efficiency you should look into tuning your caching plugin to ignore and strip these “js-only” cookies from the request. Good candidates are Google’s _ga and __utm cookies.

Conclusion

WordPress performance is hard, and these little non-obvious things can make a big difference. Always keep an eye out for all the cookies being set by your website, and don’t forget that JavaScript can set cookies that hurt your cachability too.

Always look at your performance metrics, most importantly the cache hit rate. If you see a sudden drop after activating a new plugin, chances are trouble is coming. If you’re looking for a neat page caching solution for WordPress, check out our plugin.

Subscribe to our free newsletter for tips about WordPress development, performance, scaling, and security.