In this post, I’m going to tell you how to optimize NGINX for High-Performance PHP Websites.
Introduction
PHP is the standard in web development. It is often run with the PHP-FPM engine to manage PHP processes.
NGINX is a robust web server that is often coupled with PHP-FPM. It works great under high load.
You may find that your PHP-based websites have performance issues in NGINX + PHP-FPM setup. In this article, I am going into details on how to tune NGINX in order to deliver the best results for PHP websites.
I will highlight common pitfalls and optimizing advice that applies to running PHP websites in NGINX, including WordPress.
The Real Challenge: PHP-FPM, Not NGINX
NGINX is often seen as the key to high-performance web hosting, and once you have performance issues in NGINX and PHP-FPM server, you rush into tuning NGINX. But let me tell you from the beginning what you already know:
NGINX is fast. You rarely need to tune it.
NGINX is so popular in web hosting for a reason. It works great and fast with any backend, whether it is PHP-FPM, Python, or anything else! And it does so because it requires minimal magical tweaks. The real magic lies in tuning PHP-FPM and caching. That is the cornerstone of high-performance websites.
Common PHP-FPM tuning pitfalls
Pitfall #1. Tuning without cache
I’ll start with the basics. The bare essential of high performance is caching.
Caching has a crucial role in PHP performance tuning. Efficient caching mechanisms like Redis for object and database caching, along with full-page caching, can significantly reduce the load on PHP.
If you don’t employ any kind of full page cache, chances are that all your tuning efforts will be fruitless.
So don’t overlook having your PHP use a full page cache. If none of your pages are cached, your website won’t have enough RAM, nor CPU resources, to handle substantial number of visitors; no tuning would help it to work faster.
So what to do in order to avoid this pitfall? Two things, really:
- Use a full page cache. The standard solution is Varnish Cache. Varnish works great with WordPress, it is straightforward to set up, easy to configure and is far more superior than any WordPress cache plugins. Because it is a software and not a script, it becomes part of your web stack, as it does not require PHP to be executed for serving cached pages, which makes cached requests lightning fast!
-
Use a data cache. This type of cache ensures that even on dynamic pages, heavy queries are cached. Redis cache works great with WordPress and is very easy to set up.
You must use both types of caches mentioned, there’s no “or” or choice situation here. Omitting any type of caching will yield your server eventually slow and unusable.
Pitall #2. Underestimating traffic and lack of proactive tuning
Many administrators fail to simulate enough connections during initial stress tests, leading to PHP-FPM crashing when current settings no longer match to existing traffic patterns. You must proactively tune your web server setup and re-evaluate your existing setups for security and performance reasons.
There is a formula for doing the proper match in regards to PHP-FPM settings. It will make your performance audits less frequent. I’ll share that formula below.
Pitfall #3. Misallocating resources
Allocating more RAM to PHP-FPM than available can lead to crashes, especially when non-cached pages are accessed.
Now that we’ve reviewed the most common pitfalls, let’s proceed and dive into PHP-FPM tuning optimization.
Understanding PHP-FPM Process Management
PHP-FPM can use one of three process management types:
- Static maintains a fixed number of child processes, ensuring immediate availability for handling requests. Ideal for high-traffic sites where response time is critical.
- Dynamic dynamically manages the number of child processes within specified limits, balancing resource use and availability. Suitable for moderately busy sites.
- Ondemand forks processes as requests are received. Best for low traffic sites where saving memory is a priority over immediate response.
Tuning PHP-FPM will seem so easy when I tell you that it’s all about choosing the right process management type out of the ones I mentioned, and then setting up the right number of child processes.
So basically only two main parameters!
PHP-FPM Tuning Steps
The importance of swap space in PHP-FPM tuning
Before you jump into tuning your PHP-FPM parameters, you need to “protect” your server with enough swap space.
Swap space acts as a protective virtual area for your RAM, when the physical RAM is fully taken by running programs, which happens either due to misconfiguration or usage spikes. If your server runs out of RAM, it starts using the swap space, which is significantly slower but can prevent crashes due to out-of-memory errors.
Tuning PHP-FPM, is essentially nothing but an attempt to make it being able to use as much RAM and CPU as possible out of the ones it has available, but not being greedy enough to take more than the physical RAM.
So before you proceed to tuning PHP-FPM, it’s wise to allocate swap space, especially during testing and stress-testing phases. This can provide a safety net against unexpected memory spikes and help you understand how your configuration behaves under memory pressure.
In simple words, allocate enough swap so that your server doesn’t crash.
Check your current configuration
The PHP-FPM settings are configured on a pool basis. You typically have one pool per website, and its configuration is located in a file with path like this: /etc/php-fpm.d/www.conf
. It is highly recommended to name your pools and corresponding filenames with the domain names of your websites. For example /etc/php-fpm.d/example.com.conf
and [example.com]
atop the file contents.
All the tuning directives discussed further are to be adjusted in the pool configuration files.
Set the process management type: static
is the way to go
The static process management is often recommended for high-traffic scenarios because it maintains a fixed number of child processes. This ensures immediate availability to handle requests and can lead to more stable CPU and memory usage. There’s no process forking and killing, things are just more stable.
There are resources that will tell you that for low-end VPS, ondemand
is the only good option. Well, it isn’t. The static
is suitable for any website, including low end server. You just have to do the right math for other settings.
So our objectively correct configuration for process management type is as sample as:
pm = static
That was easy. Let’s move on to the next big thing that is slightly more complicated.
Set the number of PHP-FPM workers
Now that we have defined our process management type, we need to tell PHP-FPM how many of its processes we want to keep on our server at all times.
Thanks to the static
process management, those processes will be ready to handle requests at any time. Those processes are called worker processes.
We want to have as many worker processes as possible, so that more visitors can be handled simultaneously. But having too many worker processes will result in exhausted RAM and start of swapping, which would cause performance issues. Thus, the right number of worker processes has to be calculated based on your server’s available memory and the average size of your PHP processes.
The number of worker processes for a PHP-FPM pool is defined by the pm.max_children
directive. Let’s review the basic principles on counting the proper value.
Formula for PHP Max Workers
To calculate pm.max_children
, consider both RAM and CPU of your server.
To fully utilize all CPU cores, you need to set pm.max_children
to at least their count.
For example, pm.max_children = 32
on a machine with a 32-core CPU, will ensure that all CPU units would be leveraged in a high load scenario, e.g. during traffic spikes.
However, the decisive factor for the proper value of this parameter is your available RAM.
I have put available RAM
in bold, because other residential programs will always need a portion of your physical RAM for their operation.
The common example being your database.
So the formula of our tuning is:
pm.max_children = (Total RAM in MB - Total RAM taken by other programs in MB) / (Average PHP worker RAM in MB)
How to get those individual values?
- The total server RAM is usually known by your hosting plan. If in doubt, you can always check it by
free -h
and look for the first displayed number. Convert it to megabytes for further calculations (unless it’s already displayed as megabytes). -
RAM taken by residential programs is more tricky to calculate. It’s fine to be rough on the numbers there. For simplicity, this should be the size of your database in megabytes. But database is stored on disk, you’d say? Yes, but with MySQL you should strive to have as much buffers (and thus, memory usage) as the database size itself. Yet, if you’re not sure, assume that the residential programs take around 20% of the total memory.
-
For average RAM per PHP process take a value of 128 MB. In reality there are techniques that you can use to calculate this metric tailored to your website. However, PHP-FPM worker processes tend to grow in size over time, depending on other parameters. Regardless, if you want to calculate this value yourself with the most ease, you have to make assumption that there is only 1 pool on the web server, as well as ensure that your PHP-FPM pool has been running a substantial amount of time. Use the following command:
ps -u example -o rss= -C php-fpm | awk '{sum+=$1; count++} END {if (count > 0) print (sum/count)/1024 " MB"; else print "No PHP-FPM processes found"}'
Here, example
has to be changed to the username of your PHP-FPM pool, which is defined by user =
directive of the pool configuration. Not sure what it’s all about? Have a read on PHP-FPM user permissions setup.
After doing all the math as per formula for pm.max_children
, ensure updating your PHP-FPM pool configuration with the right value, e.g. X
:
pm = static
pm.max_children = X
; comment the values of these directives
; pm.start_servers = ...
; pm.min_spare_servers = ...
; pm.max_spare_servers = ...
As you see, I commented out some directives as they don’t really apply with our chosen process management type, static
.
I also specifically did not put any concrete value in this example to prevent what other tutorials make you do – copy paste.
There’s never a single good value for pm.max_children
that fits all servers! You must always count it right based on your available RAM.
It might be 5
on some low end server and 50
for a middle end, but if you put the wrong value, your server will happily do only one thing for you – crash 🙂
You can verify your updated configuration for validity by running php-fpm -t
command.
Stress-test, fine-tune and monitor
To simulate traffic and adjust settings accordingly, perform stress testing.
You can choose a tool like wrk
for performing high traffic simulation. To install it, run:
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install wrk
Run the following test in terminal while observing your server (in another terminal session):
wrk -t12 -c400 -d30s https://www.example.com/
This runs a stress test for 30 seconds, using 12 threads, and keeping 400 HTTP connections open.
I recommend setting -t
parameter to the number of CPU cores on your server, in. your wrk
command.
Choose a -c
value that is higher than your pm.max_children
setting. This will simulate a scenario where the number of incoming requests exceeds the server’s capacity to handle concurrent PHP processes.
Upon running the test, check if your free RAM is depleted enough that your server started swapping. Run free -h
and look for used swap.
If the used swap has not sufficiently increased, it is safe to assume that you can increase pm.max_requests
a bit further, and thus utilize more memory and gain more performance. Use increments of 5
in your testing, or 10
if you have more RAM to play with.
Use tools NGINX Amplify to monitor the memory and CPU utilization of your PHP-FPM processes. Pay a special attention to swap usage; excessive swap usage can indicate a need to adjust your configuration and lower the value of pm.max_children
.
Iteratively tune your PHP-FPM configuration because it is never a one-time setup. It requires ongoing adjustments based on real-world usage patterns and server performance data.
Notes on pm.max_requests
You should be aware of the third rather critical parameter which is pm.max_requests
. It controls the number of requests a child process handles before it is rotated.
This is important. Even with the static process management, it is recommend to eventually refresh a worker process by starting it over again.
This will ensure that no memory leaks will hog your server’s RAM.
pm.max_requests = 10000
I would recommend to set this value to a large, but not an infinite number. We must ensure that each worker process is eventually recycled in order to free up memory. But we also have to balance it with too much unwanted recycling. Setting this to e.g. 10
would make it restarted on every 10th request (on a single core machine), so this is never a good value.
On a low end server I would set it to 500
for safety with experiments, while a heavily tested low-end server or a larger server would do fine with 10000
and more, provided you have more RAM room for experiments.
After adjusting this value it is critical to re-run stress-tests and fine tuning, as it affects the average memory size taken by a PHP-FPM worker.
Final Thoughts: NGINX and the Art of PHP Tuning
In essence, tuning a PHP-based website on NGINX doesn’t require anything extraordinary with NGINX itself. The focus should be on the PHP layer. NGINX, in its steadfast efficiency, complements a well-tuned PHP environment.
Remember, tuning PHP, especially PHP-FPM, is more an art than a strict science, blending technical know-how with a bit of guesswork and experimentation. The journey towards optimal performance is continual, requiring ongoing adjustments and monitoring.
Embrace the art of tuning PHP: it’s where the real performance gains lie. As the saying might go in the web hosting world, “Tune your PHP, and let NGINX do its magic.”