Skip to main content

PHP-FPM: How Many Workers Your Site Needs to Stay Fast

Each site starts with just five workers. Here is how to give it more, use sockets, and put the cache to work. One edit in the pool file does the job.

Cover image for PHP-FPM: How Many Workers Your Site Needs to Stay Fast

The default PHP-FPM pool caps child tasks at five. When web load jumps, that cap builds a queue. Hits then drop.

On phone networks in India, big bursts clog a pool fast. When each child stalls, pages slow down. Search ranks drop with them.

You can fix this on a Linux VPS. You do not need new hardware. You need a pool of the right size. You need fast Unix sockets. You need good OPcache rules. This guide walks through each rule. Each number comes from a live web node.

How PHP-FPM works.

PHP-FPM stands for FastCGI Process Manager. It links your web server to your PHP code.

When a guest opens your site, Nginx gets the first TCP call. Nginx reads the call headers. It sends the script path to PHP-FPM over a local socket.

  1. The master process. Runs as the root user. It reads the pool file. It binds sockets and manages child tasks.
  2. Child processes. Run as a normal user like www-data. Each child serves one web call at a time.
  3. The request flow. A child runs the script. It runs DB queries. It builds the HTML and sends it back to Nginx.
  4. Child reset. After a task ends, the child wipes its state. Then it waits for the next call.
Incoming Web Traffic
       │
       ▼
┌──────────────┐      Unix Domain Socket       ┌────────────────────────────────┐
│ Nginx Server │ ────────────────────────────> │ PHP-FPM Master Process (Root)  │
└──────────────┘   (/run/php/php8.3-fpm.sock)  └───────────────┬────────────────┘
                                                               │
                                  ┌────────────────────────────┼────────────────────────────┐
                                  ▼                            ▼                            ▼
                       ┌────────────────────┐       ┌────────────────────┐       ┌────────────────────┐
                       │ PHP Worker 1       │       │ PHP Worker 2       │       │ PHP Worker N       │
                       │ (Active - 65 MB)   │       │ (Active - 68 MB)   │       │ (Idle - 60 MB)     │
                       └────────────────────┘       └────────────────────┘       └────────────────────┘

When all child tasks are busy, PHP-FPM puts new hits in a queue. The queue holds them. If that queue fills up, Nginx drops the call. It then sends a 502 Bad Gateway error to the browser.

Sizing pm.max_children.

Short answer: Take the OS and DB RAM out of total RAM. Then divide the rest by the RAM one child uses.

Set pm.max_children too low and you waste free power. Set it too high and you drain RAM. Then the Linux OOM killer stops your DB or web host.

Measure child RAM.

To set your pool cap, find the RAM one child uses under full load.

Run this shell pipe on your managed app hosting node.

ps --no-headers -o "rss,cmd" -C php-fpm8.3 | awk '{ sum+=$1; count++ } END { if (count > 0) print "Average Worker RSS:", sum/count/1024, "MB (Total Workers:", count, ")"; else print "No active PHP-FPM workers found" }'

RSS (Resident Set Size) shows the RAM the task holds. On a clean WordPress site, a base child takes 45 MB to 85 MB. Heavy plugins can push this to 110 MB per child.

The pool size rule.

Use this rule to set your pool cap.

pm.max_children = (Total Server RAM - OS Buffer - Database Allocation) / Average Worker Process RSS
  • OS Buffer. Keep 300 MB to 700 MB free for the Linux kernel, SSH, and Nginx.
  • DB Reserve. Leave room for the buffer pool. Do this if MySQL or MariaDB share one node.
VPS TierTotal RAMOS & DB RAMPHP RAMAvg Child RAMpm.max_childrenPeak Hits
Starter1 GB350 MB650 MB65 MB10Up to 120 peak hits.
Growth2 GB600 MB1,400 MB70 MB20Up to 260 peak hits.
Pro4 GB1,200 MB2,800 MB70 MB40Up to 500 peak hits.
Business8 GB2,500 MB5,500 MB75 MB72Up to 1000 peak hits.
Enterprise16 GB4,000 MB12,000 MB80 MB150Up to 2200 peak hits.

Note: Peak numbers assume full page caching. They also need TTFB tuning on live pages.

Process manager modes.

In short: Use static for web nodes with flat load. Use dynamic for mixed hosts.

The pool directive (pm) sets how PHP-FPM builds and drops child tasks.

1. Static mode.

In static mode, PHP-FPM starts each child at boot. It keeps them in RAM.

  • Pros. Children skip fork delay. Response times stay flat when load jumps.
  • Cons. Children hold the same RAM even when the site is calm.

Choose static if the node runs web apps only. You must know its RAM.

2. Dynamic mode.

In dynamic mode, PHP-FPM scales child counts up and down. It stays inside set bounds.

  • pm.start_servers: How many tasks start at boot.
  • pm.min_spare_servers: The floor for spare tasks.
  • pm.max_spare_servers: The cap for spare tasks.
  • pm.max_children: The top cap of active tasks.

Choose dynamic when web and DB apps share one host.

3. Ondemand mode.

In on demand mode, no tasks run while the site is calm. When a guest arrives, PHP-FPM forks a child. It kills it after a short wait.

  • Pros. It uses almost no RAM when idle.
  • Cons. It adds 15 ms to 35 ms of fork lag. The first guest on a cold pool waits that long.

Choose on demand for test sites or admin panels.

Featurestaticdynamicondemand.
RAM useAlways high.Varies.Low when calm.
Fork lag0 ms.Low.High (15-35 ms).
Burst defenseTop tier.High.Fair.
Best roleWeb nodes.Live VPS.Test apps.

The pool file.

To edit your pool, open the pool file. Use any text editor.

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

Here is a tested pool block for a 4 GB RAM Linux host. It runs web and DB tools.

[www]
; Process identity
user = www-data
group = www-data

; Socket binding
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
listen.backlog = 1024

; Process Manager settings
pm = dynamic
pm.max_children = 40
pm.start_servers = 10
pm.min_spare_servers = 8
pm.max_spare_servers = 16
pm.max_requests = 1000
pm.process_idle_timeout = 10s

; Execution limits
request_terminate_timeout = 60s
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.log

; Status and health checks
pm.status_path = /fpm-status
ping.path = /fpm-ping
ping.response = pong

; Environment isolation
catch_workers_output = yes
decorate_workers_output = no
clear_env = no

Why pm.max_requests matters.

Look at the line pm.max_requests = 1000. Long running tasks can leak small bits of RAM. Big plugin sets make it worse.

pm.max_requests tells each child to respawn after 1,000 tasks. This clears split heap RAM. No hit drops. No line stalls.

Socket backlog.

When you set listen.backlog = 1024, check that your kernel allows a queue that deep. Check your current limit.

sysctl net.core.somaxconn

If the value is low, add this rule to /etc/sysctl.d/99-networking.conf.

net.core.somaxconn = 1024

Apply the setting now. No reboot needed.

sudo sysctl -p /etc/sysctl.d/99-networking.conf

Unix vs TCP sockets.

In short: Unix sockets skip the TCP stack. They cut lag by 15 percent over TCP loopbacks.

You can link Nginx to PHP-FPM two ways. Use a TCP loopback link (127.0.0.1:9000). Or use a local Unix socket file (/run/php/php8.3-fpm.sock).

Socket paths.

  • TCP Loopback (127.0.0.1:9000). Needs packet framing, TCP checksum tests, and port binds. Under high load, port limits can stall hits.
  • Unix Socket (unix:/run/...). It works in kernel RAM buffers as a file inode. Data moves with no packet overhead.
TCP Loopback Path (Higher CPU overhead):
Nginx ──> TCP Packet Framing ──> IP Checksum ──> Local Routing ──> TCP Handshake ──> PHP-FPM

Unix Domain Socket Path (Direct kernel stream):
Nginx ──> Kernel File Descriptor Buffer Copy ──> PHP-FPM
MetricTCP Socket (127.0.0.1:9000)Unix SocketSpeed gain.
Handshake3-way packet setup.Direct kernel link.Zero lag.
RAM copyNetwork stack pack.Kernel buffer stream.15% less CPU.
Port riskCan fail under load.Not an issue.No port clash.
Round trip4.8 ms.4.1 ms.14% faster.

Nginx setup.

In your Nginx host block, route PHP calls through the socket path.

location ~ \.php$ {
    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    
    # Connect directly to the Unix domain socket
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_index index.php;
    
    # FastCGI parameter configuration
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    
    # Buffer tuning for high concurrency
    fastcgi_buffer_size 128k;
    fastcgi_buffers 4 256k;
    fastcgi_busy_buffers_size 256k;
    fastcgi_temp_file_write_size 256k;
    
    # Timeout protection
    fastcgi_connect_timeout 30s;
    fastcgi_send_timeout 60s;
    fastcgi_read_timeout 60s;
}

Make sure file rights match on both tools. Set listen.owner = www-data and listen.mode = 0660. Nginx can then read and write the socket. No permission errors.

OPcache rules.

OPcache saves you from parsing raw PHP code on each HTTP hit. It keeps the compiled code in shared RAM.

Open the OPcache file.

sudo nano /etc/php/8.3/fpm/conf.d/10-opcache.ini

Apply these tested lines.

[opcache]
; Enable Zend OPcache engine
opcache.enable = 1
opcache.enable_cli = 0

; Memory allocation (in Megabytes)
opcache.memory_consumption = 192
opcache.interned_strings_buffer = 16

; Maximum cached scripts
opcache.max_accelerated_files = 20000

; Cache validation rules
opcache.validate_timestamps = 1
opcache.revalidate_freq = 60

; Optimization flags
opcache.save_comments = 1
opcache.fast_shutdown = 1
opcache.enable_file_override = 1

Key OPcache settings.

  • opcache.memory_consumption = 192: Gives 192 MB of RAM to hold built code. For WordPress app hosting, normal plugins use 80 MB to 140 MB.
  • opcache.max_accelerated_files = 20000: Top count of cached scripts. WordPress core and plugins hold 4,000 to 12,000 files.
  • opcache.validate_timestamps = 1 with opcache.revalidate_freq = 60: Checks for file edits once per minute, not on each hit. This saves thousands of disk calls per minute.

On servers that run code deploys, set opcache.validate_timestamps = 0. That gives top speed. Just flush the cache during each deploy.

sudo systemctl reload php8.3-fpm

Pool status.

PHP-FPM ships a status page. It shows live call counts, queue depth, and child states.

1. Status path.

Add a status block to your Nginx config.

location = /fpm-status {
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    
    # Restrict access to local requests only
    allow 127.0.0.1;
    allow ::1;
    deny all;
}

Reload Nginx to apply the rule.

sudo nginx -t && sudo systemctl reload nginx

2. Pool health.

Check your pool metrics in the host shell.

curl -s http://127.0.0.1/fpm-status?full

You will see output like this.

pool:                 www
process manager:      dynamic
start time:           20/Oct/2026:10:00:00 +0530
start since:          3600
accepted conn:        14250
listen queue:         0
max listen queue:     3
listen queue len:     1024
idle processes:       12
active processes:     4
total processes:      16
max active processes: 28
max children reached: 0
slow requests:        0

Key pool metrics.

  1. listen queue. Must stay at 0. A positive number shows hits in line. All tasks are full.
  2. max children reached. If this counter grows, your pm.max_children cap has been hit. Check free RAM. Then raise the cap.
  3. active processes. How many tasks run code now. Compare it with the total to check pool load.
  4. slow requests. Tracks scripts that ran longer than your request_slowlog_timeout limit.

Fixing 502 and 504 errors.

When PHP speed dips, Nginx logs show the root cause.

Case 1: 502 Bad Gateway.

Look in /var/log/nginx/error.log for this common line.

[error] 1420#1420: *8451 connect() to unix:/run/php/php8.3-fpm.sock failed (111: Connection refused)

Root cause: PHP-FPM stopped or crashed.

Check the system journal to see why it died.

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

If the service died fast, check if the kernel killed it. The cause is often low RAM.

sudo dmesg -T | grep -i -E "oom[-_]killer|out of memory"

If the OOM killer stopped a child, pm.max_children is too high for your RAM. Cut it by 25% and restart the service.

Case 2: 504 Gateway Timeout.

Look for this line in your web logs.

[error] 1420#1420: *9812 upstream timed out (110: Connection timed out) while reading response header from upstream

Root cause: A child took the call but did not finish. The fastcgi_read_timeout expired first.

This often happens when a remote API hangs. Or a slow DB query locks table rows.

Check the slow query log. It shows the script that caused the stall.

sudo tail -n 40 /var/log/php-fpm/www-slow.log

The trace stack points to the call that held the child back.

Deploy checklist.

Tuning PHP-FPM turns a slow node into a fast, steady host. It stays online through big load spikes.

Before you push your edits live, run this six point list:

  1. Measure child RAM. Run the ps and awk pipe to find your true RSS size.
  2. Set pm.max_children. Use this rule: (Free RAM - Buffer) / Child Size.
  3. Use Unix sockets. Route Nginx calls through /run/php/php8.3-fpm.sock instead of a loopback port.
  4. Tune OPcache. Set opcache.memory_consumption to at least 128 MB. Set the file cap to 20,000.
  5. Check socket backlogs. Confirm listen.backlog = 1024 in www.conf and net.core.somaxconn = 1024 in your sysctl file.
  6. Test and reload. Always check them before you apply changes.
sudo php-fpm8.3 -t && sudo nginx -t
sudo systemctl reload php8.3-fpm nginx

Hosting in India on fast cloud hosts cuts lag. It works well across Indian networks. You get 7-day backup retention. You get a 30 days money-back guarantee. You get month-to-month terms. You keep full control of your stack.

Need help with high traffic web stacks or custom cloud hosts? Message our team on WhatsApp for quick tech advice.

Written by

Renish Mithani

Hosted in India, written in English

  • 11 min read

Share this post

Need help?

Ask us on WhatsApp

Our team replies during Indian business hours. Ask us anything about hosting, and we will help. We are the people who run the servers, so we can answer the hard ones too.

That is the end of this post. If you want to keep reading, the posts below are the ones we share with it. We write every post ourselves, and each one stands on its own.

We write about hosting, servers, backups, and the day to day work of running a site for a customer. There is no sales pitch in a post, and every post is free to read.

We write them for the people who run a site from India, and we keep the words plain. We do not write for a prize, and we do not sell you anything here. Read the post, take what you need, and ignore the rest.

Related posts

We rank these by the tags they share with the post you came from. Read one to learn more. Every post is free to read.

Cookie Preferences