PHP frameworks test
Tags: php, laravel, symfony
Published: 2019-12-14 12:21:47

For a long time, I made all my side projects with a custom-built PHP framework (or rather a set of libraries). At the beginning of my PHP journey (in 2011), writing these libraries was a good exercise in learning the language.
Recently, though, I started to run into all kinds of limitations:
- security concerns
- a constant need of patching for edge cases
- building simple functionality instead of focusing on building a product

The only frameworks I worked with before were Zend Framework 1 and Symfony 5.

With my previous project, I had a first-hand experience of running a relatively high load web site. The highest traffic level I saw was 10 million page views a day and up to 10,000 concurrent sessions. At the backend, we had 7 web nodes serving PHP requests (Linux, Nginx+php-fpm, MySQL, MongoDB, PHP7, Memcache). And being stuck with Zend Framework 1 was not fun :)

The other one, Symfony 5, I tried for a small project for a month and left it with a feeling of too much magic happening behind the scene for my taste. And the learning curve was pretty steep.

So I decided to take a look at the current scene of PHP frameworks to get the feel of everyone and test the performance (in case I would be lucky enough to get high traffic again). 

Here is <a href="https://pixelbot.co/php-frameworks-list/">the list</a> I chose what to test from.

##The goal
I want to answer the following questions:
1. How many requests can a framework serve consistently on a cheap VPS?
2. What is the performance overhead of using a framework comparing to plain PHP or a self-written set of libraries or even plain HTML?

##The results
How many simple HTTP-requests can a framework consistently serve on a comparatively cheap Digital Ocean droplet (40$, dedicated CPU, 2 cores, 4Gb of memory)?

<img title="PHP frameworks test: requests per second" src="https://pixelbot.co/img/php-frameworks-test-requests-per-second.png" class="img-fluid">

<table class="table table-sm"><caption>“Hello world!” test</caption><thead><tr><th scope="col">Name</th><th scope="col" class="num">Requests<br>per second</th><th scope="col" class="num">Requests<br>per day</th><th scope="col" class="num">Latency<br>ms</th></tr></thead><tbody><tr></tr></tbody><tbody><tr><th scope="row">HTML <em></em></th><td class="num">11,367</td><td class="num">982,127,520</td><td class="num">77</td></tr><tr><th scope="row">PHP <em>7.2.24</em></th><td class="num">2,597</td><td class="num">224,406,720</td><td class="num">77</td></tr><tr><th scope="row">Fatfree <em>3.7.0</em></th><td class="num">2,113</td><td class="num">182,544,480</td><td class="num">78</td></tr><tr><th scope="row">Phalcon <em>3.4.5</em></th><td class="num">2,111</td><td class="num">182,409,120</td><td class="num">78</td></tr><tr><th scope="row">Slim <em>4.3.0</em></th><td class="num">1,542</td><td class="num">133,248,960</td><td class="num">79</td></tr><tr><th scope="row">Yii <em>2.0.26</em></th><td class="num">1,250</td><td class="num">108,002,880</td><td class="num">79</td></tr><tr><th scope="row">Slim (skeleton) <em>4.3.0</em></th><td class="num">1,161</td><td class="num">100,347,840</td><td class="num">80</td></tr><tr><th scope="row">Symfony <em>5.0.1</em></th><td class="num">402</td><td class="num">34,727,040</td><td class="num">82</td></tr><tr><th scope="row">Laravel <em>5.8.35</em></th><td class="num">329</td><td class="num">28,455,840</td><td class="num">83</td></tr><tr><th scope="row">Laravel <em>6.5.2</em></th><td class="num">318</td><td class="num">27,502,560</td><td class="num">85</td></tr><tr><th scope="row">CakePHP <em>3.8.6</em></th><td class="num">185</td><td class="num">15,973,920</td><td class="num">83</td></tr><tr><th scope="row">CodeIgniter <em>4.0.0-rc3 </em></th><td class="num">173</td><td class="num">14,906,880</td><td class="num">89</td></tr><tr><th scope="row">Zend (skeleton) <em>3.1.13</em></th><td class="num">108</td><td class="num">9,365,760</td><td class="num">87</td></tr></tbody></table>


##The setup
- 2 servers: “Web” (SF1 region) and “Client” (NYC1 region)
- Nginx 1.14.0, PHP 7.2.24 (Php-fpm), Ubuntu 18.04.3. 
- Both servers accept connections only from each other via HTTP
- The “Client” server opens a connection to the “Web” server using wrk (v4.1.0), adding 1 more connection every 10 seconds.
- The “Web” server PHP usage is limited to 50% (more about it later)

##The rules
Testing PHP frameworks is complicated because there is much ground to cover: template engines, ORMs, caching... For now, I am choosing to test a simple “Hello world!” response with PHP. I’m assuming it is an ideal setup for testing performance overhead of various frameworks - a way to test the cost of a framework “takeoff.”

- The test-case is a simple plain text “Hello world!” response
- So no template engines
- No ORMs
- No controllers (if possible)
- No distinctions between frameworks level of complexity (micro, full-stack)
- I try to optimize every framework according to its “Deployment” docs section, if any. Usually, it is running “composer install --optimize-autoloader --no-dev” and running frameworks command for config caching

I’m interested in how many HTTP-requests a framework can consistently serve:
- Without a significant latency increase (<10%) 
- Without errors (timeouts, connection errors)
- Using not more than 50% of the server CPU

##A note about frameworks performance, CPU, memory
An unintuitive fact about frameworks is that every one of them can serve a roughly equal number of requests with roughly the same latency.

Let me repeat that: in an ideal world, with an infinite amount of computing resources, all frameworks perform almost equally.

It is not that one framework is inherently slower than the other. Some frameworks are slower just because they consume more CPU or more memory or make more reads from a disk or a database.

From my experience with high-traffic websites, when we deal with running php-fpm+Nginx, it is usually CPU we should be worried about, not memory. If we take an example configuration of 1 CPU core and 4GB of memory, php-fpm+Nginx can easily consume 100% CPU with less than 5% of memory usage.

That is why I choose a 40$ Digital Ocean droplet: it is the cheapest one with a dedicated, not shared CPU.

In real life, we do not want to max out or CPUs. It leads to an increase in server response time (latency), which is terrible. And it does not leave us any headroom for traffic spikes, which is wrong. Usually, for production web-nodes, it is a good idea to stay well below 50% of CPU limit. 

For this test, I decided to limit PHP usage on the “Web” server to 50%. 

##The attitude
I do not imply that frameworks that perform slower are worse:
- As the results show, if you serve less than 10,000,000 pageviews a day, you are good to go with any framework.
- It is often a tradeoff: the faster to build something on a framework, the more syntactic sugar, the more features, the more complexity - the slower it is.

##Observations

###Laravel
Laravel is the only framework, which was ridiculously slow and error-prone out of the box. I could not get it to serve even 10 requests per second without multiple timeout errors and latency instantly spiking over 100 ms.

Then I looked at the slow log and discovered that Laravel had some troubles with sessions handling (of course, session folders had all the necessary rights and user/group assigned to them).

After changing config values for SESSION_DRIVER from “file” to “array,” Laravel performance increased 10x.

Maybe I missed something important there, so please let me know if that is the case.

###Phalcon
Phalcon is a beast. It is the only framework that comes as Zephir/C extension. And in some cases, it is faster than plain PHP.

###FatFree
The only framework I could not make to accept requests with a trailing slash (like http://domain.com/fatfree/). Only http://domain.com/fatfree worked.