How to make a PHP site faster with OPcache and PHP-FPM settings

Most of the speed comes from one thing: OPcache being on for your website. After that, size PHP-FPM to your memory, find the slow pages, and measure every change.

Measure first. Take the timings in step 1 before you change anything, and run the same test after each change. If a setting doesn't make a difference you can measure, undo it.

Before you start

  • The examples use Ubuntu 24.04 with PHP 8.3 and Debian 13 with PHP 8.4, with Nginx and PHP-FPM. Change 8.3 to your version (php -v).
  • The site is called shop, lives in /var/www/shop and has its own PHP-FPM pool in /etc/php/8.3/fpm/pool.d/shop.conf. If you don't have your own pool yet, see our guide to one pool per site. Without one, your settings go in the shared pool file www.conf.
  • What OPcache does: PHP normally reads and compiles every PHP file on every request. OPcache keeps the compiled files in memory, so the next request skips that work.

1. Measure how fast it is now

Time a few requests to a normal page with curl. Run it from the server itself or from another machine, but always from the same place:

for i in 1 2 3 4 5; do
  curl -s -o /dev/null -w '%{http_code} first byte %{time_starttransfer}s, total %{time_total}s\n' https://example.com/
done

"First byte" is how long the server took to start answering. That's the part PHP controls. Write the numbers down.

To see how many requests per second the site can serve, use ab (in the apache2-utils package):

sudo apt install apache2-utils
ab -n 300 -c 4 https://example.com/

This sends 300 requests, 4 at a time. Look at "Requests per second" and "Failed requests". Run it twice, because the first run is often slower. It puts load on your live site, so run it at a quiet time and only on your own sites. If your site has a page cache plugin, test a page that isn't cached, or you only measure the cache.

2. Check that OPcache is on for the website

The php command and PHP-FPM read different settings files: /etc/php/8.3/cli/ for the command line and /etc/php/8.3/fpm/ for the website. So php -i only tells you about the command line:

php -i | grep '^opcache.enable'

It shows opcache.enable => On, but also opcache.enable_cli => Off: on the command line, OPcache does nothing. That's the normal default and fine. To see the website's settings, ask PHP-FPM:

php-fpm8.3 -i | grep -E '^opcache\.(enable|memory_consumption|max_accelerated_files|validate_timestamps|revalidate_freq) '

This reads the files, not the running PHP-FPM, and doesn't see settings inside a pool file. The sure test is to ask the website itself. Don't leave a phpinfo() page lying around. Make a small file with a random name, open it once, and delete it:

echo '<?php $s = opcache_get_status(false); echo $s ? "on, {$s["opcache_statistics"]["num_cached_scripts"]} files, " . round($s["memory_usage"]["used_memory"] / 1048576) . " MB used, full: " . ($s["cache_full"] ? "yes" : "no") : "off";' | sudo tee /var/www/shop/oc-7fk2m.php
curl https://example.com/oc-7fk2m.php
sudo rm /var/www/shop/oc-7fk2m.php

Open a few pages of the site first, so the cache has something in it. On our test WordPress it printed on, 494 files, 29 MB used, full: no. If it prints off, install the package and restart PHP-FPM: sudo apt install php8.3-opcache. (PHP 8.5 has OPcache built in, so there's no separate package.)

3. Pick your OPcache settings

On Ubuntu 24.04 and Debian 13, OPcache is on by default with 128 MB of memory and room for 10,000 files. That fits many sites. Raise it if the check in step 2 says full: yes, the memory used is close to 128 MB, or you run several sites on the same PHP version. All pools of one PHP version share one OPcache, so add up your sites.

To count the PHP files of a site:

find /var/www/shop -name '*.php' | wc -l

Our WordPress test site had 1,487 PHP files, but only 494 were used on the pages we opened. A Laravel or Symfony app with its vendor folder often has many more.

Put your settings in a file of your own, so PHP updates don't overwrite them:

sudo nano /etc/php/8.3/fpm/conf.d/99-opcache.ini
; Memory for compiled files, in MB (default 128)
opcache.memory_consumption = 256
; Memory for repeated text such as class and function names, in MB (default 8)
opcache.interned_strings_buffer = 16
; How many files fit in the cache (default 10000)
opcache.max_accelerated_files = 20000
sudo php-fpm8.3 -t
sudo systemctl reload php8.3-fpm

Then run the check from step 2 again. To be honest about it: on our small test site, which already fit in the defaults, these settings made no difference we could measure. They help when the cache is full.

4. Decide how new code is picked up

By default OPcache checks every 2 seconds whether a PHP file has changed (opcache.validate_timestamps = 1 and opcache.revalidate_freq = 2). New code shows up on its own, a moment after you upload it.

You can turn the check off to save a little work on every request:

opcache.validate_timestamps = 0

Then PHP never notices changed files until you reload PHP-FPM. In our test, after we changed a file, the site kept showing the old version until the reload. So make the reload the last step of every deploy:

sudo systemctl reload php8.3-fpm

Only use this if you deploy with a script that always reloads, like the one in our Laravel no-downtime deploy guide. Keep the check on for WordPress and other apps that update themselves from the admin area: with it off, a plugin update or security fix doesn't take effect until someone reloads PHP-FPM.

5. JIT: usually not worth it

PHP 8 also has a "JIT", which turns busy code into machine code. It helps code that does a lot of math in PHP, like image processing. Typical websites spend most of their time waiting for the database and building HTML, so they gain little. Ubuntu and Debian ship with it off.

In our test, WordPress with JIT on wasn't faster in a way we could measure, and the biggest PHP worker used about 14 MB more memory. If you want to try it, add this to your 99-opcache.ini, reload, and measure:

opcache.jit = tracing
opcache.jit_buffer_size = 64M

If you see no gain, remove the two lines again.

6. Size PHP-FPM to your memory

Each PHP-FPM worker handles one request at a time. pm.max_children is the most workers a pool may run. Too few, and visitors wait in line. Too many, and the server runs out of memory, which is much worse.

Measure how big a worker is. Do it after the site has had some visitors, because a fresh worker is much smaller than a busy one:

ps --no-headers -o rss,cmd -C php-fpm8.3 | awk '/pool/ { s += $1; n++ } END { printf "%d workers, average %d MB\n", n, s / n / 1024 }'

In our test, a WordPress worker grew from about 14 MB when idle to about 48 MB after serving pages. Your app will be different, so measure.

Then do the math. Take the memory you can give to PHP: total memory, minus the database, the web server and some room to spare. Check with free -m. Divide it by the worker size. For example, 1,000 MB for PHP and 50 MB per worker gives pm.max_children = 20. If you run several pools, they share that memory, so split it between them.

Pick how workers start in the pool file:

  • pm = ondemand starts workers only when requests come in and stops them after they've been idle for a while. Good for small or quiet sites and servers with many sites.
  • pm = dynamic always keeps a few workers ready. Good for a busy site, because no request waits for a worker to start. This is the default in www.conf.
; in /etc/php/8.3/fpm/pool.d/shop.conf
pm = ondemand
pm.max_children = 20
pm.process_idle_timeout = 30s

For dynamic, also set how many workers wait: pm.start_servers, pm.min_spare_servers and pm.max_spare_servers. The defaults in www.conf (2, 1 and 3) are a fine start. Then check and reload:

sudo php-fpm8.3 -t
sudo systemctl reload php8.3-fpm

How to tell the limit is too low: PHP-FPM writes a warning in its log when every worker is busy:

sudo grep max_children /var/log/php8.3-fpm.log

A line like server reached pm.max_children setting (5), consider raising it means requests had to wait. We saw it with the default of 5 as soon as we sent 20 requests at once. Raise the limit only if the memory math above allows it.

7. Find slow pages with the slow log

PHP-FPM can write down every request that takes longer than a time you choose, together with the PHP function it was stuck in. Add this to the pool file:

request_slowlog_timeout = 5s
slowlog = /var/log/php8.3-fpm-shop-slow.log
sudo php-fpm8.3 -t
sudo systemctl reload php8.3-fpm

The request isn't stopped; it's only logged. A test page that waited 2 seconds, with the limit set to 1 second, gave this entry:

[12-Nov-2026 15:21:52]  [pool www] pid 14445
script_filename = /var/www/shop/slow-q2.php
[0x00007f55a9c140e0] sleep() /var/www/shop/slow-q2.php:1
[0x00007f55a9c14080] wait_for_api() /var/www/shop/slow-q2.php:1

Read it from the top: the top line is where PHP was waiting, the lines below show who called it. In a real app you'll often see a database query or a call to another website. That tells you where to look; PHP settings won't fix a slow query. Our guide to finding slow database queries shows how to track it down.

Set the time high enough that only real problems show up, for example 5 seconds, and check the log now and then. Our guide to keeping logs from filling the disk shows how to rotate log files like this one.

8. Measure again

Run the same curl and ab commands from step 1, from the same place. Change one thing at a time, so you know which change helped.

Our numbers, from a small test: a fresh WordPress with 30 sample posts, Nginx and PHP-FPM in a container on a shared lab machine, testing the home page from the same machine. Treat them as a rough idea, not a promise:

  • OPcache off: about 0.22 to 0.27 seconds to the first byte, about 12 requests per second.
  • OPcache on, default settings: about 0.05 to 0.07 seconds, about 40 to 55 requests per second. Roughly 4 times faster, on both PHP 8.3 and 8.4.
  • Bigger OPcache settings, timestamp check off, or JIT on: no difference we could tell apart from normal ups and downs.

So the big win was making sure OPcache was on. Everything after that is about not running out of memory and finding the slow parts of your own app.

If something goes wrong

  • Your changes to PHP files don't show up: you turned off opcache.validate_timestamps. Run sudo systemctl reload php8.3-fpm.
  • A setting doesn't seem to apply: you may have changed the cli folder instead of fpm, or forgot the reload. Check with the test file from step 2.
  • The site gets slower under load, or Nginx shows "502 Bad Gateway": look for the max_children warning from step 6, and for "Out of memory" in sudo journalctl -k. If memory runs out, lower pm.max_children. Our guide to stopping a small server from running out of memory covers swap and the OOM killer.
  • PHP-FPM won't start after a change: run sudo php-fpm8.3 -t. It names the file and line with the mistake.
  • The slow log stays empty and the PHP-FPM log says failed to ptrace(ATTACH) child: PHP-FPM isn't allowed to look into its workers. We saw this when PHP ran inside a container. It works on a normal server.

Official sources

Next steps: if you plan to move to a newer PHP version, see our guide to moving a PHP website to a newer PHP version, and copy your 99-opcache.ini along. And work through the security checklist for a new PHP server.

ServerNest is being built to set pool limits and PHP settings for each site, check them before PHP-FPM reloads, and collect each pool's slow log. The product is still in development. Join early access to try it first.

← Back to the security blog