Optimizing PHP Application Concurrency
Last updated November 13, 2020
Table of Contents
FPM spawns and manages child processes that execute the actual PHP application code. Each of these processes handles one request from the web server at a time, meaning that more processes will yield greater concurrency and thus better application performance under higher traffic conditions.
The configured PHP memory limit applies to each child process that runs the application, which may consume memory up to that limit before it gets terminated.
Default settings and behavior
PHP-FPM is configured to run using the
static process managing mode, meaning it spawns a fixed number of child processes. This is the optimal configuration for environments like Heroku, where dyno instance are fully isolated and have a fixed RAM allocation.
The number of child processes to spawn (controlled by the
pm.max_children setting of PHP-FPM) is determined using the
WEB_CONCURRENCY environment variable, which is automatically set to a suitable value by Heroku.
If your application uses multiple buildpacks, you must ensure that the PHP buildpack (as the primary language buildpack of your application) is executed after other language buildpacks; otherwise, the
WEB_CONCURRENCY defaults of a buildpack that runs after the PHP buildpack may apply to your application.
For example, when using the
heroku/nodejs buildpack together with
WEB_CONCURRENCY defaults will be incorrect if the
heroku/nodejs buildpack runs after the
When booting an application, the dyno type will automatically be detected, and the
WEB_CONCURRENCY environment variable will be set to the amount of available RAM on the dyno divided by the memory limit configured for each PHP process:
$ heroku ps:scale web=1:standard-2x $ heroku logs 2020-02-06T14:52:40… heroku[web.1]: State changed from down to starting 2020-02-06T14:52:42… heroku[web.1]: Starting process with command `heroku-php-apache2` 2020-02-06T14:52:43… app[web.1]: Detected 1073741824 Bytes of RAM 2020-02-06T14:52:43… app[web.1]: PHP memory_limit is 128M Bytes 2020-02-06T14:52:43… app[web.1]: Starting php-fpm with 8 workers... 2020-02-06T14:52:43… app[web.1]: Starting httpd... 2020-02-06T14:52:44… heroku[web.1]: State changed from starting to up
memory_limit on Heroku is the default for the respective PHP version; currently at 128 MB for all versions of PHP.
This means that out of the box, the following settings apply:
|Dyno Type||Available RAM||PHP |
|free, hobby, standard-1x||512 MB||128 MB||4|
|standard-2x||1024 MB||128 MB||8|
|performance-m||2.5 GB||128 MB||20|
|performance-l||14 GB||128 MB||1121|
For backwards compatibility, the defaults for the
performance-l dyno type do not utilize the entire amount of memory available for PHP versions before 7.4. If you would like to use a higher number of processes than Heroku assigns automatically, use the technique outlined in the Tuning Concurrency Manually section of this document.
These defaults are intentionally chosen to not leave any “headroom” for the PHP-FPM master process or the web server processes because applications are extremely unlikely to consume their entire memory limit on each request and at full saturation, meaning that dynos are slightly over-subscribed by default.
To increase the number of child processes (and thus the number of requests that may be handled concurrently), you simply lower an application’s
memory_limit setting - the memory limit is the primary method of adjusting PHP application concurrency on Heroku.
Tuning concurrency using
memory_limit for PHP-FPM
Setting memory limit via
.user.ini config file containing a memory limit setting may be placed into your application’s document root, which by default is the top level directory of your application. For example, to set a memory limit of 64 MB for an application, your
.user.ini would have the following contents:
memory_limit = 64M
Pay close attention to the correct shorthand notation required by PHP, used here to indicate megabytes using the “
Your application’s document root may be different from the top level directory of your application if you’ve configured it using a
Procfile command argument.
If you deploy your app with this file and watch your application restart, you’ll then notice that the number of workers was adjusted automatically for the given memory limit, e.g. for a standard-1x dyno:
$ heroku logs 2019-01-15T07:51:24.476056+00:00 heroku[web.1]: State changed from down to starting 2019-01-15T07:51:30.765076+00:00 heroku[web.1]: Starting process with command `heroku-php-apache2` 2019-01-15T07:51:33.188816+00:00 app[web.1]: Optimizing defaults for 1X dyno... 2019-01-15T07:51:33.370674+00:00 app[web.1]: 8 processes at 64MB memory limit. 2019-01-15T07:51:33.414407+00:00 app[web.1]: Starting php-fpm... 2019-01-15T07:51:33.414423+00:00 app[web.1]: Starting httpd... 2019-01-15T07:51:35.865579+00:00 heroku[web.1]: State changed from starting to up
.user.ini files in sub-directories of the document root will not be evaluated when Heroku determines the memory limit at dyno boot time, but take effect at runtime as documented when serving requests to PHP files in such directories, so keep this in mind in the unlikely (and absolutely not recommended) case you have different
memory_limit settings for different sub-directories of your application.
Setting the memory limit using PHP-FPM configuration
Instead of a
.user.ini file, you may also use a PHP-FPM config include to add a
php_admin_value directive to change the
memory_limit setting. For example, to set a memory limit of 64 MB for an application, you would create a file named e.g.
fpm_custom.conf with the following contents:
php_value[memory_limit] = 64M
For these settings to take effect, you need to use the
-F option in your
Procfile command so the config gets loaded:
web: heroku-php-apache2 -F fpm_custom.conf
If you deploy your app with the new
fpm_custom.conf and the changed
Procfile and watch your application restart, you’ll notice that the number of workers was adjusted automatically for the given memory limit, e.g. for a standard-1x dyno:
$ heroku logs 2019-01-15T07:51:24.476056+00:00 heroku[web.1]: State changed from down to starting 2019-01-15T07:51:30.765076+00:00 heroku[web.1]: Starting process with command `heroku-php-apache2 -F fpm_custom.conf` 2019-01-15T07:51:33.109122+00:00 app[web.1]: Using PHP-FPM configuration include 'fpm_custom.conf' 2019-01-15T07:51:33.188816+00:00 app[web.1]: Optimizing defaults for 1X dyno... 2019-01-15T07:51:33.370674+00:00 app[web.1]: 8 processes at 64MB memory limit. 2019-01-15T07:51:33.414407+00:00 app[web.1]: Starting php-fpm... 2019-01-15T07:51:33.414423+00:00 app[web.1]: Starting httpd... 2019-01-15T07:51:35.865579+00:00 heroku[web.1]: State changed from starting to up
Runtime changes of
Any change made to the memory limit at runtime using
ini_set("memory_limit", ...) will not affect the concurrency, as the memory limit used for calculation is determined at boot time.
If you have many processes increasing the memory limit beyond its initially configured value at runtime using
ini_set(), and these processes are actually consuming that additional memory, you may get R14 errors indicating that your application has started paging to disk, which may degrade performance. In this case, either increase the static
memory_limit, or set
WEB_CONCURRENCY manually to a lower value.
In many cases, it may be desirable to have a lower memory limit to achieve higher concurrency, and using
ini_set() to dynamically set a higher limit at runtime for only the few code paths in your application that temporarily require a higher memory limit.
Determining a suitable memory limit
The amount of memory your application needs depends on the amount of data it processes during a request, and how much of that data it holds in memory at the same time. Tasks like image processing or handling large database result sets are typically quite memory intensive.
The default memory limit of 128 MB in PHP is a conservative default intended to give enough “breathing room” for virtually any kind of application. It is likely that your code does not consume that much memory during a request, so lowering the limit is a great way of optimizing your application performance.
Once you’ve determined what the maximum memory usage of your application is, don’t forget to leave a reasonable amount of “headroom” when setting a limit to allow for a safety margin for growth and unforeseen circumstances (e.g. growing data sets over time).
Measuring and optimizing memory usage locally
On your development machine, you can use the
memory_get_peak_usage() function in your code (usually towards the very end of a script) to determine the peak memory used. You could, for instance,
file_put_contents("path/to/logfile", memory_get_peak_usage()."\n", FILE_APPEND); at the end of your code, run a load/feature test on it, and determine the highest value in that log file e.g. using
sort path/to/logfile | tail -n 1.
Another quick and easy approach is lowering the memory limit configured in PHP in subsequent steps (of e.g. 16 MB) until you start getting “memory limit exceeded” error messages. A limit of 64 MB is often a safe bet and will result in twice as many worker processes compared to the default configuration.
When performing load tests locally (using
httperf or similar), you may also observe the amount of memory consumed by your
php-fpm processes using
Remember to use realistic conditions when performing tests locally, e.g. using suitable large result sets from a database. It’s also highly recommended to audit your code for any functions, loops or algorithms that may scale unfavorably with an increase in input data size and optimize these accordingly.
Measuring memory usage on Heroku
Platform specific nuances aside, an application’s memory consumption, given the same input, should be virtually identical between your local development environment and Heroku, so it’s recommended to work on finding the optimal memory limit in development first.
The log-runtime-metrics Heroku Labs functionality will periodically report memory consumption to the
heroku logs stream. When performing load tests, a memory usage that’s drastically lower than what’s available for the respective dyno type may be an indicator that your memory limit is set too high. Try lowering the limit to increase concurrency and thus actual memory usage.
You may also inspect a basic memory usage report and graph in the overview section for your app on the Heroku Dashboard. Note that this displays an average value across all running dynos.
Application performance monitoring tools such as New Relic will record memory usage and report them for your analysis.
Heroku Pipelines help automate a workflow for promoting deployments from one environment to another (e.g., from staging to production). This makes it easy to measure and optimize changes to memory usage on a non-production app that you can then promote to production when the changes are ready.
Tuning concurrency manually
If you’d like to manually set the number of child processes running your application, you can adjust the
WEB_CONCURRENCY environment variable by setting a config var.
For instance, to statically set the number of child processes to 8, use
$ heroku config:set WEB_CONCURRENCY=8
WEB_CONCURRENCY manually, make sure that its value multiplied by your
memory_limit does not exceed the amount of RAM available on your dyno type.
This will cause your application to restart, and your dyno(s) will report the static setting during startup:
$ heroku logs 2019-01-15T07:51:24.476056+00:00 heroku[web.1]: State changed from down to starting 2019-01-15T07:51:30.765076+00:00 heroku[web.1]: Starting process with command `heroku-php-apache2 -F fpm_custom.conf` 2019-01-15T07:51:33.109122+00:00 app[web.1]: Using PHP-FPM configuration include 'fpm_custom.conf' 2019-01-15T07:51:33.370674+00:00 app[web.1]: Using WEB_CONCURRENCY=8 processes. 2019-01-15T07:51:33.414407+00:00 app[web.1]: Starting php-fpm... 2019-01-15T07:51:33.414423+00:00 app[web.1]: Starting httpd... 2019-01-15T07:51:35.865579+00:00 heroku[web.1]: State changed from starting to up
If you set
WEB_CONCURRENCY to a fixed value, you may have to adjust it when you scale to a different dyno type to take into account the different amount of available RAM on the new dyno type.