A friend of mine was having an interesting problem with his server lately. He has an Ubuntu box running a PHP web app being served by Apache. This web app was built using PSR.

Note: For anyone who isn't familiar, "PHP Standard Recommendations" (PSR) is an open specification created by the PHP Framework Interop Group to create a common technical basis for the implementation of interoperable components using best programming practices. I highly recommend anyone programming in PHP to familiarize themselves with it.

The particular issue came up in his implementation of PSR-7 (the module specifying HTTP message interfaces.) At a high level, a user should be able to go to a URL, and his app would request the file on disk and stream it to the client. Instead of gzipping these files on-the-fly, the files were already stored as gzip files on disk. All he needed to do was set the correct headers and stream the file contents.

This worked well until he started trying to download large files. If a file was still downloading to the client, any subsequent requests the client would make to the server (for example, in another browser tab) would hang or timeout until the download was completed.

In the apache logs, he found the following two errors worth noting:

[proxy_fcgi:error] [pid 28889] (70007)The timeout specified has expired: [client ***:***] AH01075: Error dispatching request to :***: (polling)
WARNING: [pool ***] server reached pm.max_children setting (5), consider raising it

Adjusting the value of pm.max_children did not fix his problem, of course. After a bit of troubleshooting, I had him put this small piece of code right before his streaming object to resolve the problem:

session_write_close();

Magic! 😏

Why does this work?

By default, PHP writes session data to disk and tries to claim an exclusive resource lock per request. For transient requests this isn't going to be much of an issue; but when he started streaming larger files, the session lock was being held open for the entire stream (queuing any subsequent access to that session file until the latest lock is released.) Using session_write_close(), we can manually close the current session handler (without destroying the session) and release the lock file.

In this scenario, there is no need to read or write session data for a download request once his streams started. As a result, we can close the session before the streaming starts, making the session lock transient again.

Alternatively, he could have also pushed sessions out to a proper caching layer (like redis, memcached, etc.) but that would have been significantly more work, and this worked just fine for his use case.