App Deployment·8 min read·

PHP-FPM Pools and Process Counts on 2 GB RAM

Size PHP-FPM pm.max_children on a 2 GB VPS so workers fit beside nginx and MySQL, then split pools if two apps should not share one process budget.

NB

Netbay Cloud Team

Netbay Engineering

On this page

PHP-FPM defaults are for a machine that does not exist on your VPS. The Ubuntu www pool uses pm=dynamic with max_children=5 on some images and much higher on others. On a 2 GB box that also runs nginx and MariaDB, an oversized pool OOMs. An undersized pool queues requests until nginx times out. Sizing is arithmetic, not folklore.

This post sizes a single pool for one app, then shows when to split pools. The host is Ubuntu 24.04, Intel Xeon Platinum, High-Speed SSD, 2 GB RAM, which is a common Lucknow plan for a Laravel or WordPress site.

2 GB RAM split for a PHP VPS OS + sshd ~250 MB nginx ~50 MB MariaDB ~300 MB PHP-FPM workers ~900 MB budget RSS ~80-100 MB each max_children = 8 to 10 leave 200 MB headroom Measure RSS on your app. Do not copy a 32 GB tutorial.

Measure RSS, then do the division

memory_limit is a cap, not the size of a worker. A Laravel request might sit at 80 to 120 MB RSS. WordPress with a page builder can be higher. Look at actual processes after traffic, not at php.ini.

ps -o pid,rss,cmd -C php-fpm8.3 gives RSS in kilobytes. Average the workers under load. Then:

available_for_php = 2048 - OS - nginx - mysql - redis - margin

max_children = floor(available_for_php / average_rss)

On a 2 GB VPS with MariaDB, a honest budget is about 800 to 1000 MB for FPM. At 100 MB RSS that is 8 to 10 children. At 50 MB RSS (a tiny API) you can go to 16. At 150 MB (heavy WP) you may only afford 6. Going over the budget is how the OOM killer picks php-fpm or mysqld.

Leave 150 to 200 MB free. Burst, page cache, and a composer run need it. High-Speed SSD swap is not a plan. If you add a 1 GB swap file, use it as a cushion, not as capacity you size against.

pm=ondemand versus dynamic versus static

ini
[www]
user = www-data
group = www-data
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 8
pm.process_idle_timeout = 10s
pm.max_requests = 500
request_terminate_timeout = 60s
php_admin_value[memory_limit] = 256M

ondemand spawns workers when requests arrive and reaps them after process_idle_timeout. On a low-traffic 2 GB VPS it is the right default because idle RAM goes back to the OS and to MariaDB's buffer pool.

dynamic keeps min spare servers warm. That helps latency on bursty sites and costs RAM at 3 a.m. Use dynamic when you have measured idle workers you can afford: pm.min_spare_servers=2, pm.max_spare_servers=4, pm.start_servers=2, pm.max_children=8.

static pre-forks max_children and never reaps. It is for busy boxes where fork cost matters and RAM is dedicated. On 2 GB it is usually wrong.

pm.max_requests recycles a worker after N jobs to contain leaks. 500 is a sane start. request_terminate_timeout should match or sit under nginx fastcgi_read_timeout. A 300s PHP timeout behind a 60s nginx timeout just leaves ghosts.

One pool per app

The default www pool is fine for one site. Two sites on one VPS should not share it. A WordPress traffic spike should not starve your API. Copy www.conf to site-a.conf and site-b.conf, give each a listen socket, a prefix, and its own max_children that sums to the RAM budget.

[site-a] listen = /run/php/php8.3-fpm-site-a.sock and pm.max_children=5. [site-b] similar with 3. nginx vhosts point at the matching socket. php_admin_value[open_basedir] can pin each pool to its tree if you want extra isolation.

Do not run a pool as root. Do not give two apps the same writable storage. Separate pools plus separate unix sockets is the isolation you get without containers.

Watch the listen queue

If access logs show 502/504 and FPM log says server reached pm.max_children, you are at capacity. Raising max_children is correct only if RAM allows. Otherwise add opcache, cut plugins, add a page cache, or move MariaDB. CPU on Xeon Platinum is rarely the first wall at this size; RAM and slow queries are.

status_path = /status with ping.path = /ping, allowed only from 127.0.0.1, gives listen queue and idle process counts. Do not expose /status on the public vhost.

After changing pool files, php-fpm8.3 -t && systemctl reload php8.3-fpm. A bad pool file prevents the whole daemon from starting if it is in pool.d/. Test first.

Takeaway

max_children is (RAM minus everyone else) divided by measured RSS, with a margin. ondemand is the 2 GB default. Split pools when apps share a box. Copy-pasting pm.max_children=50 from a 16 GB blog is how you meet the OOM killer.

You can spin up a 2 GB Ubuntu 24.04 instance on Netbay in under 60 seconds and size FPM against real RSS in Lucknow - netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance