The PHP Production Runtime: Web Server, FPM Workers, and Opcache
How PHP-FPM workers and opcache combine to serve requests in production, and how to configure them for the South Florida hosting landscape.
The PHP Production Runtime: Web Server, FPM Workers, and Opcache
Enabling opcache and installing PHP-FPM does not make a production server fast or stable. The PHP production runtime is a pipeline of three pieces that must be configured together: the web server SAPI boundary, the PHP-FPM worker pool, and the opcache shared memory segment. Misconfigure one, and the other two cannot compensate. The web server passes a call to a worker. The worker compiles and executes the code. The opcache stores the compiled bytecode so the next call skips compilation. If the worker pool runs out of available processes, calls queue at the SAPI boundary until they time out. If the opcache is too small for the codebase, compiled scripts are evicted before they are ever reused. The hit rate drops to zero. A phpinfo() page that says opcache.enable is on tells you nothing about whether the cache is working.
The SAPI Boundary: Nginx, Apache, and the Request Handoff
Every PHP call crosses a Server API boundary. The web server, whether nginx or Apache, does not execute PHP. It hands the call to a PHP process through a SAPI module. The three SAPIs that matter in production are FPM, mod_php, and the CLI. mod_php embeds PHP inside Apache workers. It ties PHP process concurrency to Apache's MPM model, wasting memory on keepalive connections. FPM runs PHP as a separate pool of persistent workers, decoupled from the web server. nginx talks FastCGI directly. Apache can use mod_proxy_fcgi to do the same. The CLI SAPI is for cron jobs and queue workers, never for serving HTTP. A production health check that hits the web server but does not exercise the FPM socket is testing only half the runtime.
PHP-FPM Worker Configuration: The Stability Controls
Set the Worker Ceiling with pm.max_children
PHP-FPM worker configuration lives in the pool file, usually named www.conf. pm.max_children sets the absolute ceiling on simultaneous worker processes. Exceed it, and new connections queue in the socket backlog until they hit the web server's timeout. The correct number is not a formula from a blog post. Measure the memory footprint of one idle worker. Multiply by pm.max_children. Ensure that total fits inside the server's available RAM with room for the opcache, the database, and the OS buffer cache. A server with 4 GB of RAM and a 60 MB worker footprint can safely run about 40 children. A server with 90 MB workers on the same box can run about 25 before swapping starts. Swapping is a production outage.
Defend Against Memory Leaks with pm.max_requests
pm.max_requests sets the number of calls a worker handles before it exits and is replaced. This is the defense against slow memory leaks in application code. A worker that never exits accumulates leaked memory until it exhausts the process limit. Set pm.max_requests to a value between 500 and 2000. Monitor the process list for workers that grow over their lifetime. Too low a value causes unnecessary process churn and a measurable CPU cost from forking replacements. Too high a value lets a leaking worker consume memory for hours before it is recycled.
Opcache Production PHP: What phpinfo() Does Not Tell You
Switch Off Timestamp Validation
An opcache production PHP deployment has three configuration knobs. The first two are binary. opcache.enable must be on. opcache.validate_timestamps should be off in production. When validate_timestamps is on, the engine stats every file on every call to check for changes. That system-call cost erases most of the opcache's benefit. Turn it off, and the opcache serves what it has. The trade-off: a deploy no longer picks up new code automatically. You must clear the cache explicitly, either by calling opcache_reset() through a deploy hook or by reloading PHP-FPM.
opcache.revalidate_freq matters only when validate_timestamps is on, which it should not be in production. Set it to 0 as a safety gate.
Size the Shared Memory and File Keys
The next settings determine whether the opcache actually works. opcache.memory_consumption sets the size of the shared memory segment in megabytes. A codebase with 1200 PHP files needs roughly 80 MB to store all of them. Set it to 64 MB on that codebase, and the oldest scripts are evicted to make room for new ones. The hit rate in the opcache status page will show a restart rate that equals the call rate. The cache is clearing itself constantly. The claimed effectiveness, as reported by phpinfo(), is that the opcache is enabled. The real effectiveness is a hit rate at or above 99% on a warm cache. See this by running opcache_get_status() and dividing hits by hits plus misses.
opcache.max_accelerated_files controls how many distinct script keys the cache can hold. A large framework application can contain 8000 source files from vendor dependencies alone. Set the value higher than the script count, then add 20% headroom. opcache.interned_strings_buffer stores the strings the engine reuses across calls, such as class names and function signatures. A buffer of 16 MB is sufficient for most applications. A buffer of 4 MB on a Symfony project causes fragmentation that degrades performance even with a perfect script hit rate.
The Cache Warm-Up Problem and the Deploy Cycle
A deploy that clears the opcache without a warm-up causes a stampede. The first calls after the clear each trigger script compilation. In a pool with 40 workers, 40 calls hit simultaneously and all try to compile the same files into the opcache at once. CPU spikes to 100%. Calls time out. The application appears down for 10 to 30 seconds.
Run a cache warm-up script after the deploy and before the new code is live. Walk the application's file list and include every PHP file once, forcing compilation into the cache while the previous pool is still serving traffic. Run the warm-up from the CLI SAPI, not through FPM, so it does not consume a worker slot. After the warm-up completes, reload FPM to cut over to the new code.
PHP Web Server SAPI and South Florida PHP Hosting Runtime
Shared Hosting and the cPanel Ceiling
The South Florida PHP hosting runtime spans three distinct tiers. Each sets different defaults for the SAPI and the opcache. The first tier is shared hosting managed through cPanel. The MultiPHP selector lets you pick a PHP version, but the PHP-FPM worker configuration is controlled by the host. pm.max_children is set to a conservative value between 5 and 15. The opcache is enabled but memory_consumption is capped at 64 MB or less. A site that outgrows these limits must move to a VPS.
DigitalOcean Droplets Start with mod_php
The second tier is a DigitalOcean LAMP image, the starting point for many local Laravel deployments. The one-click image installs Apache with mod_php by default, not FPM. Your first production change on a fresh Droplet: disable mod_php and install PHP-FPM. Then configure Apache's proxy module to pass FastCGI. The opcache is enabled in the image but validate_timestamps defaults to on. Switch it off.
AWS Miami Local Zone for Latency-Sensitive Apps
The third tier is the AWS Miami Local Zone. For latency-sensitive applications serving South Florida end users, the Local Zone places compute within single-digit milliseconds of the user. PHP applications that call external APIs from the Local Zone see a round-trip reduction of 20 to 40 ms compared to us-east-1. The FPM and opcache configuration is the same as any other AWS instance. The placement matters for the user-facing response time.
The Single Thing That Goes Wrong
Ship a deploy script that clears the opcache and does nothing else. The next call that hits the application compiles every file on the critical path. If that call is a user's, they wait. If that call is a health check, it may time out and trigger a false alarm. The production runtime does not fail because PHP is slow. It fails because the deploy process treats the opcache as a deploy artifact instead of a runtime component that must be warmed before traffic is served.
Common Questions
How do I check if the opcache is actually working?
Run a script that calls opcache_get_status() and reads the opcache_statistics array. The key ratio is opcache_hit_rate. A rate below 90% on a warm cache means the memory_consumption is too small or max_accelerated_files is too low. Ignore the phpinfo() line that says Opcode Caching is Up and Running.
What is the correct pm.max_children value for my server?
Measure the resident memory of one idle FPM worker with ps or top. Multiply by the number of children you intend to run. Add the opcache shared memory and the OS overhead. The total must not exceed the server's RAM. Test under load, not at idle.
Should I use Apache mod_php or PHP-FPM in production?
Use PHP-FPM. mod_php embeds the interpreter inside Apache and cannot decouple PHP processes from HTTP connections. FPM lets you tune the worker pool independently and avoids wasting memory on keepalive connections that are not executing PHP.
Why does my site slow down after a deploy even though opcache is enabled?
The opcache is being cleared on deploy without a warm-up. The first calls after the clear compile scripts into the cache simultaneously, causing a CPU spike. Run a CLI warm-up script that includes every PHP file before the new pool goes live.