DEV Community

Cover image for Tuning PHP-FPM for Laravel: Workers, Memory, and Process Management
Deploynix
Deploynix

Posted on • Originally published at deploynix.io

Tuning PHP-FPM for Laravel: Workers, Memory, and Process Management

PHP-FPM is the process manager that sits between Nginx and your Laravel application. Every web request that reaches your server ultimately becomes a PHP-FPM worker processing your code. How many workers you run, how much memory each one can use, and how the process manager spawns and recycles them directly determines your application's throughput, response time, and stability.

Most Laravel applications run on default PHP-FPM settings. These defaults are designed to be safe for any application on any server, which means they are optimal for nothing in particular. Tuning PHP-FPM for your specific application and server resources can double your throughput or cut your response time in half.

This guide explains every important PHP-FPM setting, how to calculate the right values for your Deploynix-managed server, and how to monitor the results.

How PHP-FPM Processes Work

When PHP-FPM starts, it creates a master process and a pool of worker processes. Each worker can handle one request at a time. When Nginx passes a request to PHP-FPM, an available worker picks it up, executes your Laravel application (bootstrapping the framework, running middleware, calling the controller, rendering the response), and then becomes available for the next request.

If all workers are busy when a new request arrives, it waits in a queue. If the queue fills up, new requests get a 502 error from Nginx. The number of workers directly determines how many simultaneous requests your application can handle.

Each worker is a separate PHP process with its own memory space. A typical Laravel application uses 30-80 MB per worker, depending on the number of loaded classes, cached data, and request complexity. A complex request (generating a PDF, processing a large dataset) might use 128 MB or more.

Process Manager Modes: Static vs Dynamic vs OnDemand

PHP-FPM offers three process management modes. Each has different trade-offs between resource usage and response time.

Static Mode (pm = static)

In static mode, PHP-FPM starts a fixed number of workers when it boots and keeps them running permanently. No workers are created or destroyed based on traffic.

pm = static
pm.max_children = 10
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Predictable memory usage — you always know exactly how much RAM PHP-FPM is using
  • No overhead from spawning new workers during traffic spikes
  • Fastest response to requests because workers are always warm and ready
  • No latency penalty from process creation

Cons:

  • Workers consume memory even during idle periods
  • If you set max_children too high, you waste RAM; too low, and you cannot handle peak traffic

Best for: Servers dedicated to a single application with consistent traffic patterns and enough RAM to keep all workers running full-time. This is the recommended mode for production Laravel applications on servers with 4 GB or more RAM.

Dynamic Mode (pm = dynamic)

Dynamic mode starts a base number of workers and scales up or down based on demand.

pm = dynamic
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
Enter fullscreen mode Exit fullscreen mode
  • start_servers: Number of workers to create on startup
  • min_spare_servers: Minimum idle workers to keep running (PHP-FPM spawns new workers if spare count drops below this)
  • max_spare_servers: Maximum idle workers (PHP-FPM kills excess idle workers above this)
  • max_children: Absolute maximum workers regardless of demand

Pros:

  • Adapts to traffic patterns — uses less memory during quiet periods
  • Good balance between resource efficiency and performance

Cons:

  • Spawning workers takes time (10-50ms) — during sudden traffic spikes, the first few requests may be slower
  • More complex to configure correctly — four interrelated parameters

Best for: Servers running multiple applications or servers with limited RAM (1-2 GB) where keeping idle workers running is wasteful. This is the default mode and works well for most Deploynix-managed servers.

OnDemand Mode (pm = ondemand)

OnDemand mode starts with zero workers and creates them only when requests arrive. Idle workers are killed after a timeout.

pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 10s
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Minimal resource usage during idle periods — perfect for servers running many low-traffic sites
  • Simple configuration — only two parameters

Cons:

  • Every request after an idle period incurs worker spawn overhead
  • The first request after idle can take noticeably longer (50-200ms extra)
  • Poor for applications with steady traffic — constantly creating and destroying workers wastes CPU

Best for: Development servers or servers hosting many sites where most are rarely accessed. Not recommended for production Laravel applications with regular traffic.

Calculating max_children

The most important PHP-FPM setting is max_children — the maximum number of workers that can exist simultaneously. Too few and requests queue up. Too many and the server runs out of memory, triggering the OOM killer (which may kill MySQL, Valkey, or the PHP-FPM master process).

The Formula

max_children = (Total Available RAM - RAM used by other services) / Average RAM per PHP worker
Enter fullscreen mode Exit fullscreen mode

Measuring RAM Per Worker

Check your current worker memory usage:

ps -eo pid,rss,comm | grep php-fpm | awk '{sum+=$2; count++} END {print "Average:", sum/count/1024, "MB"}'
Enter fullscreen mode Exit fullscreen mode

For a typical Laravel application:

  • Simple CRUD app: 30-40 MB per worker
  • Medium complexity (API + Blade + queues): 40-60 MB per worker
  • Complex (PDF generation, image processing, large datasets): 60-120 MB per worker

Examples by Server Size

1 GB RAM server (DigitalOcean/Vultr $5/month):

  • Available for PHP-FPM: ~400 MB (after MySQL, Nginx, Valkey, OS)
  • At 40 MB per worker: max_children = 10
  • At 60 MB per worker: max_children = 6
  • Recommended: Start with 5, monitor, adjust

2 GB RAM server (Hetzner $3-5/month):

  • Available for PHP-FPM: ~1000 MB
  • At 40 MB per worker: max_children = 25
  • At 60 MB per worker: max_children = 16
  • Recommended: Start with 10, monitor, adjust

4 GB RAM server (Hetzner $5-10/month):

  • Available for PHP-FPM: ~2500 MB
  • At 40 MB per worker: max_children = 62
  • At 60 MB per worker: max_children = 41
  • Recommended: Start with 20, monitor, adjust

8 GB RAM server (Hetzner $10-20/month):

  • Available for PHP-FPM: ~5500 MB
  • At 40 MB per worker: max_children = 137
  • At 60 MB per worker: max_children = 91
  • Recommended: Start with 50, monitor, adjust

These recommendations intentionally leave headroom. RAM usage per worker varies between requests, and some requests consume significantly more than the average.

Memory Limit Configuration

PHP's memory_limit setting controls how much memory a single PHP process can allocate. This is separate from the actual memory used by a worker.

memory_limit = 256M
Enter fullscreen mode Exit fullscreen mode

Key distinction: memory_limit is a ceiling, not an allocation. A worker with memory_limit = 256M might use only 40 MB for most requests. But if a request tries to allocate more than the limit (loading a large CSV into memory, for example), PHP kills the process with a fatal error.

Recommendations

  • Web requests: 256 MB is the default on Deploynix-managed servers and sufficient for the vast majority of Laravel applications
  • Queue workers: 256-512 MB gives headroom for data-intensive jobs
  • Artisan commands: 512 MB or higher for imports, exports, and bulk operations (Deploynix sets CLI memory_limit to 512 MB by default)

You can set different memory limits for web and CLI contexts. On Deploynix-managed servers, custom settings are placed in /etc/php/8.4/fpm/conf.d/99-deploynix.ini for FPM and /etc/php/8.4/cli/conf.d/99-deploynix.ini for CLI.

The Relationship Between memory_limit and max_children

If memory_limit = 256M and max_children = 20, the theoretical maximum PHP-FPM memory usage is 5,120 MB (5 GB). Deploynix uses exactly these defaults, which is safe for servers with 4 GB or more RAM since workers rarely hit their memory limit simultaneously. On smaller servers, consider lowering max_children.

In practice, workers rarely hit their memory limit simultaneously. But you should calculate the worst case and ensure it does not exceed available RAM. Lower the memory_limit for web requests to constrain the worst case, and investigate requests that routinely use high memory.

Slow Log: Finding Your Bottlenecks

PHP-FPM includes a slow log feature that captures stack traces for requests exceeding a configurable execution time. This is invaluable for identifying what makes your application slow.

Configuration

slowlog = /var/log/php8.4-fpm-slow.log
request_slowlog_timeout = 5s
Enter fullscreen mode Exit fullscreen mode

Any request taking longer than 5 seconds triggers a stack trace dump showing exactly which function was executing when the timeout was reached.

Reading Slow Log Output

A typical slow log entry:

[18-Mar-2026 14:23:45]  [pool www] pid 12345
script_filename = /home/deploynix/myapp/current/public/index.php
[0x00007f8a2c0] Illuminate\Database\Connection->select()
[0x00007f8a2d0] Illuminate\Database\Query\Builder->runSelect()
[0x00007f8a2e0] App\Models\Report->generateMonthlyStats()
[0x00007f8a2f0] App\Http\Controllers\ReportController->show()
Enter fullscreen mode Exit fullscreen mode

This immediately tells you that the generateMonthlyStats() method is executing a slow database query. Without the slow log, you would know the request was slow but not why.

Practical Usage

Set request_slowlog_timeout to a value slightly above your desired response time:

  • Target response time: 200ms → Set slow log to 1s (captures significantly slow requests without noise)
  • Target response time: 500ms → Set slow log to 2s

Review the slow log weekly. Look for patterns — the same function appearing repeatedly indicates a systematic issue that optimization will resolve.

Process Recycling: pm.max_requests

pm.max_requests = 500
Enter fullscreen mode Exit fullscreen mode

This setting tells PHP-FPM to recycle a worker after it has handled 500 requests. The worker process is killed and a new one is spawned. This prevents memory leaks from accumulating over time.

Why This Matters

PHP is designed for request-response cycles, and each request should be independent. But in practice, some extensions, packages, or application code leak small amounts of memory per request. Over thousands of requests, a worker that started at 40 MB might grow to 80 MB or more.

Setting pm.max_requests = 500 ensures no worker lives long enough for leaks to cause problems. The cost is occasional worker recycling overhead, which is negligible (one new process every 500 requests).

Tuning the Value

  • 0 (disabled): Workers never recycle. Only use this if you are certain your application has no memory leaks.
  • 100-200: Aggressive recycling. Good for applications with known memory leaks.
  • 500-1000: Balanced recycling. Recommended for most Laravel applications.
  • 5000+: Minimal recycling. Only for applications with confirmed stable memory usage.

Monitor worker memory over time. If workers grow significantly between recycling, reduce pm.max_requests. If worker memory is stable, you can increase it safely.

Request Timeout: request_terminate_timeout

request_terminate_timeout = 60s
Enter fullscreen mode Exit fullscreen mode

This is the maximum time a PHP-FPM worker will spend processing a single request before being killed. This prevents runaway requests (infinite loops, extremely slow queries, unresponsive API calls) from permanently occupying a worker.

Recommendations

  • Web requests: 30-60 seconds. If a web request takes longer than 30 seconds, something is wrong — the user has likely already navigated away.
  • If using Octane: FrankenPHP, Swoole, and RoadRunner have their own timeout mechanisms. Coordinate these with PHP-FPM's timeout to avoid conflicts.

Set this value lower than Nginx's fastcgi_read_timeout. If PHP-FPM kills the request first, it sends a proper error response. If Nginx times out first, the user gets a generic 504 without useful information.

Monitoring PHP-FPM Performance

PHP-FPM Status Page

You can enable the PHP-FPM status page to see real-time worker utilization by adding the following to your pool configuration:

pm.status_path = /status
Enter fullscreen mode Exit fullscreen mode

Then access it through Nginx (restricted to localhost or your IP):

pool:                 www
process manager:      dynamic
start time:           18/Mar/2026:10:00:00
start since:          14400
accepted conn:        28650
listen queue:         0
max listen queue:     3
listen queue len:     0
idle processes:       5
active processes:     3
total processes:      8
max active processes: 10
max children reached: 0
Enter fullscreen mode Exit fullscreen mode

Key metrics to watch:

  • listen queue: Requests waiting for a worker. If this is regularly above 0, you need more workers.
  • max listen queue: Peak queue depth. If this exceeds 10-20, your max_children is too low for your traffic.
  • max children reached: Number of times all workers were busy. If this increments, you need more workers or faster request processing.
  • idle processes vs active processes: The ratio tells you if your worker count is appropriate. If idle processes are always high, you have too many workers.

Deploynix Monitoring Integration

Deploynix's real-time monitoring shows CPU and memory usage at the server level. Correlate these metrics with PHP-FPM performance:

  • High CPU, high active processes: Your application is CPU-bound. Optimize slow code or upgrade CPU.
  • High memory, many workers: You are close to RAM limits. Reduce max_children or upgrade RAM.
  • Low CPU, high listen queue: Workers are waiting on I/O (database, external APIs). Optimize queries or add more workers.

Complete Configuration Template

Here is a tuned PHP-FPM pool configuration for a 4 GB Deploynix-managed server running a single Laravel application:

[www]
user = www-data
group = www-data

listen = /var/run/php/php8.4-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 2
pm.max_spare_servers = 8
pm.max_requests = 500
pm.process_idle_timeout = 10s

request_terminate_timeout = 60s
request_slowlog_timeout = 5s
slowlog = /var/log/php8.4-fpm-slow.log

catch_workers_output = yes
Enter fullscreen mode Exit fullscreen mode

For a 2 GB server:

pm = dynamic
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
Enter fullscreen mode Exit fullscreen mode

For an 8 GB dedicated app server (no database on the same machine):

pm = static
pm.max_children = 50
pm.max_requests = 1000
Enter fullscreen mode Exit fullscreen mode

Testing Your Configuration

After changing PHP-FPM settings, validate and restart:

php-fpm8.4 -t
systemctl restart php8.4-fpm
Enter fullscreen mode Exit fullscreen mode

Then load test your application to verify the configuration handles your expected traffic. A simple tool like wrk or ab can simulate concurrent requests:

wrk -t4 -c100 -d30s https://your-app.com/
Enter fullscreen mode Exit fullscreen mode

Watch for:

  • Response time stability under load
  • Listen queue staying at or near zero
  • Memory usage staying within safe limits
  • No OOM kills in system logs

Conclusion

PHP-FPM tuning is not guesswork when you understand the underlying model: workers consume memory, each request needs one worker, and your server has finite memory. The calculation is arithmetic. The art is in monitoring real-world usage and adjusting.

Top comments (0)