Let’s get your server capable of running 100s or 1000s of concurrent PHP processes without blinking.
When people think about PHP and performance, memory tuning isn’t always the first thing that comes to mind. But if you’re running a website or app that needs to scale, especially one built with Laravel or WordPress, memory allocation and PHP-FPM tuning might be the difference between a snappy experience and a server meltdown.
Let’s walk through how to tune PHP and PHP-FPM for different memory environments — 8GB, 16GB, and 32GB — and understand the real cost of getting this wrong.
Memory Use in PHP Is About Scale, Not Just Size
Unlike C, you don’t manage memory in PHP by allocating and freeing it manually. Instead, PHP spins up a new process (or pool child) for each request, uses memory, and then recycles that memory when the request is done. This model is easy to reason about — until you start thinking in scale.
Every process uses memory. Multiply that by concurrent users, and things get expensive fast.
That means optimizing memory usage isn’t about maxing it out per process. It’s about reducing how much each process needs so you can run more of them simultaneously — without dipping into swap memory (a nightmare we’ll get to in a second).
The Cost of Getting It Wrong: Hello, Swap!
Let’s say your server has 8GB of RAM, and each PHP process uses 256MB. You’re capped at about 30 concurrent PHP workers — assuming nothing else is using RAM (which is never true). So, maybe you get 20–25 concurrent users before performance nosedives.
Now let’s say traffic spikes. More users hit the site, more processes are needed, and boom — you hit your RAM ceiling.
At that point, Linux starts using swap memory (aka: your hard drive as pretend RAM). It’s slow. Your CPU cries. Your site grinds.
You don’t want swap. It’s not a backup plan. It’s a red flag.
PHP-FPM + Memory = Throughput
Here’s where PHP-FPM tuning comes in. PHP-FPM is responsible for managing pools of PHP workers (aka: those individual processes). You tell it:
- How many processes to start (
pm.start_servers
) - The max number it can spin up (
pm.max_children
) - And how aggressive it should be (
pm.min_spare_servers
,pm.max_spare_servers
)
But here’s the catch: Each process eats memory. So, pm.max_children * average memory per process
must be less than your total usable RAM.
Let’s get practical.
Configuring for 8GB, 16GB, and 32GB Servers
Let’s assume your app uses ~100MB of memory per PHP process under load. You’ll want to confirm this by watching real usage with tools like top
, htop
, or ps
.
Rule of Thumb: Leave at least 1GB of RAM for the OS and other processes (like MySQL, Redis, Nginx).
8GB Server
- OS + other services: 1.5–2GB
- Available for PHP: ~6GB
- Memory per process: 100MB
pm = dynamic
pm.max_children = 60
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15
16GB Server
- Available for PHP: ~12–13GB
pm = dynamic
pm.max_children = 120
pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 30
32GB Server
- Available for PHP: ~28–29GB
pm = dynamic
pm.max_children = 280
pm.start_servers = 30
pm.min_spare_servers = 15
pm.max_spare_servers = 45
Don’t Forget About Nginx, Apache, and Your Database
When people calculate memory usage, they often obsess over PHP-FPM — and forget everything else.
If you’re running:
- Nginx or Apache to serve assets, route requests, and proxy to PHP
- MySQL or PostgreSQL to fetch and write data
- Redis, ElasticSearch, or any other services
They all eat memory. And they eat more of it under load.
Service | Typical Usage |
---|---|
Nginx | 50–150MB |
Apache | 300MB–1GB+ |
MySQL | 500MB–2GB |
Redis | ~100MB+ |
Your total usable RAM isn’t just for PHP-FPM. It’s shared. So budget accordingly.
The php.ini
Memory Limit Is Per Worker — Not a Global Limit
Here’s a huge misconception that causes a lot of people to overload their servers:
“I’ll raise the memory limit in
php.ini
to 4GB so my app has more headroom.”
Wrong. That memory limit is per process — meaning every single PHP-FPM worker can eat up to 4GB.
So let’s say:
- You’re on an 8GB VM
- You set
memory_limit = 4096M
- You configure PHP-FPM with
pm.max_children = 3
Boom. Three simultaneous users, and you’re already in swap. But, lets be honest, you probably set php.ini to 4GB and your max_childern
to 200 (that is 400GB of expected memory).
A Better Approach
Set memory_limit
in php.ini
to something that reflects the real use-case of your app:
- 64M–128M: For most typical PHP applications
- 256M: If you have complex tasks or image processing
- 512M+: Only if you’re doing large imports or PDF generation
Then, tune pm.max_children
so that all your processes together don’t exceed your free memory after accounting for Nginx, the DB, and the OS.
Also, instead of raising the default, you can set the memory limit per request in PHP for things like PDF generation within the PHP file being accessed like this:
<?php
ini_set('memory_limit', '512M');
This lets you handle memory-hungry tasks (like PDF exports or CSV imports) without sacrificing concurrency across your whole application.
Then, tune pm.max_children
so that all your processes together don’t exceed your free memory after accounting for Nginx, the DB, and the OS.
Running Multiple PHP Versions? Watch Your Idle Workers
If you’re hosting multiple apps on the same server — each with different PHP versions (like PHP 7.4, 8.1, 8.3) — you might assume they only eat memory when in use.
But here’s the catch:
Each version of PHP-FPM runs its own pool. That means its own workers. That means its own memory usage — even when idle.
If you’ve got:
php7.4-fpm
withpm.max_children = 20
php8.1-fpm
withpm.max_children = 20
php8.3-fpm
withpm.max_children = 20
Now you’ve got 60 workers potentially consuming memory, even if most of them are idle. Depending on your memory_limit
, that could mean 6–12GB+ of memory allocated before your apps do anything.
What To Do Instead
- Only run the versions you need. Disable unused ones.
- Use
pm = ondemand
for infrequently used versions. - Lower the pool settings on low-traffic versions.
pm = ondemand
pm.max_children = 5
pm.process_idle_timeout = 10s
Why Use pm = ondemand
for Infrequently Used PHP Versions?
When you configure PHP-FPM, you choose how it manages worker processes using the pm
setting. There are three modes:
Mode | Behavior |
---|---|
static | Spawns exactly pm.max_children workers at all times |
dynamic | Spawns between pm.min_spare_servers and pm.max_children , scaling up |
ondemand | Spawns zero workers at rest, only creating them on incoming traffic |
So if you have a PHP version installed just to support one legacy app that gets a few hits a day:
static
ordynamic
means workers are always sitting in memory, idle.ondemand
means no workers exist until they’re needed, and they shut down after inactivity.
This avoids wasting RAM on processes that aren’t doing anything.
Final Thoughts
PHP scales not by giving each request all the memory it wants, but by being stingy. With proper tuning, a 32GB server can handle hundreds of concurrent users (e.g., 400 at 70MB per process), while a 64GB server can exceed 1,000 (at 60MB each). Add proper caching, and you can support thousands of concurrent requests.
But if you misconfigure PHP-FPM or leave the default memory_limit
at 512M — expect swap, slowdowns, and angry users.
Scale is about how little each request needs, not how much each request gets.
Get your memory and PHP-FPM config right. Your future self (and your server) will thank you.
Leave a Reply