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.
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.
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
[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] = 256Mondemand 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