PHP performance in production is mostly a configuration discipline, not a rewriting exercise. The engine that ships in 2026 is fast: the PHP 7 rewrite, years of OPcache refinement and the JIT compiler have removed the language itself from the list of plausible bottlenecks for typical web workloads. When a PHP application is slow in production, the cause is almost always one of four things: OPcache misconfigured or undersized, PHP-FPM pools sized by guesswork, an I/O problem hiding behind application code, or work being done per-request that should be done once. This post works through each, in the order you should check them.
OPcache: the single highest-leverage setting
Without OPcache, PHP re-parses and re-compiles every file on every request. With it, compiled opcodes live in shared memory and execution skips straight past compilation. It is enabled by default in most distributions, but the default sizing is conservative for a modern framework application that loads thousands of files. A production baseline looks like this:

opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
Two of these deserve explanation. max_accelerated_files must exceed the number of PHP files your application can load; a Laravel or Symfony project with its vendor directory easily passes ten thousand, and once the limit is hit, the overflow files are recompiled every request with no error to tell you so. validate_timestamps = 0 stops OPcache from checking file modification times on every request, which eliminates a stat call per file. The trade-off is that deploys must explicitly reset the cache, by reloading PHP-FPM or deploying to a fresh directory. If your deployment process cannot guarantee that, leave validation on with a revalidate_freq of a few seconds rather than shipping stale code.
Check the cache health rather than assuming it: opcache_get_status() reports hit rate, memory usage and the number of cached scripts. A hit rate below 99 percent or a full wasted_memory pool means the configuration needs attention.
PHP-FPM: size pools with arithmetic, not folklore
PHP-FPM manages a pool of worker processes, and each worker handles exactly one request at a time. The most common production failure mode is simple: more concurrent requests than workers, so requests queue, latency spikes, and upstream proxies start timing out. The fix is arithmetic. Measure the real memory footprint of a worker under load, then divide:
; average worker uses ~60 MB, server has 8 GB, leave 2 GB for everything else
pm = dynamic
pm.max_children = 100
pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 30
pm.max_requests = 1000
Measure per-worker memory with something like ps aggregated across the pool during peak traffic, not with a single idle worker, because memory grows as caches warm. pm.max_requests recycles workers periodically, which papers over slow leaks in long-running extensions. For servers dedicated to PHP with steady traffic, pm = static removes process churn entirely; for spiky or shared workloads, dynamic is the safer default. Whichever you choose, enable the FPM status page and watch listen queue: any sustained non-zero value means requests are waiting for a free worker, and that queue time is invisible in your application logs.
Preloading: real gains, real constraints
Opcode caching removes compilation, but classes are still linked, and files still included, on every request. opcache.preload, available since PHP 7.4, goes further: a preload script runs once at server start, and everything it loads stays resident in memory, fully linked, shared by all workers. For large frameworks this shaves a measurable slice off every request, typically most noticeable on lightweight endpoints where bootstrap dominates.

opcache.preload = /var/www/app/config/preload.php
opcache.preload_user = www-data
Symfony generates a suitable preload file as part of its cache warmup, and Laravel projects commonly preload the framework and the hottest application paths. The constraints are worth knowing before you commit: preloaded code cannot be replaced without restarting PHP-FPM, so it changes your deploy procedure, and preloading everything is counterproductive because rarely used code wastes shared memory. Preload the framework core and your hot paths, deploy with a graceful FPM reload, and verify the effect with a load test rather than trusting intuition.
Profile before you optimise anything
Everything above is generic; beyond it, guessing is expensive. The profiler exists so you do not optimise the wrong thing. In 2026 the practical toolkit has three tiers. For always-on production visibility, sampling profilers and APM tools such as Excimer-based profilers, Tideways or Blackfire monitoring add negligible overhead and show you where wall time actually goes across real traffic. For deep single-request analysis, Xdebug's profiler or Blackfire's instrumented mode produce call graphs that make the expensive path obvious. For quick server-side checks, even microtime() timers around suspect boundaries beat speculation.
When you profile a typical PHP application, the results are humbling in a useful way. The top of the list is rarely PHP execution; it is database queries issued in loops, HTTP calls to internal services, serialisation of large payloads, or cache misses cascading into recomputation. That leads to the practical hierarchy of fixes:
- Eliminate N+1 queries. One query per row is the most common performance bug in database-backed PHP; eager loading or a joined query removes it.
- Cache computed results. Redis or APCu in front of expensive queries and API calls, with explicit invalidation rules.
- Move slow work off the request. Email, PDF generation, image processing and webhook fan-out belong in a queue worker, not in the user's request.
- Only then micro-optimise PHP. Algorithmic fixes in hot loops occasionally matter; scattered micro-tweaks almost never do.
Two quiet wins people forget
The realpath cache stores resolved file paths, and its default size is small for large dependency trees; raising realpath_cache_size to a few megabytes and realpath_cache_ttl upward reduces filesystem stat traffic, especially on network filesystems. And Composer's optimised autoloader, enabled with composer install --optimize-autoloader --classmap-authoritative in production, replaces filesystem probing with a prebuilt class map. Neither change will transform your response times alone, but both are free.

The takeaway
Production PHP performance work follows a strict order of operations: confirm OPcache is sized and hitting, size FPM pools from measured memory, add preloading if bootstrap cost matters, then profile and fix what the profiler actually shows, which is usually I/O. Runtimes like FrankenPHP and Swoole can push throughput further by keeping the application booted between requests, but they amplify a well-tuned application rather than rescuing a misconfigured one. Get the boring configuration right first; it is an afternoon of work that outperforms weeks of speculative code changes.
Related Service
π» Web Development
Custom websites and web applications built with PHP, Laravel, WordPress, and React β fast, secure, scalable, and tailored to your business goals.
Explore Web Development →
Reviews & Comments
Reviews are moderated and appear after approval.
No reviews yet β be the first to share your thoughts.
Leave a Review