PHP is everywhere and is arguably the language most widely deployed on the Internet Web.

However, it’s not exactly known for its high performance capabilities, especially when it comes to highly concurrent systems. And that’s the reason that for such specialized use cases, languages such as Node (yes, I know, it’s not a language), Go and Elixir are taking over.

That said, there’s a LOT you can do to improve the PHP performance on your server. This article focuses on the php-fpm side of things, which is the natural way to configure on your server if you’re using Nginx.

In case you know what php-fpm is, feel free to jump to the section on optimization.

What is php-fpm?

Not many developers are interested in the DevOps side of things, and even among those who do, very few know what’s going on under the hood. Interestingly, when the browser sends a request to a server running PHP, it’s not PHP that forms the point the first contact; instead, it’s the HTTP server, the major ones of which are Apache and Nginx. These “web servers” then have to decide how to connect to PHP, and pass on the request type, data, and headers to it.

The request-response cycle in the case of PHP (Image credit: ProinerTech)

In modern PHP applications, the “find file” part above is the index.php, which the server is configured to delegate all requests to.

Now, how exactly the web server connects to PHP has evolved, and this article would explode in length if we were to get into all the nitty-gritty. But roughly speaking, during the time that Apache dominated as the web server of choice, PHP was a module included inside the server.

So, whenever a request was received, the server would start a new process, which will automatically include PHP, and get it executed. This method was called mod_php, short for “php as a module.” This approach had its limitations, which Nginx overcame with php-fpm.

In php-fpm the responsibility of managing PHP, processes lie with the PHP program within the server. In other words, the web server (Nginx, in our case), doesn’t care about where PHP is and how it is loaded, as long as it knows how to send and receive data from it. If you want, you can think of PHP in this case as another server in itself, which manages some child PHP processes for incoming requests (so, we have the request reaching a server, which was received by a server and passed on to a server — pretty crazy! :-P).

If you’ve done any Nginx setups, or even just pried into them, you’ll come across something like this:

     location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php7.2-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

The line we’re interested in is this: fastcgi_pass unix:/run/php/php7.2-fpm.sock;, which tells Nginx to communicate with the PHP process through the socket named php7.2-fpm.sock. So, for every incoming request, Nginx writes data through this file, and on receiving the output, sends it back to the browser.

Once again, I must emphasize that this isn’t the most complete or most accurate picture of what goes on, but it’s entirely accurate for most DevOps tasks.

With that aside, let’s recap what we’ve learned so far:

  • PHP doesn’t directly receive requests sent by browsers. Web servers like Nginx first intercept these.
  • The web server knows how to connect to the PHP process, and passes on all the request data (literally pastes everything over) to PHP.
  • When PHP is finished doing its part, it sends the response back to the web server, which sends it back to the client (or browser, in most cases).

Or graphically:

How PHP and Nginx work together (Image credit: DataDog)

Great so far, but now comes the million-dollar question: what exactly is PHP-FPM?

The “FPM” part in PHP stands for “Fast Process Manager”, which is just a fancy way of saying that the PHP running on a server isn’t a single process, but rather some PHP processes that are spawned, controller, and killed off by this FPM process manager. It is this process manager that the web server passes the requests to.

The PHP-FPM is an entire rabbit hole in itself, so feel free to explore if you wish, but for our purposes, this much explanation will do. 🙂

Why optimize php-fpm?

So why worry about all this dance when things are working all right? Why not just leave things as they are.

Ironically, that is precisely the advice I give for most use cases. If your setup is working fine and doesn’t have extraordinary use cases, use the defaults. However, if you’re looking to scale beyond a single machine, then squeezing out the max from one is essential as it can cut down the server bills in half (or even more!).

Another thing to realize is that Nginx was built for handling huge workloads. It’s capable of handling thousands of connection at the same time, but if the same isn’t true of your PHP setup, you’re just going to waste resources as Nginx will have to wait for PHP to finish with the current process and accept the next, conclusively negative any advantages that Nginx was built to provide!

So, with that out of the way, let’s look at what exactly we’d change when trying to optimize php-fpm.

How to optimize PHP-FPM?

The configuration file location for php-fpm may differ on the server, so you’ll need to do some research for locating it. You can use find command if on UNIX. On my Ubuntu, the path is /etc/php/7.2/fpm/php-fpm.conf. The 7.2 is, of course, the version of PHP that I’m running.

Here’s what the first few lines of this file look like:

 ;;;;;;;;;;;;;;;;;;;;;
; FPM Configuration ;
;;;;;;;;;;;;;;;;;;;;;

; All relative paths in this configuration file are relative to PHP's install
; prefix (/usr). This prefix can be dynamically changed by using the
; '-p' argument from the command line.

;;;;;;;;;;;;;;;;;;
; Global Options ;
;;;;;;;;;;;;;;;;;;

[global]
; Pid file
; Note: the default prefix is /var
; Default Value: none
pid = /run/php/php7.2-fpm.pid

; Error log file
; If it's set to "syslog", log is sent to syslogd instead of being written
; into a local file.
; Note: the default prefix is /var
; Default Value: log/php-fpm.log
error_log = /var/log/php7.2-fpm.log

A few things should be immediately obvious: the line pid = /run/php/php7.2-fpm.pid tells us which file contains the process id of the php-fpm process.

We also see that /var/log/php7.2-fpm.log is where php-fpm is going to store its logs.

Inside this file, add three more variables like this:

emergency_restart_threshold 10
emergency_restart_interval 1m
process_control_timeout 10s

The first two settings are cautionary and are telling the php-fpm process that if ten child processes fail within a minute, the main php-fpm process should restart itself.

This might not sound robust, but PHP is a short-lived process that does leak memory, so restarting the main process in cases of high failure can solve a lot of problems.

The third option, process_control_timeout, tells the child processes to wait for this much time before executing the signal received from the parent process. This is useful in cases where the child processes are in the middle of something when the parent processes send a KILL signal, for example. With ten seconds on hand, they’ll have a better chance of finishing their tasks and exiting gracefully.

Surprisingly, this isn’t the meat of php-fpm configuration! That’s because for serving web requests, the php-fpm creates a new pool of processes, which will have a separate configuration. In my case, the pool name turned out to be www and the file I wanted to edit was /etc/php/7.2/fpm/pool.d/www.conf.

Let’s see what this file starts like:

; Start a new pool named 'www'.
; the variable $pool can be used in any directive and will be replaced by the
; pool name ('www' here)
[www]

; Per pool prefix
; It only applies on the following directives:
; - 'access.log'
; - 'slowlog'
; - 'listen' (unixsocket)
; - 'chroot'
; - 'chdir'
; - 'php_values'
; - 'php_admin_values'
; When not set, the global prefix (or /usr) applies instead.
; Note: This directive can also be relative to the global prefix.
; Default Value: none
;prefix = /path/to/pools/$pool

; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
;       will be used.
user = www-data
group = www-data

A quick look at the end of the snippet above solves the riddle of why the server process runs as www-data. If you’ve come across file permission issues when setting up your website, you’ve likely changed the owner or group of the directory to www-data, thus allowing the PHP process to be able to write into log files and upload documents, etc.

Finally, we arrive at the source of the matter, the process manager (pm) setting. Generally, you’ll see the defaults as something like this:

pm = dynamic
pm.max_children = 5
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 200

So, what does “dynamic” here mean? I think the official docs best explain this (I mean, this should already be part of the file you’re editing, but I’ve reproduced it here just in case it isn’t):

 ; Choose how the process manager will control the number of child processes.
; Possible Values:
;   static  - a fixed number (pm.max_children) of child processes;
;   dynamic - the number of child processes are set dynamically based on the
;             following directives. With this process management, there will be
;             always at least 1 children.
;             pm.max_children      - the maximum number of children that can
;                                    be alive at the same time.
;             pm.start_servers     - the number of children created on startup.
;             pm.min_spare_servers - the minimum number of children in 'idle'
;                                    state (waiting to process). If the number
;                                    of 'idle' processes is less than this
;                                    number then some children will be created.
;             pm.max_spare_servers - the maximum number of children in 'idle'
;                                    state (waiting to process). If the number
;                                    of 'idle' processes is greater than this
;                                    number then some children will be killed.
;  ondemand - no children are created at startup. Children will be forked when
;             new requests will connect. The following parameter are used:
;             pm.max_children           - the maximum number of children that
;                                         can be alive at the same time.
;             pm.process_idle_timeout   - The number of seconds after which
;                                         an idle process will be killed.
; Note: This value is mandatory.

So, we see that there are three possible values:

  • Static: A fixed number of PHP processes will be maintained no matter what.
  • Dynamic: We get to specify the minimum and a maximum number of processes that php-fpm will keep alive at any given point in time.
  • ondemand: Processes are created and destroyed, well, on demand.

So, how do these settings matter?

In simple terms, if you have a website with low traffic, the “dynamic” setting is a waste of resources most of the time. Assuming that you have pm.min_spare_servers set to 3, three PHP processes will be created and maintained even when there’s no traffic on the website. In such cases, “ondemand” is a better option, letting the system decide when to launch new processes.

On the other hand, websites that handle large amounts of traffic or must respond quickly will get punished in this setting. Creating a new PHP process, making it part of a pool, and monitoring it, is extra overhead that is best avoided.

Using pm = static fixes the number of child processes, letting maximum system resources to be used in serving the requests rather than managing PHP. If you do go this route, beware that it has its guidelines and pitfalls. A rather dense but highly useful article about it is here.

Final words

Since articles on web performance can spark wars or serve to confuse people, I feel that a few words are in order before we close this article. Performance tuning is as much about guesswork and dark arts as it is system knowledge.

Even if you know all the php-fpm settings by heart, success isn’t guaranteed. If you had no clue about the existence of php-fpm, then you don’t need to waste time worrying about it. Just keep doing what you’re already doing and carry on.

At the same time, avoid the end of being a performance junkie. Yes, you can get an even better performance by recompiling PHP from scratch and removing all the modules you won’t be using, but this approach isn’t sane enough in production environments. The whole idea of optimizing something is to take a look at whether your needs differ from the defaults (which they seldom do!), and make minor changes as needed.

If you are not ready to spend time on optimizing your PHP servers, then you may consider leveraging a reliable platform like Kinsta who take care of performance optimization and security.