PHP session quirks
Tags: php, performance, engineeringmonday
Published: 2020-11-16 07:21:00

Hello there, fellow developer!

Did you know that PHP Sessions are blocking on a single server instance, but vulnerable to race conditions bugs on multi-server architecture? 

Here are the important things you should know about how sessions work in PHP.


First thing you should know is how sessions are stored.
The default session save handler is called “files”, which just saves all the session data in a file. The file is conveniently named exactly like the value of the PHPSESSID, which is how the server knows where is your session data and if your session even exists on the server and how to retrieve the session data.

Second thing you should know is that “files” session handler is blocking by design, and there is no way to disable this constraint on this session handler. What this means is that every time the server tries to open your session file, it locks the file (using [flock](https://www.php.net/manual/en/function.flock.php)) which prevents any other processes from opening the file – until the lock has been lifted, which happens automatically after the PHP script/request has finished. This is actually a great technique to prevent race-conditions. You can imagine the following snippet of code.

```php
<?php

if ($_SESSION['received_payment'] === false) {
  $_SESSION['received_payment] = true;
  sendMoney();
}
```
Running this code in parallel, and without locks could result in the sendMoney() being called multiple times! This is a race condition which is solved by locks. Remember, while PHP is single-threaded, you can achieve concurrency by running multiple processes in parallel, Apache or Nginx does this for you. The same trick is used by pm2 to parallelise node processes.

So there is no problem, right? Wrooong 🙂

The problem is that this pattern scales poorly with regards to the total time required to completely process all requests it received in parallel. The requests themselves are received in parallel, but due to the locking they are executed in sequence. This means that if you have 10 parallel ajax calls to process, and let say that each call takes 500ms to process, you will have to wait a total of 5 seconds until all the ajax requests have been resolved. Even worse is if the first call needs 4 seconds to complete, and the rest 9 call need 100ms each. You will still  end up waiting 5 seconds, but you will wait a full 4 seconds before seeing any results!
There is a great [demo](https://github.com/mattiasgeniar/demo-php-blocking-sessions) with which you can fiddle with. 

I also made my own experiment, here is with "slow" sessions https://github.com/krukru/php-session-quirks/blob/master/example_1/screenshots/with-sharding/sharding.gif
And here is what happens when sessions get closed as soon as they are opened. https://github.com/krukru/php-session-quirks/blob/master/example_0/screenshots/with-sharding-max-workers.png
There are also some other things to take into consideration, like the browser connection limit, and your web server concurrency settings - but these are beyond the scope of this post.


So how to mitigate this issue?

There are two viable solutions.

<b>The first solution is to close the session as soon as you are finished with reading session data.</b> Sessions are most often used just to determine if the user is logged or a guest. After that point the session is no longer needed (in most cases) and if you close the session early, you are allowing the next request to be processed concurrently.

The second solution is to use the read-only session flag, when you will only be doing “read” operations from the session. Again a good example is checking if the user is a guest or logged in user. Here you are only reading from the session, not writing anything – this has the nice property that there is no possibility for race conditions (since data is not being changed) and there is no need for locks!
This approach has its caveats. Read-only sessions are only supported from php 7, which is an  issue for frameworks who wish to support php 5 (looking at you [Yii2](https://github.com/yiisoft/yii2/blob/master/framework/composer.json#L66)). Another issue is that major frameworks like Zend, Symfony are slow to support this, see https://github.com/zendframework/zend-session/issues/39 and https://github.com/symfony/symfony/issues/24875

So your best bet is to just close-early and avoid sessions as much as possible 🙂

Remember, this only applies to ajax calls from the same user (the same PHPSESSID) and only if the session is being used (session_start() called anywhere in the script lifecycle)!

Ok, but what about multi-server architecture? Well, now you can no longer use “files” as your session handler, since a session could exist on one server instance, but not on another.

How you approach this issue is by using some shared memory space to manage your sessions, redis and memcached being the strongest candidates for the job.

Redis session handler does not support locks at all, and memcached has started supporting it with various degrees of success (there are bugs https://github.com/php-memcached-dev/php-memcached/issues/310).

This means that you <b>cannot</b> get that sweet sweet race-condition safety you get with “files” session handler. The trivial snippet with “received_payment” session gets very difficult to implement correctly.

The solution for this case, is unfortunately to change your code logic and use either a database for locking or specifically some locking mechanism (like  https://symfony.com/doc/current/components/lock.html), and again avoid sessions as much as possible.

How do you approach session management? How do sessions work in a node backend environment? Please share your thought and experiences in the comments below! :) 

And happy developing!

Links and resources:
* The original post that got me thinking about the issue https://pasztor.at/blog/stop-using-php-sessions/
* My attempt at recreating the various experiments https://github.com/krukru/php-session-quirks