Back to tutorials
Tutorial

VPS PHP-FPM Pool Tuning Tutorial (2026): Faster WordPress With Per-Site Limits on Nginx or Apache

VPS PHP-FPM pool tuning tutorial for WordPress: per-site pools, memory limits, slowlog, and safe reloads on Nginx/Apache.

By Anurag Singh
Updated on Sep 20, 2026
Category: Tutorial
Share article
VPS PHP-FPM Pool Tuning Tutorial (2026): Faster WordPress With Per-Site Limits on Nginx or Apache

Most WordPress “CPU spikes” on a VPS aren’t CPU problems. More often, one PHP-FPM pool lets a single noisy site grab every worker. The box hits swap, and then everything starts throwing 502/504s.

This VPS PHP-FPM pool tuning tutorial shows how to split WordPress sites into separate pools, set sane per-site limits, and enable slow logs. You’ll be able to point to what’s actually slow.

The examples use Ubuntu 24.04 LTS and Debian 12/13-style paths (I’ll call them out as we go). The same approach works with Nginx + PHP-FPM or Apache + PHP-FPM (proxy_fcgi).

If you don’t want to spend your evenings chasing pool settings after traffic jumps, this setup fits managed VPS hosting. You still run your preferred stack. You also get help when something gets hot.

What you’ll build (and how to know it worked)

  • Per-site PHP-FPM pools with separate Unix sockets (one site can’t starve the others).
  • Hard limits for each pool: max children, request timeouts, memory caps.
  • Diagnostics: status page (optional), slowlog, and log correlation to your web server.
  • A repeatable sizing method using real RSS numbers from your VPS.

You’ll know you got it right when the front-end stays responsive during spikes. 502/504 bursts stop being “random,” and swap usage stays boring.

If you’re already hitting OOM, fix that first. Use this swap configuration tutorial before you start tuning pools.

Prerequisites (quick checklist)

  • A VPS or dedicated server with root access (2 vCPU / 4 GB RAM minimum is workable for multiple WordPress sites).
  • PHP-FPM installed (PHP 8.2/8.3/8.4 packages; examples use php8.3-fpm).
  • Your web server: Nginx or Apache.
  • One WordPress site (or several) you can map to separate Linux users.

If you host multiple sites and you care about predictable resources (NVMe, steady CPU time), start with a HostMyCode VPS rather than shared hosting. Pool isolation matters most when uptime is on you.

Step 1: Baseline your current PHP-FPM behavior (5 minutes)

Before you touch config, grab a quick snapshot of load, sockets, and memory. On Ubuntu/Debian:

sudo systemctl status php8.3-fpm --no-pager
sudo ss -xl | grep php-fpm || true
sudo journalctl -u php8.3-fpm -n 200 --no-pager

# RAM and swap snapshot
free -h
swapon --show

# Top memory consumers (RSS)
ps -o pid,user,cmd,rss --sort=-rss | head -n 15

Next, locate the active pools and the main FPM config:

# Pool configs live here
ls -la /etc/php/8.3/fpm/pool.d/

# Main FPM settings
grep -nE '^(pid|error_log|include)' /etc/php/8.3/fpm/php-fpm.conf

On many “single site” installs, one pool named www ends up serving everything. That pool becomes the bottleneck. You’re going to split it apart.

Step 2: Pick a sizing method that matches a VPS

On a VPS, RAM is usually the first hard limit. PHP workers are processes. Each child can sit in the 60–200 MB RSS range, depending on plugins, WooCommerce, and how opcache behaves under load.

A practical sizing method that stays honest:

  1. During real traffic, measure average worker RSS.
  2. Reserve RAM for the OS and services (web server, SSH, agents).
  3. Reserve RAM for MariaDB/Redis if they run locally.
  4. Split what’s left across pools, then enforce hard caps.

To get a realistic RSS number, list PHP-FPM children and sample their RSS:

# Show php-fpm worker processes and RSS in MB
ps -C php-fpm8.3 -o pid,rss,cmd --sort=-rss | head -n 15 | awk '{printf "%s\t%.1fMB\t%s\n", $1, $2/1024, $3" "$4" "$5" "$6" "$7" "$8" "$9}'

A rule of thumb that still holds in 2026: plan for 120 MB per busy WordPress worker, unless you’ve measured lower.

  • Available for PHP: (Total RAM − 1.2 GB OS/daemons − DB/cache allowance)
  • Max children across all pools: Available for PHP / 120 MB

If you’re seeing 502/504s while CPU load looks fine, suspect worker queueing inside FPM. That’s not “slow CPU.” You’ll confirm it with slowlog in a later step.

Step 3: Create one Linux user per WordPress site (pool identity)

Per-site pools work best when each site runs as its own Unix user. Example for site1 and site2:

sudo adduser --disabled-password --gecos "" site1
sudo adduser --disabled-password --gecos "" site2

# Example docroots
sudo mkdir -p /var/www/site1/public /var/www/site2/public
sudo chown -R site1:site1 /var/www/site1
sudo chown -R site2:site2 /var/www/site2

If you use a control panel, it probably already created users per site. Reuse those usernames as pool names. That keeps the system consistent.

Step 4: Split the default “www” pool into per-site pools

Pool configs live in /etc/php/8.3/fpm/pool.d/. Start by backing up the default:

sudo cp -a /etc/php/8.3/fpm/pool.d/www.conf /root/www.conf.bak.$(date +%F)

Create a pool for site1:

sudo nano /etc/php/8.3/fpm/pool.d/site1.conf

Paste this and adjust only what you need. Keep the first pass simple. Tune the numbers after you confirm routing works.

[site1]
user = site1
group = site1

; Use a per-site Unix socket
listen = /run/php/php8.3-fpm-site1.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

; Process manager: start conservative on a VPS
pm = dynamic
pm.max_children = 8
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 500

; Kill hung scripts, but don't break normal admin tasks
request_terminate_timeout = 120s

; Useful safety + debugging
catch_workers_output = yes
php_admin_value[error_log] = /var/log/php8.3-fpm/site1-error.log
php_admin_flag[log_errors] = on

; Limit damage from "memory leak" plugins
php_admin_value[memory_limit] = 256M

; Slow request tracing
slowlog = /var/log/php8.3-fpm/site1-slow.log
request_slowlog_timeout = 5s

Repeat for site2, with its own socket and its own log paths.

Then create the log directory and files:

sudo install -d -m 0755 /var/log/php8.3-fpm
sudo touch /var/log/php8.3-fpm/site1-error.log /var/log/php8.3-fpm/site1-slow.log
sudo touch /var/log/php8.3-fpm/site2-error.log /var/log/php8.3-fpm/site2-slow.log
sudo chown -R www-data:adm /var/log/php8.3-fpm || true

Now choose what happens to www.conf:

  • If every site will run in its own pool, disable www by renaming it to www.conf.disabled.
  • If you still need a generic pool for misc apps, keep it—but make sure each vhost points to the right socket.
# Optionally disable default pool
sudo mv /etc/php/8.3/fpm/pool.d/www.conf /etc/php/8.3/fpm/pool.d/www.conf.disabled

Validate and reload:

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

If the reload fails, the unit logs usually show why:

sudo journalctl -u php8.3-fpm -n 200 --no-pager

Step 5: Point Nginx or Apache at the correct pool socket

At this stage, you’ve created pools. Your web server may still be sending traffic to the old socket. Fix that per site.

Nginx: update the fastcgi_pass socket per site

Edit your server block (commonly /etc/nginx/sites-available/site1.conf):

sudo nano /etc/nginx/sites-available/site1.conf

Inside the PHP location block:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm-site1.sock;
}

Validate and reload:

sudo nginx -t
sudo systemctl reload nginx

If your vhost layout is messy, standardize it first. This Nginx server blocks tutorial shows a clean multi-site setup that plays nicely with per-site pools.

Apache: set the right PHP-FPM socket per vhost

With Apache + proxy_fcgi, you usually set a handler in the vhost. Example:

<FilesMatch \.php$>
    SetHandler "proxy:unix:/run/php/php8.3-fpm-site1.sock|fcgi://localhost/"
</FilesMatch>

Test and reload:

sudo apachectl configtest
sudo systemctl reload apache2

Step 6: Add real observability: slowlog + correlation to 502/504

You already enabled slowlog and request_slowlog_timeout. Now make sure they capture the right failures.

  • Set the slowlog threshold low enough to catch pain (3–8 seconds is a solid starting range).
  • Make sure the web server doesn’t time out earlier than FPM does.

For Nginx, find your current timeouts. They might live in /etc/nginx/nginx.conf or in per-site configs:

grep -R "fastcgi_read_timeout" -n /etc/nginx | head
grep -R "proxy_read_timeout" -n /etc/nginx | head

If Nginx gives up at 60s but FPM kills scripts at 120s, you’ll see 504s while PHP keeps running. Keep these aligned.

A common pattern:

  • Nginx fastcgi_read_timeout: 120s
  • FPM request_terminate_timeout: 120s

When a page drags, tail the slowlog:

sudo tail -n 50 /var/log/php8.3-fpm/site1-slow.log

The slowlog gives you a stack trace and the script path. That often points to a plugin, a remote API call, a checkout hook, or raw PHP execution time.

Pair it with your access/error logs. For a step-by-step way to correlate timestamps (and request IDs when available), use this VPS log analysis tutorial.

Step 7: Tune pool limits without guessing

Now size each pool based on measured RSS. Then decide what must stay fast under load.

Set max_children to cap memory usage

If site1 pays the bills and site2 is a low-priority blog, allocate workers accordingly. Example on a 4 GB VPS:

  • Reserve ~1.2 GB OS + services
  • Reserve ~700 MB for DB/cache (if local)
  • Leaves ~2.1 GB for PHP workers
  • At 120 MB/worker ≈ 17 workers total across pools

That might translate to:

  • site1 pm.max_children = 12
  • site2 pm.max_children = 5

Apply changes and reload FPM:

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

Use pm.max_requests to reduce long-lived bloat

pm.max_requests recycles workers after N requests. For WordPress, 300–1000 is a useful range.

If RSS grows steadily over hours, lower it. If traffic is high and stable, keep it higher. That reduces churn from constant respawns.

Keep start/min/max spare servers modest on a VPS

Pre-forking too aggressively burns memory while the site is idle. For small pools, these values are a sensible default:

  • pm.start_servers = 2
  • pm.min_spare_servers = 2
  • pm.max_spare_servers = 4

For busy sites, increase gradually. Watch memory and swap as you go.

Step 8: Add a PHP-FPM status endpoint (optional, but useful)

Status gives you hard numbers: active processes, idle processes, and whether you’re hitting max_children. Hitting that limit is one of the most common causes of queueing and 502s.

Enable status and ping in each pool:

; Add to site1.conf
pm.status_path = /fpm-status
ping.path = /fpm-ping
ping.response = pong

Reload PHP-FPM:

sudo systemctl reload php8.3-fpm

Then restrict access at the web server.

Nginx status location (allow only your IP)

location = /fpm-status {
    allow 203.0.113.10;
    deny all;
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm-site1.sock;
}

location = /fpm-ping {
    allow 203.0.113.10;
    deny all;
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm-site1.sock;
}

Test:

curl -sS http://127.0.0.1/fpm-ping
curl -sS http://127.0.0.1/fpm-status | head

If you want external uptime checks plus alerting, plug this into your monitoring. The stack in this monitoring setup tutorial fits well for small hosting fleets.

Step 9: Fix two common WordPress pool killers

WP-Cron traffic bursts

On busy sites, WP-Cron tends to fire at the worst time. It can stack up PHP workers fast.

If scheduled posts miss or WooCommerce emails lag, fix cron properly first. Follow this WordPress cron troubleshooting tutorial, then re-check whether your pools still queue.

Bot spikes that exhaust workers

Even with “enough” max_children, junk traffic can keep PHP permanently busy. If you run Nginx, rate limiting on login and XML-RPC-style endpoints can free workers quickly.

Use this Nginx rate limiting tutorial before you simply crank worker counts.

Step 10: Safe change workflow (so you don’t take sites down)

Pool tuning is “tiny config edits.” Those same tiny edits can cause avoidable outages. Use the same workflow every time:

  1. Validate config: php-fpm8.3 -t
  2. Reload, don’t restart: systemctl reload php8.3-fpm
  3. Smoke test: load homepage + wp-admin, submit a form, checkout (if WooCommerce)
  4. Watch logs for 5 minutes: FPM error log + Nginx/Apache error log

Keep an escape hatch, too. If you lock yourself out, regaining SSH access becomes the job.

If your team shares admin access, a bastion host is usually cleaner than opening port 22 everywhere. This SSH jump host setup guide walks through a solid approach.

Troubleshooting: symptoms → likely cause → fix

  • 502 Bad Gateway spikes → pool hit pm.max_children or socket mismatch → raise max_children within RAM budget; verify vhost points to correct socket.
  • 504 Gateway Timeout → web server timeout lower than PHP execution time → align Nginx/Apache timeouts with request_terminate_timeout; use slowlog to find the offender.
  • Memory keeps climbing → plugin leak or cache behavior → lower pm.max_requests; review slowlog; consider reducing memory_limit for non-critical pools.
  • Site works on CLI but not web → permissions/user mismatch → ensure pool user owns files; check open_basedir or restrictive permissions.
  • Random slowness, no errors → swapping or disk pressure → confirm swap activity and disk space; if needed, follow a disk audit approach like “find what’s filling /var”.

Production checklist (copy/paste)

  • Each WordPress site has its own pool and Unix socket.
  • pm.max_children totals fit within your measured RAM budget.
  • request_terminate_timeout matches your Nginx/Apache timeouts.
  • Slowlog enabled at 5s (adjust per site).
  • Status endpoint restricted to admin IPs (optional but recommended).
  • Bot spikes mitigated before increasing workers.
  • Reload workflow used; no blind restarts.

Summary: a VPS-friendly way to keep WordPress fast under load

Per-site PHP-FPM pools prevent a single “bad neighbor” from taking out the rest of your VPS. Hard caps make memory use predictable.

Slow logs turn “the site is slow” into a concrete plugin, endpoint, or remote call you can fix. Once you’ve set this up once, adding more sites is mostly repetition. The remaining work is disciplined sizing.

If you run client sites or multiple WordPress installs, start with a VPS that gives you clean resource isolation. HostMyCode offers HostMyCode VPS plans for hands-on admins, and managed VPS hosting if you want help tuning, monitoring, and keeping pools stable during traffic spikes.

If your WordPress sites are fighting over RAM, move them to a VPS where you can isolate PHP-FPM pools properly and enforce per-site limits. A HostMyCode VPS gives you root access for this tuning, and managed VPS hosting can take on the ongoing performance, monitoring, and stability work.

FAQ

Should I use one PHP-FPM pool per domain or per vhost user?

Use one pool per Unix user. In most hosting setups that maps 1:1 to a domain. If a single user owns multiple small sites, keeping them in one pool can reduce overhead.

What’s a safe starting point for pm.max_children on a 2 GB VPS?

Start low: 4–8 total workers across all pools, then measure RSS under traffic. On 2 GB, you can run out of RAM fast, especially with WooCommerce.

Will increasing pm.max_children always make WordPress faster?

No. It can reduce queueing, but if you exceed RAM you’ll trigger swapping or OOM and performance collapses. Size to RAM first, then fix slow endpoints.

Do I need a status page if I already have server monitoring?

Status answers a question most monitoring can’t: “Are requests waiting because PHP-FPM ran out of workers?” Keep it locked to your IP and it’s low risk, high value.

How do I handle changes without downtime?

Validate with php-fpm8.3 -t, then systemctl reload. Avoid restarts during peak traffic. For rollbacks, keep pool config backups and revert the socket path in the vhost.

VPS PHP-FPM Pool Tuning Tutorial (2026): Faster WordPress With Per-Site Limits on Nginx or Apache | HostMyCode