Skip to main content
Blog|
Learning center

What Is PHP-FPM and How Does It Work

|
Mar 21, 2026|13 min read
LEARNING CENTERWhat Is PHP-FPM and How DoesIt WorkHOSTNEYhostney.comMarch 21, 2026

PHP-FPM (FastCGI Process Manager) is the process manager that runs your PHP code on the server. When a visitor loads a WordPress page, the web server does not execute PHP itself. It passes the request to PHP-FPM, which manages a pool of worker processes that execute the PHP code, generate the HTML, and pass the response back to the web server.

Every WordPress page load, every WooCommerce checkout, every admin panel interaction goes through PHP-FPM. It is the layer between your web server (Nginx or Apache) and your PHP application. How it is configured determines how many requests your site can handle simultaneously, how much memory PHP consumes, and what happens when traffic exceeds your server’s capacity.

If you have ever seen a 503 Service Temporarily Unavailable error on a WordPress site, there is a good chance PHP-FPM was the bottleneck. Understanding how it works helps you understand why.

How PHP execution works without PHP-FPM#

Before PHP-FPM existed, the most common way to run PHP with Apache was mod_php. This embedded the PHP interpreter directly into every Apache worker process. When Apache started, each worker loaded PHP into memory regardless of whether it would serve a PHP request or a static file.

The problem: serving an image, a CSS file, or a JavaScript file does not require PHP, but the Apache process serving it carried the full PHP interpreter in memory anyway. On a server handling a mix of static and dynamic content – which describes every WordPress site – this wasted significant memory.

mod_php also tied PHP’s lifecycle to Apache’s. Restarting PHP meant restarting Apache. Configuring different PHP versions or settings for different sites on the same server was difficult. There was no separation between the web server and the PHP runtime.

PHP-FPM solved this by running PHP as a separate service with its own process management. The web server handles HTTP connections and static files. PHP-FPM handles PHP execution. Each does what it is good at, independently.

How PHP-FPM works#

The request flow

When a visitor requests a WordPress page, this is what happens:

  1. The web server (Nginx or Apache) receives the HTTP request
  2. The web server determines the request needs PHP (it is a .php file or a rewritten URL pointing to index.php)
  3. The web server passes the request to PHP-FPM through a Unix socket or TCP connection
  4. PHP-FPM assigns the request to an available worker process from its pool
  5. The worker executes the PHP code – loading WordPress, querying the database, running plugin code, generating HTML
  6. The worker sends the response back to the web server through the socket
  7. The web server sends the response to the visitor
  8. The worker returns to the pool, ready for the next request

Static files – images, CSS, JavaScript, fonts – never touch PHP-FPM. The web server serves them directly. This is one reason Nginx is particularly efficient for WordPress: its event-driven architecture handles static files with minimal overhead while passing only PHP requests to PHP-FPM.

Unix socket vs TCP connection

PHP-FPM can listen for requests in two ways:

Unix socket ( /run/php-fpm/www.sock or similar). Communication happens through a file on the filesystem. Both the web server and PHP-FPM must be on the same machine. Unix sockets have lower overhead than TCP because they skip the network stack entirely – no TCP handshake, no packet framing, no loopback interface.

TCP connection ( 127.0.0.1:9000 ). Communication happens over the network stack, even when both services are on the same machine. TCP adds overhead from the network layer but allows PHP-FPM to run on a different machine from the web server.

For WordPress hosting where the web server and PHP run on the same machine, Unix sockets are the standard choice. The latency difference per request is small, but across thousands of requests per minute, it adds up. TCP is used when PHP-FPM runs on a separate server, which is a scaling pattern for high-traffic architectures.

Worker processes

A PHP-FPM worker is a running PHP process waiting to execute code. Each worker handles one request at a time. While a worker is busy executing your WordPress page load – running queries, processing plugin code, generating output – it cannot accept another request.

This is the fundamental constraint. The number of available workers determines how many PHP requests your site can handle simultaneously. If you have 10 workers and 15 requests arrive at the same time, 10 are processed immediately and 5 wait in a queue until a worker finishes.

Each worker consumes memory. A WordPress site with a moderate set of plugins typically uses 40-80 MB per worker process. A WooCommerce store with a page builder, SEO plugin, and analytics can push past 100 MB per worker. Multiply that by the number of workers, and you see how PHP-FPM drives memory consumption on a hosting server.

Process pool configuration#

The pool configuration determines how PHP-FPM manages its worker processes. This is where the practical performance tuning happens. PHP-FPM offers three process management modes:

Static mode

pm = static
pm.max_children = 10

PHP-FPM starts a fixed number of workers when it launches and keeps them running at all times. In this example, 10 workers are always running, always consuming memory, always ready to handle requests.

When it makes sense: When your traffic is consistent and you want predictable memory usage with no worker startup latency. If your server has enough RAM to keep all workers running permanently, static mode eliminates the overhead of spawning and killing processes.

The tradeoff: Memory is allocated whether the workers are busy or idle. If traffic drops to zero, those 10 workers are still sitting in memory doing nothing.

Dynamic mode

pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10

PHP-FPM starts with a set number of workers ( start_servers ) and adjusts the pool size based on demand. It keeps a minimum number of idle workers ready ( min_spare_servers ) and scales up to a maximum ( max_children ) as traffic increases. When traffic drops, excess idle workers are killed back down to max_spare_servers .

When it makes sense: For most WordPress hosting. Dynamic mode balances memory usage with responsiveness. During quiet periods, fewer workers consume less memory. During traffic spikes, additional workers are spawned to handle the load.

The tradeoff: Spawning a new worker takes time (typically a few milliseconds). Under a sudden traffic spike, there is a brief window where requests queue while workers are being created. The min_spare_servers setting controls how many idle workers are pre-created to absorb this.

On-demand mode

pm = ondemand
pm.max_children = 20
pm.process_idle_timeout = 10s

No workers run when there is no traffic. Workers are spawned only when a request arrives, and they are killed after a period of inactivity ( process_idle_timeout ).

When it makes sense: For low-traffic sites where memory conservation is the priority. A personal blog that gets a handful of visitors per hour does not need workers sitting idle consuming RAM.

The tradeoff: Every request after an idle period incurs worker startup latency. The first visitor after a quiet period waits longer because a worker has to be spawned before the request can be processed. For a site with steady traffic, this mode adds unnecessary latency. For a site that is mostly idle, it frees memory for other uses.

pm.max_children: why it matters#

pm.max_children is the single most important PHP-FPM setting. It sets the absolute maximum number of worker processes that can run simultaneously. When all workers are busy and max_children is reached, new requests wait in a queue. If the queue fills up, PHP-FPM refuses the connection and the web server returns a 503 error.

How to calculate it

The basic formula:

max_children = available memory for PHP / average memory per worker

If your hosting plan allocates 1 GB of RAM and your WordPress site uses 80 MB per worker:

max_children = 1024 MB / 80 MB = 12 workers

In practice, you need to leave memory for the operating system, MySQL, Nginx, and other services. On managed hosting, the provider typically calculates this for you. On a VPS, you need to account for everything running on the server.

What happens when it is too low

If max_children is set too low for your traffic, workers fill up during normal load, requests queue, and visitors see slow page loads or 503 errors. This is the most common performance problem on WordPress hosting – not slow PHP code, not a slow database, but simply not enough PHP workers to handle the concurrent request volume.

What happens when it is too high

If max_children is set too high, the workers collectively consume more memory than the server has available. The system starts swapping (using disk as memory), which is dramatically slower than RAM. In the worst case, the OOM killer terminates processes to free memory, potentially killing PHP-FPM itself or MySQL.

Setting max_children higher than your memory can support does not give you more capacity. It gives you worse performance across all requests because of swap thrashing.

The relationship with timeouts

When workers are all busy, incoming requests queue. How long they queue before the web server gives up is controlled by timeout settings. If a request waits in the queue and eventually gets processed, the visitor experiences a slow page load. If it waits past the web server’s timeout (typically 30-60 seconds), the visitor sees a 504 Gateway Timeout.

Pool exhaustion can produce either a 503 (immediate refusal because the queue is full) or a 504 (timeout because the request waited too long in the queue). The underlying cause is the same: more concurrent PHP demand than the pool can handle.

Memory limits per pool#

PHP-FPM enforces memory limits at two levels:

php.ini memory_limit

The memory_limit directive in php.ini sets the maximum memory a single PHP script can allocate during execution. If a script tries to allocate more than this limit, PHP terminates it with a fatal error: “Allowed memory size exhausted.”

This is a per-request limit, not a per-worker limit. A worker with a 256 MB memory_limit can allocate up to 256 MB for one request, then release that memory and handle another request that allocates a different amount.

For WordPress, 128 MB is the minimum recommended. WooCommerce stores, sites with page builders, and plugin-heavy installations often need 256 MB or more. WordPress itself defines a WP_MEMORY_LIMIT constant (default 40 MB, 256 MB for multisite) that can be overridden in wp-config.php, but the php.ini memory_limit is the hard ceiling.

Pool-level resource limits

Beyond per-script memory limits, PHP-FPM pools can be constrained at the operating system level. On managed hosting, cgroups (control groups) enforce total memory limits for all workers in a pool combined. If a pool’s workers collectively exceed the cgroup memory limit, the kernel intervenes rather than letting the pool consume the server’s memory.

This is the mechanism that prevents one site from consuming all the RAM on a shared server. The per-script memory_limit prevents a single request from going wild. The cgroup limit prevents the pool as a whole from consuming more than its allocation.

The slow log: debugging slow PHP#

PHP-FPM has a built-in slow log that records a stack trace whenever a request takes longer than a configured threshold to execute. This is one of the most useful debugging tools for WordPress performance problems.

Configuration

slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 5s

When any request takes longer than 5 seconds, PHP-FPM writes a stack trace to the slow log showing exactly which function was executing when the timeout fired. This tells you not just that a request was slow, but where in the code the time was spent.

What the output looks like

[pool www] pid 12345
script_filename = /home/user/public_html/index.php
[0x00007f8a2c0d3a80] query() /home/user/public_html/wp-includes/class-wpdb.php:2056
[0x00007f8a2c0d3a40] get_results() /home/user/public_html/wp-content/plugins/slow-plugin/query.php:142
[0x00007f8a2c0d39e0] heavy_query() /home/user/public_html/wp-content/plugins/slow-plugin/main.php:85

This stack trace tells you that the slow request was caused by a database query in a plugin called “slow-plugin,” specifically in the heavy_query() function. Without the slow log, you would only know that the page was slow. With it, you know exactly which plugin and which function to investigate.

When to use it

Enable the slow log when:

  • Pages are intermittently slow and you cannot reproduce the issue consistently
  • You suspect a specific plugin is causing performance problems but need evidence
  • Your site is experiencing 504 Gateway Timeouts and you need to identify which requests are exceeding the timeout
  • WooCommerce operations (cart, checkout) are slower than expected

The slow log adds minimal overhead because it only activates when a request exceeds the threshold. It can safely remain enabled in production.

Pool isolation: shared vs dedicated#

How PHP-FPM pools are configured on a hosting server determines whether your site’s PHP performance is affected by other sites on the same machine.

Shared pool (traditional shared hosting)

On traditional shared hosting, multiple accounts share a single PHP-FPM pool. The server runs one PHP-FPM instance with one pool of workers, and requests from all accounts are assigned to workers from this shared pool.

The problem: when another account on the server is running slow PHP code or handling a traffic spike, it consumes workers from the shared pool. Your site’s requests queue behind theirs, even though your site is not doing anything unusual. A slow WooCommerce store running unoptimized queries on another account can cause your lightweight blog to return 503 errors because all the shared workers are busy.

This is the PHP-level noisy neighbor problem. CloudLinux’s LVE limits CPU and memory per account, but it does not give each account its own PHP-FPM pool. The workers are still shared.

Dedicated pool per account

A better model gives each hosting account its own PHP-FPM pool with its own workers. Account A has its pool of workers. Account B has a separate pool. The pools are independent – exhausting one pool has no effect on the other.

This requires more memory per server (each pool has its own minimum set of workers) but eliminates cross-account PHP interference entirely. Your pm.max_children is yours. Your workers are yours. Another account’s traffic spike is their problem, not yours.

Per-container PHP-FPM (full isolation)

Container-based hosting takes this further. Instead of running multiple PHP-FPM pools on a shared operating system, each account runs its own PHP-FPM instance inside its own container. The PHP-FPM process, the workers, the configuration, and the PHP version are all isolated per account.

This means:

  • Each account has its own PHP-FPM master process and its own pool of workers
  • PHP version and configuration are independent per account – one account can run PHP 8.3 while another runs PHP 8.1
  • A PHP-FPM crash in one container does not affect PHP-FPM in other containers
  • Memory limits are enforced at the container level by kernel cgroups, not by PHP-FPM configuration alone
  • The slow log, error log, and access patterns are per-account with no cross-contamination

This is the strongest isolation model for PHP-FPM on shared infrastructure. It provides the dedicated PHP experience of a VPS without requiring you to manage PHP-FPM configuration, pool tuning, or version upgrades yourself.

PHP-FPM configuration on Hostney#

Hostney runs PHP-FPM per-container. Each hosting account gets its own Podman container with its own PHP-FPM instance. Here is what that means in practice:

Dedicated workers. Your PHP-FPM pool is yours. Other accounts on the same server have their own pools inside their own containers. A traffic spike or slow plugin on another account does not consume your workers, queue your requests, or affect your response times.

Per-account PHP version. PHP runs inside your container with the version you select through the control panel. Switching from PHP 8.1 to PHP 8.3 restarts PHP-FPM in your container only. Multiple PHP versions from 5.6 through 8.5 are available. Each site on your account can run a different version if needed.

Resource enforcement. Each container has cgroup-enforced CPU, RAM, and I/O limits. Your PHP-FPM workers operate within your plan’s resource allocation. The pm.max_children is calculated based on your plan’s RAM allocation and the memory characteristics of your WordPress installation, so the pool size matches what your resources can actually support.

Container-level crash isolation. If PHP-FPM crashes in your container (a rare event, but it happens), it is restarted automatically within your container. Other accounts are unaffected because they run separate PHP-FPM instances in separate containers.

The slow log is available per account for debugging performance issues. Error logging is configured per account with logs accessible through the control panel.

This architecture means your PHP performance is determined by your site’s code, your plugins, your traffic, and your plan’s resources – not by what other accounts on the server are doing.

Explore the hosting plans for resource allocations per plan, or read more about container isolation and Nginx + PHP-FPM integration to understand the full stack.