Optimizing PHP Application Concurrency
Last updated March 28, 2024
Table of Contents
PHP applications on Heroku run under the PHP-FPM FastCGI Process Manager and communicate with the Apache or Nginx web servers using the FastCGI protocol.
PHP-FPM spawns and manages child processes that execute the actual PHP application code. Each of these processes handle one request from the web server at a time. More processes yield greater concurrency and better application performance under higher traffic conditions.
On Heroku, PHP-FPM uses the static
process managing mode, which spawns a fixed number of child processes. This configuration is optimal for environments like Heroku, where dyno instances are fully isolated and have a fixed RAM allocation.
The configured PHP memory limit applies to each child process that runs the application, which can consume memory up to that limit before it gets terminated. Configuring the memory limit is the primary method of adjusting PHP application concurrency on Heroku.
Heroku Enterprise customers with Premier or Signature Success Plans can request in-depth guidance on this topic from the Customer Solutions Architecture (CSA) team. Learn more about Expert Coaching Sessions here or contact your Salesforce account executive.
Default Settings and Behavior
If your application uses multiple buildpacks, ensure that the PHP buildpack, as the primary language buildpack of your application, executes after other language buildpacks. Otherwise, the WEB_CONCURRENCY
defaults of the other buildpack can overwrite the value previously set by the PHP buildpack.
The number of child processes spawned is controlled by the pm.max_children
setting of PHP-FPM. That setting is determined using the WEB_CONCURRENCY
environment variable that Heroku’s PHP buildpack automatically sets to a suitable value.
When booting an application, we automatically detect the dyno type and set the WEB_CONCURRENCY
environment variable to an appropriate default value.
$ heroku ps:scale web=1:standard-2x
$ heroku logs
2024-02-06T14:52:40… heroku[web.1]: State changed from down to starting
2024-02-06T14:52:42… heroku[web.1]: Starting process with command `heroku-php-apache2`
2024-02-06T14:52:43… app[web.1]: Available RAM is 1G Bytes
2024-02-06T14:52:43… app[web.1]: PHP memory_limit is 128M Bytes
2024-02-06T14:52:43… app[web.1]: Starting php-fpm with 8 workers...
2024-02-06T14:52:43… app[web.1]: Starting httpd...
2024-02-06T14:52:44… heroku[web.1]: State changed from starting to up
WEB_CONCURRENCY Defaults by Dyno Type
The default value for memory_limit
is 128M
in all currently supported versions of PHP, but you can configure a different value. This table outlines the WEB_CONCURRENCY
defaults Heroku sets for various dyno types at three different PHP memory_limit
values:
Dyno Type | Dyno RAM | CPU cores | WEB_CONCURRENCY for memory_limit of… |
||
---|---|---|---|---|---|
64M |
128M 1 |
256M |
|||
Eco Basic Standard-1X |
512 MB | shared | 8 | 4 | 2 |
Standard-2X | 1 GB | shared | 16 | 8 | 4 |
Private-S Shield-S |
1 GB | 2 | 16 | 8 | 4 |
Performance-M Private-M Shield-M |
2.5 GB | 2 | 40 | 20 | 10 |
Performance-L Private-L Shield-L |
14 GB | 8 | 224 962 |
112 482 |
56 242 |
Performance-L-RAM Private-L-RAM Shield-L-RAM |
30 GB | 4 | 128 | 64 | 32 |
Performance-XL Private-XL Shield-XL |
62 GB | 8 | 288 | 144 | 72 |
Performance-2XL Private-2XL Shield-2XL |
126 GB | 16 | 640 | 320 | 160 |
1: The default value for memory_limit is 128M in all currently supported versions of PHP.2: On Performance-L dynos, WEB_CONCURRENCY defaults to lower values for PHP versions before 7.4 for backwards compatibility.
|
For backwards compatibility, the defaults for the performance-l
dyno type don’t use the entire amount of memory available for PHP versions before 7.4. If you want to use more processes than Heroku automatically assigns, see Tuning Concurrency Manually.
These defaults are intentionally chosen to not leave any “headroom” for the PHP-FPM parent process or the web server processes. Applications are unlikely to consume their entire memory limit on each request and at full saturation, so dynos are slightly over-subscribed by default.
WEB_CONCURRENCY Default Calculations
To compute a WEB_CONCURRENCY
value that doesn’t exceed the available RAM nor spawn too many PHP-FPM worker processes per available CPU core, Heroku calculates:
- A RAM-based limit for
WEB_CONCURRENCY
using the PHPmemory_limit
and available RAM (the RAM-based limit) - A CPU-based limit for
WEB_CONCURRENCY
using the PHPmemory_limit
, available RAM, and a logarithmic scaling factor that includes the CPU core count
Heroku uses the lower of these two limits as the value for WEB_CONCURRENCY
. In all cases, the configured PHP memory_limit
is determined automatically.
The default memory_limit
on Heroku is the default for the respective PHP version. The limit is currently 128 MB for all versions of PHP.
If the CPU-based limit overrides the RAM-based limit, a message emits during startup:
$ heroku ps:scale web=1:performance-xl
$ heroku logs
2024-02-06T14:52:40… heroku[web.1]: State changed from down to starting
2024-02-06T14:52:42… heroku[web.1]: Starting process with command `heroku-php-apache2`
2024-02-06T14:52:43… app[web.1]: Available RAM is 62G Bytes
2024-02-06T14:52:43… app[web.1]: PHP memory_limit is 128M Bytes
2024-02-06T14:52:43… app[web.1]: Maximum number of workers that fit available RAM at memory_limit is 496
2024-02-06T14:52:43… app[web.1]: Limiting number of workers to 144
2024-02-06T14:52:43… app[web.1]: Starting php-fpm with 144 workers...
2024-02-06T14:52:43… app[web.1]: Starting httpd...
2024-02-06T14:52:44… heroku[web.1]: State changed from starting to up
Regardless of which calculation is used for WEB_CONCURRENCY
, there’s a linear correlation between the PHP memory_limit
and the number of PHP-FPM child processes. For example, halving the memory_limit
value doubles the WEB_CONCURRENCY
result, and vice versa, as shown in the table further above.
Tuning Concurrency Using memory_limit
Configuring the memory limit is the primary method of adjusting PHP application concurrency on Heroku. Refer to this table to see how the number of child processes changes for each dyno type and memory limit setting. See Determining a Suitable Memory Limit for more guidance.
Configuring the memory_limit
for PHP-FPM
Setting Memory Limit via .user.ini
You can add a .user.ini
config file containing a memory limit setting to your application’s document root, usually the top-level directory of your application. For example, to set a memory limit of 64 MB for an application, set it in the .user.ini
:
memory_limit = 64M
You must use the correct shorthand notation required by PHP to indicate megabytes using the M
suffix.
Your application’s document root can differ from the top-level directory of your application if you configured it using a Procfile
command argument.
If you deploy your app with this file, you can see the number of workers automatically adjust for the given memory limit. For example, 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
Additional .user.ini
files in sub-directories of the document root don’t get evaluated when Heroku determines the memory limit at dyno boot time. They can take effect at runtime as documented when serving requests to PHP files in such directories. Keep this in mind in the unlikely case you have different memory_limit
settings for different sub-directories of your application. We recommend using the same settings for the sub-directories.
Setting the Memory Limit using PHP-FPM Configuration
Instead of a .user.ini
file, you can also use a PHP-FPM config include to add a php_value
or php_admin_value
directive to change the memory_limit
setting. For example, to set a memory limit of 64 MB for an application, create a file named, for example, fpm_custom.conf
:
php_value[memory_limit] = 64M
For these settings to take effect, you must use the -F
option in your Procfile
command to load the config:
web: heroku-php-apache2 -F fpm_custom.conf
If you deploy your app with the new fpm_custom.conf
and the changed Procfile
, you can see the number of workers automatically adjust for the given memory limit. For example, 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 memory_limit
Any change made to the memory limit at runtime using ini_set("memory_limit", ...)
won’t affect the concurrency, as the memory limit used for calculating WEB_CONCURRENCY
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 can get R14 errors. That error indicates that your application started paging to disk, which can degrade performance. In this case, either increase the static memory_limit
, or set WEB_CONCURRENCY
manually to a lower value.
In many cases, it can be desirable to have a lower memory limit to achieve higher concurrency. Use 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 in this case.
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 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’s likely that your code doesn’t consume that much memory during a request, so lowering the limit is a great way of optimizing your application performance.
After determining the maximum memory usage for your application, allow for a safety margin when setting your limit for future growth and unforeseen circumstances. For example, growing data sets over time.
Measuring and Optimizing Memory Usage Locally
On your development machine, use the memory_get_peak_usage()
function in your code, usually towards the very end of a script, to determine the peak memory used. For instance, put 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. For example, using sort path/to/logfile | tail -n 1
.
Another approach is lowering the memory limit configured in PHP in subsequent steps, for example, increments of 16 MB, until you start getting “memory limit exceeded” error messages. A limit of 64 MB is often safe and results in twice as many worker processes compared to the default configuration.
When performing load tests locally, using ab
, siege
, httperf
, or similar, you can also observe the amount of memory consumed by your php-fpm
processes using ps
or top
.
To properly profile your applications during development, we recommend XHProf. The complementing xhprof.io GUI makes navigating profiling results easy and convenient.
Remember to use realistic conditions when performing tests locally, for example, using suitable large result sets from a database. It’s also highly recommended to audit your code for any functions, loops, or algorithms that can 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
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
:
$ heroku config:set WEB_CONCURRENCY=8
When setting WEB_CONCURRENCY
manually, ensure that its value multiplied by your memory_limit
doesn’t exceed the amount of RAM available on your dyno type.
Setting the config var causes your application to restart, and your dyno(s) 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, remember to adjust it when you scale to a different dyno type to optimize for the amount of available RAM on the new dyno type.