{"id":3710,"date":"2025-12-29T23:14:52","date_gmt":"2025-12-29T20:14:52","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/isolating-php-session-and-queue-workers-with-php-fpm-supervisor-and-systemd\/"},"modified":"2025-12-29T23:14:52","modified_gmt":"2025-12-29T20:14:52","slug":"isolating-php-session-and-queue-workers-with-php-fpm-supervisor-and-systemd","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/isolating-php-session-and-queue-workers-with-php-fpm-supervisor-and-systemd\/","title":{"rendered":"Isolating PHP Session and Queue Workers with PHP\u2011FPM, Supervisor and systemd"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>On a busy PHP application server, using a single PHP\u2011FPM pool for everything \u2013 web requests, admin panel, API calls, queue workers and cron jobs \u2013 quickly turns into a bottleneck. A long\u2011running report or a stuck queue worker can block precious PHP\u2011FPM children, increase response times and make the whole stack feel unstable. The good news: you can avoid most of this pain by cleanly isolating session traffic and background workers with the tools you already have: PHP\u2011FPM, Supervisor and systemd.<\/p>\n<p>In this article we\u2019ll walk through a practical pool architecture we use at dchost.com for PHP sites and frameworks like Laravel, Symfony, WordPress and custom apps. We\u2019ll look at why isolation matters, how to design separate PHP\u2011FPM pools for web vs queue vs CLI, where Supervisor and systemd fit in, and how to choose resource limits so the store checkout stays fast even when heavy jobs are running in the background.<\/p>\n<div id=\"toc_container\" class=\"toc_transparent no_bullets\"><p class=\"toc_title\">\u0130&ccedil;indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#Why_You_Should_Isolate_PHP_Sessions_and_Queue_Workers\"><span class=\"toc_number toc_depth_1\">1<\/span> Why You Should Isolate PHP Sessions and Queue Workers<\/a><ul><li><a href=\"#The_classic_singlepool_problem\"><span class=\"toc_number toc_depth_2\">1.1<\/span> The classic single\u2011pool problem<\/a><\/li><li><a href=\"#What_isolation_gives_you_in_practice\"><span class=\"toc_number toc_depth_2\">1.2<\/span> What isolation gives you in practice<\/a><\/li><\/ul><\/li><li><a href=\"#The_Building_Blocks_PHPFPM_Pools_Supervisor_and_systemd\"><span class=\"toc_number toc_depth_1\">2<\/span> The Building Blocks: PHP\u2011FPM Pools, Supervisor and systemd<\/a><ul><li><a href=\"#PHPFPM_pools_as_isolation_units\"><span class=\"toc_number toc_depth_2\">2.1<\/span> PHP\u2011FPM pools as isolation units<\/a><\/li><li><a href=\"#Supervisor_keeping_CLI_workers_alive\"><span class=\"toc_number toc_depth_2\">2.2<\/span> Supervisor: keeping CLI workers alive<\/a><\/li><li><a href=\"#systemd_units_slices_and_timers\"><span class=\"toc_number toc_depth_2\">2.3<\/span> systemd: units, slices and timers<\/a><\/li><\/ul><\/li><li><a href=\"#Session_and_Worker_Isolation_Architecture_Patterns\"><span class=\"toc_number toc_depth_1\">3<\/span> Session and Worker Isolation: Architecture Patterns<\/a><ul><li><a href=\"#Pattern_1_Separate_PHPFPM_pools_for_web_vs_queue\"><span class=\"toc_number toc_depth_2\">3.1<\/span> Pattern 1: Separate PHP\u2011FPM pools for web vs queue<\/a><\/li><li><a href=\"#Pattern_2_Isolating_sessionheavy_vs_stateless_traffic\"><span class=\"toc_number toc_depth_2\">3.2<\/span> Pattern 2: Isolating session\u2011heavy vs stateless traffic<\/a><\/li><li><a href=\"#Pattern_3_Sessions_and_cache_storage_isolation\"><span class=\"toc_number toc_depth_2\">3.3<\/span> Pattern 3: Sessions and cache storage isolation<\/a><\/li><\/ul><\/li><li><a href=\"#Designing_Web_vs_Worker_Resources_and_Limits\"><span class=\"toc_number toc_depth_1\">4<\/span> Designing Web vs Worker Resources and Limits<\/a><ul><li><a href=\"#Different_latency_expectations\"><span class=\"toc_number toc_depth_2\">4.1<\/span> Different latency expectations<\/a><\/li><li><a href=\"#Example_sizing_pools_on_a_4_vCPU_8_GB_RAM_VPS\"><span class=\"toc_number toc_depth_2\">4.2<\/span> Example: sizing pools on a 4 vCPU \/ 8 GB RAM VPS<\/a><\/li><li><a href=\"#Using_systemd_slices_and_priorities\"><span class=\"toc_number toc_depth_2\">4.3<\/span> Using systemd slices and priorities<\/a><\/li><\/ul><\/li><li><a href=\"#Putting_It_Together_Example_Implementation\"><span class=\"toc_number toc_depth_1\">5<\/span> Putting It Together: Example Implementation<\/a><ul><li><a href=\"#1_Define_multiple_PHPFPM_pools\"><span class=\"toc_number toc_depth_2\">5.1<\/span> 1. Define multiple PHP\u2011FPM pools<\/a><\/li><li><a href=\"#2_Route_requests_to_the_right_pool_via_Nginx\"><span class=\"toc_number toc_depth_2\">5.2<\/span> 2. Route requests to the right pool via Nginx<\/a><\/li><li><a href=\"#3_Configure_queue_workers_with_Supervisor\"><span class=\"toc_number toc_depth_2\">5.3<\/span> 3. Configure queue workers with Supervisor<\/a><\/li><li><a href=\"#4_Or_run_workers_via_systemd_units\"><span class=\"toc_number toc_depth_2\">5.4<\/span> 4. Or run workers via systemd units<\/a><\/li><li><a href=\"#5_Observability_separate_logs_and_metrics\"><span class=\"toc_number toc_depth_2\">5.5<\/span> 5. Observability: separate logs and metrics<\/a><\/li><\/ul><\/li><li><a href=\"#Relating_This_to_PerSite_PHPFPM_Pool_Architecture\"><span class=\"toc_number toc_depth_1\">6<\/span> Relating This to Per\u2011Site PHP\u2011FPM Pool Architecture<\/a><ul><li><a href=\"#Persite_and_perrole_pools_together\"><span class=\"toc_number toc_depth_2\">6.1<\/span> Per\u2011site and per\u2011role pools together<\/a><\/li><li><a href=\"#When_to_move_beyond_a_single_server\"><span class=\"toc_number toc_depth_2\">6.2<\/span> When to move beyond a single server<\/a><\/li><\/ul><\/li><li><a href=\"#Operational_Tips_and_Common_Pitfalls\"><span class=\"toc_number toc_depth_1\">7<\/span> Operational Tips and Common Pitfalls<\/a><ul><li><a href=\"#Avoiding_deadlocks_and_session_contention\"><span class=\"toc_number toc_depth_2\">7.1<\/span> Avoiding deadlocks and session contention<\/a><\/li><li><a href=\"#Graceful_deploys_with_queue_and_pool_isolation\"><span class=\"toc_number toc_depth_2\">7.2<\/span> Graceful deploys with queue and pool isolation<\/a><\/li><li><a href=\"#Testing_before_enabling_in_production\"><span class=\"toc_number toc_depth_2\">7.3<\/span> Testing before enabling in production<\/a><\/li><\/ul><\/li><li><a href=\"#Conclusion_Calm_PHP_Servers_Through_Thoughtful_Isolation\"><span class=\"toc_number toc_depth_1\">8<\/span> Conclusion: Calm PHP Servers Through Thoughtful Isolation<\/a><\/li><\/ul><\/div>\n<h2><span id=\"Why_You_Should_Isolate_PHP_Sessions_and_Queue_Workers\">Why You Should Isolate PHP Sessions and Queue Workers<\/span><\/h2>\n<h3><span id=\"The_classic_singlepool_problem\">The classic single\u2011pool problem<\/span><\/h3>\n<p>Most PHP applications start with the default setup: one PHP\u2011FPM pool, one user, one set of limits. Nginx or Apache sends all traffic to the same unix socket or TCP port. At first this is fine, but as soon as you add:<\/p>\n<ul>\n<li>Queued jobs (emails, imports, webhooks, video processing)<\/li>\n<li>Long\u2011running reports or exports from the admin panel<\/li>\n<li>API consumers that can spike traffic at any time<\/li>\n<li>High\u2011value sessions (logged\u2011in customers, admin users)<\/li>\n<\/ul>\n<p>you hit a frustrating pattern: someone runs a big export or a queue worker misbehaves, PHP\u2011FPM children get stuck, and suddenly normal page loads wait behind background work. Checkout pages, login forms and simple product pages suffer because they share the same pool with CPU\u2011hungry jobs.<\/p>\n<h3><span id=\"What_isolation_gives_you_in_practice\">What isolation gives you in practice<\/span><\/h3>\n<p>By isolating PHP sessions and queue workers into separate PHP\u2011FPM pools and managed processes, you gain:<\/p>\n<ul>\n<li><strong>Predictable response times for web sessions<\/strong>: session\u2011bearing requests use a dedicated, tightly sized pool that is never starved by queue workers.<\/li>\n<li><strong>Safe capacity for background jobs<\/strong>: queues get their own pool and processes; if they spike, web users don\u2019t feel it as much.<\/li>\n<li><strong>Cleaner failure domains<\/strong>: if a queue worker leaks memory, crashes or deadlocks, it doesn\u2019t take the entire site down.<\/li>\n<li><strong>Security separation<\/strong>: you can run web pools and worker pools under different Unix users, with different filesystem and network access.<\/li>\n<li><strong>More accurate tuning<\/strong>: each pool has its own <code>pm.max_children<\/code>, <code>pm.max_requests<\/code>, timeouts and INI settings tuned for its workload.<\/li>\n<\/ul>\n<p>We\u2019ve written before about <a href=\"https:\/\/www.dchost.com\/blog\/en\/why-background-jobs-matter-so-much-on-a-vps\/\">why background jobs matter so much on a VPS<\/a>; the missing piece for many teams is giving those jobs their own resource \u201clane\u201d instead of mixing them with interactive traffic.<\/p>\n<h2><span id=\"The_Building_Blocks_PHPFPM_Pools_Supervisor_and_systemd\">The Building Blocks: PHP\u2011FPM Pools, Supervisor and systemd<\/span><\/h2>\n<h3><span id=\"PHPFPM_pools_as_isolation_units\">PHP\u2011FPM pools as isolation units<\/span><\/h3>\n<p>At the heart of this architecture are multiple PHP\u2011FPM pools. Each pool is essentially a mini\u2011PHP runtime with its own:<\/p>\n<ul>\n<li>User and group (file permissions, OS\u2011level isolation)<\/li>\n<li>Process manager settings (<code>pm<\/code>, <code>pm.max_children<\/code>, <code>pm.max_requests<\/code>)<\/li>\n<li>PHP INI overrides (memory_limit, max_execution_time, opcache, etc.)<\/li>\n<li>Listen socket (e.g. <code>\/run\/php-fpm-web.sock<\/code>, <code>\/run\/php-fpm-queue.sock<\/code>)<\/li>\n<\/ul>\n<p>In practice, we usually define at least three pools for a serious PHP application:<\/p>\n<ul>\n<li><strong>web<\/strong>: for normal HTTP requests (frontend, API, maybe admin)<\/li>\n<li><strong>session\u2011critical<\/strong>: sometimes a smaller dedicated pool for cart\/checkout or login flows<\/li>\n<li><strong>queue<\/strong>: for worker processes that run CLI entrypoints (Laravel queue workers, custom daemons)<\/li>\n<\/ul>\n<p>For a deeper dive into tuning pool parameters like <code>pm.max_children<\/code> and <code>pm.max_requests<\/code> specifically for high\u2011traffic PHP apps, you can check our guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/wordpress-ve-woocommerce-icin-php-fpm-ayarlari-pm-pm-max_children-ve-pm-max_requests-hesaplama-rehberi\/\">PHP\u2011FPM settings for WordPress and WooCommerce<\/a>. The logic applies almost one\u2011to\u2011one to Laravel, Symfony and custom frameworks as well.<\/p>\n<h3><span id=\"Supervisor_keeping_CLI_workers_alive\">Supervisor: keeping CLI workers alive<\/span><\/h3>\n<p><strong>Supervisor<\/strong> is a simple, battle\u2011tested process manager for long\u2011running CLI programs. It\u2019s ideal for things like:<\/p>\n<ul>\n<li><code>php artisan queue:work<\/code> or <code>php artisan horizon<\/code> in Laravel<\/li>\n<li>Symfony Messenger workers<\/li>\n<li>Custom PHP daemons (e.g. websocket bridges, importers)<\/li>\n<\/ul>\n<p>Supervisor handles:<\/p>\n<ul>\n<li>Automatic restart when a worker exits<\/li>\n<li>Process count (how many workers per queue)<\/li>\n<li>Logging stdout\/stderr to dedicated log files<\/li>\n<\/ul>\n<p>Even if you later move to pure systemd units, Supervisor is often the easiest way to start isolating queue workers without changing how your app is built.<\/p>\n<h3><span id=\"systemd_units_slices_and_timers\">systemd: units, slices and timers<\/span><\/h3>\n<p>On modern Linux distributions, <strong>systemd<\/strong> is the init system responsible for services. It gives you:<\/p>\n<ul>\n<li><strong>Service units<\/strong>: to run PHP workers or Scheduler commands as managed daemons<\/li>\n<li><strong>Resource control<\/strong>: CPU, memory and IO limits using cgroups and slices<\/li>\n<li><strong>Timers<\/strong>: cron\u2011like scheduling with better health checks and logging<\/li>\n<\/ul>\n<p>You can choose either Supervisor or systemd for managing queue workers; both are valid. We tend to use:<\/p>\n<ul>\n<li>PHP\u2011FPM for <strong>short\u2011lived HTTP requests<\/strong><\/li>\n<li>Supervisor or systemd for <strong>long\u2011running CLI workers<\/strong><\/li>\n<\/ul>\n<p>If you\u2019re curious about how systemd timers compare to classic cron for scheduled jobs, we have a separate guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/cron-mu-systemd-timer-mi-neden-nasil-ve-ne-zaman-hangisini-secmeli\/\">Cron vs systemd timers and reliable scheduling<\/a>.<\/p>\n<h2><span id=\"Session_and_Worker_Isolation_Architecture_Patterns\">Session and Worker Isolation: Architecture Patterns<\/span><\/h2>\n<h3><span id=\"Pattern_1_Separate_PHPFPM_pools_for_web_vs_queue\">Pattern 1: Separate PHP\u2011FPM pools for web vs queue<\/span><\/h3>\n<p>The most impactful, low\u2011risk step is splitting your PHP\u2011FPM configuration into at least two pools:<\/p>\n<ul>\n<li><strong><code>[web]<\/code> pool<\/strong> \u2013 handles Nginx\/Apache PHP requests.<\/li>\n<li><strong><code>[queue]<\/code> pool<\/strong> \u2013 used only by CLI workers (if you choose to run workers via FPM, which is rare) or kept as a separate context if you want distinct INI settings.<\/li>\n<\/ul>\n<p>More commonly, we keep CLI workers as plain CLI PHP (not via FPM), but we still use the concept of \u201cweb pool vs worker context\u201d by:<\/p>\n<ul>\n<li>Running PHP\u2011FPM for HTTP traffic only<\/li>\n<li>Running CLI workers via <code>php -d ... artisan queue:work<\/code> with a dedicated php.ini or environment<\/li>\n<\/ul>\n<p>Either way, the key is that web sessions and background workers do <strong>not<\/strong> share the same pool of FPM children anymore.<\/p>\n<h3><span id=\"Pattern_2_Isolating_sessionheavy_vs_stateless_traffic\">Pattern 2: Isolating session\u2011heavy vs stateless traffic<\/span><\/h3>\n<p>In many real projects, not all traffic is equal:<\/p>\n<ul>\n<li>Logged\u2011out product\/category pages can be cached heavily<\/li>\n<li>Logged\u2011in dashboard, cart and checkout depend on PHP sessions and per\u2011user data<\/li>\n<li>Public API endpoints may be stateless but high\u2011volume<\/li>\n<\/ul>\n<p>You can map these to different FPM pools, for example:<\/p>\n<ul>\n<li><strong><code>[web_public]<\/code><\/strong> \u2013 for mostly cached, stateless pages<\/li>\n<li><strong><code>[web_session]<\/code><\/strong> \u2013 for cart\/checkout and any route that touches sessions<\/li>\n<li><strong><code>[api]<\/code><\/strong> \u2013 for API requests with their own rate limits and timeouts<\/li>\n<\/ul>\n<p>Nginx can send different URI patterns to different sockets:<\/p>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\">location \/checkout {\n    include fastcgi_params;\n    fastcgi_pass unix:\/run\/php-fpm-web_session.sock;\n}\n\nlocation \/api\/ {\n    include fastcgi_params;\n    fastcgi_pass unix:\/run\/php-fpm-api.sock;\n}\n\nlocation ~ .php$ {\n    include fastcgi_params;\n    fastcgi_pass unix:\/run\/php-fpm-web_public.sock;\n}<\/code><\/pre>\n<p>This prevents a burst of API traffic from eating the same pool that keeps user sessions alive.<\/p>\n<h3><span id=\"Pattern_3_Sessions_and_cache_storage_isolation\">Pattern 3: Sessions and cache storage isolation<\/span><\/h3>\n<p>Pool isolation works even better when combined with smart session and cache storage. For example, you might store sessions in:<\/p>\n<ul>\n<li>Files (with strict directory permissions per pool)<\/li>\n<li>Redis (with separate databases or key prefixes)<\/li>\n<li>Memcached (with namespace separation)<\/li>\n<\/ul>\n<p>We covered the trade\u2011offs in detail in our guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/php-session-ve-cache-depolamasini-dogru-secmek-dosya-redis-ve-memcachedin-wordpress-ve-laravel-performansina-etkisi\/\">choosing PHP session and cache storage (files vs Redis vs Memcached)<\/a>. In an isolated architecture, it becomes natural to give each pool its own session storage strategy and TTLs.<\/p>\n<h2><span id=\"Designing_Web_vs_Worker_Resources_and_Limits\">Designing Web vs Worker Resources and Limits<\/span><\/h2>\n<h3><span id=\"Different_latency_expectations\">Different latency expectations<\/span><\/h3>\n<p>A core principle we try to stick to at dchost.com is: <strong>different latency expectations mean different resource pools<\/strong>.<\/p>\n<ul>\n<li>Web requests: users feel anything above a few hundred milliseconds. For checkout and login, even small spikes are noticeable.<\/li>\n<li>Queue workers: can often take seconds or even minutes per job, as long as the queue drains within your business SLA.<\/li>\n<\/ul>\n<p>From this, the tuning approach becomes clearer:<\/p>\n<ul>\n<li>Give the <strong>web pool<\/strong> stricter timeouts, smaller <code>max_execution_time<\/code>, and often a higher process count for concurrency.<\/li>\n<li>Give <strong>queue workers<\/strong> more generous execution time, but limit how many can run in parallel to avoid stealing CPU from the web pool.<\/li>\n<\/ul>\n<h3><span id=\"Example_sizing_pools_on_a_4_vCPU_8_GB_RAM_VPS\">Example: sizing pools on a 4 vCPU \/ 8 GB RAM <a href=\"https:\/\/www.dchost.com\/vps\">VPS<\/a><\/span><\/h3>\n<p>Suppose you run a Laravel or WooCommerce store on a 4 vCPU \/ 8 GB RAM VPS from dchost.com. A reasonable starting point might be:<\/p>\n<ul>\n<li><strong>web_session pool<\/strong>\n<ul>\n<li><code>pm = dynamic<\/code><\/li>\n<li><code>pm.max_children = 12<\/code><\/li>\n<li><code>pm.start_servers = 4<\/code><\/li>\n<li><code>pm.min_spare_servers = 4<\/code><\/li>\n<li><code>pm.max_spare_servers = 8<\/code><\/li>\n<li><code>pm.max_requests = 1000<\/code><\/li>\n<\/ul>\n<\/li>\n<li><strong>web_public pool<\/strong>\n<ul>\n<li><code>pm.max_children = 8<\/code> (public pages mostly cached)<\/li>\n<\/ul>\n<\/li>\n<li><strong>queue workers<\/strong>\n<ul>\n<li>4\u20136 workers via Supervisor or systemd (not FPM children)<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>We cap queue workers so they can\u2019t saturate all 4 vCPUs. On NVMe\u2011backed plans, like our NVMe VPS options, IO contention is usually low, but we still want margin for database and webserver processes. For a deeper view on right\u2011sizing CPU\/RAM and IO, see our article on <a href=\"https:\/\/www.dchost.com\/blog\/en\/woocommerce-laravel-ve-node-jsde-dogru-vps-kaynaklarini-nasil-secersin-cpu-ram-nvme-ve-bant-genisligi-rehberi\/\">choosing VPS specs for WooCommerce, Laravel and Node.js<\/a>.<\/p>\n<h3><span id=\"Using_systemd_slices_and_priorities\">Using systemd slices and priorities<\/span><\/h3>\n<p>If you run queue workers under systemd, you can use slices and cgroups to enforce priorities. For example:<\/p>\n<ul>\n<li>Keep PHP\u2011FPM in the default slice (normal priority)<\/li>\n<li>Place workers in a lower\u2011priority slice with <code>CPUQuota<\/code> and <code>IOWeight<\/code> limits<\/li>\n<\/ul>\n<p>This ensures that even under heavy queue load, web requests remain responsive. It\u2019s a powerful complement to pool\u2011level settings.<\/p>\n<h2><span id=\"Putting_It_Together_Example_Implementation\">Putting It Together: Example Implementation<\/span><\/h2>\n<h3><span id=\"1_Define_multiple_PHPFPM_pools\">1. Define multiple PHP\u2011FPM pools<\/span><\/h3>\n<p>Example <code>\/etc\/php-fpm.d\/web_public.conf<\/code>:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">[web_public]\nuser = www-data\ngroup = www-data\nlisten = \/run\/php-fpm-web_public.sock\nlisten.owner = www-data\nlisten.group = www-data\n\npm = dynamic\npm.max_children = 8\npm.start_servers = 2\npm.min_spare_servers = 2\npm.max_spare_servers = 4\npm.max_requests = 1000\n\nphp_admin_value[session.save_handler] = redis\nphp_admin_value[session.save_path] = &quot;tcp:\/\/127.0.0.1:6379?database=0&quot;<\/code><\/pre>\n<p>Example <code>\/etc\/php-fpm.d\/web_session.conf<\/code>:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">[web_session]\nuser = www-data\ngroup = www-data\nlisten = \/run\/php-fpm-web_session.sock\nlisten.owner = www-data\nlisten.group = www-data\n\npm = dynamic\npm.max_children = 12\npm.start_servers = 4\npm.min_spare_servers = 4\npm.max_spare_servers = 8\npm.max_requests = 800\n\nphp_admin_value[max_execution_time] = 30\nphp_admin_value[memory_limit] = 512M\nphp_admin_value[session.save_handler] = redis\nphp_admin_value[session.save_path] = &quot;tcp:\/\/127.0.0.1:6379?database=1&quot;<\/code><\/pre>\n<p>Note how we use different Redis databases to isolate sessions between pools. You could also change cookie settings or GC probabilities per pool if needed.<\/p>\n<h3><span id=\"2_Route_requests_to_the_right_pool_via_Nginx\">2. Route requests to the right pool via Nginx<\/span><\/h3>\n<p>Example Nginx configuration snippet:<\/p>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\">location \/checkout {\n    include fastcgi_params;\n    fastcgi_param SCRIPT_FILENAME $document_root\/index.php;\n    fastcgi_pass unix:\/run\/php-fpm-web_session.sock;\n}\n\nlocation \/wp-login.php {\n    include fastcgi_params;\n    fastcgi_param SCRIPT_FILENAME $document_root\/index.php;\n    fastcgi_pass unix:\/run\/php-fpm-web_session.sock;\n}\n\nlocation \/api\/ {\n    include fastcgi_params;\n    fastcgi_param SCRIPT_FILENAME $document_root\/index.php;\n    fastcgi_pass unix:\/run\/php-fpm-web_public.sock;\n}\n\nlocation ~ .php$ {\n    include fastcgi_params;\n    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n    fastcgi_pass unix:\/run\/php-fpm-web_public.sock;\n}<\/code><\/pre>\n<p>In a Laravel application, you would typically route everything to <code>public\/index.php<\/code> but still decide per\u2011prefix which pool to use.<\/p>\n<h3><span id=\"3_Configure_queue_workers_with_Supervisor\">3. Configure queue workers with Supervisor<\/span><\/h3>\n<p>Example <code>\/etc\/supervisor.d\/laravel-queue.conf<\/code>:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">[program:laravel-queue]\ncommand=\/usr\/bin\/php \/var\/www\/app\/artisan queue:work --sleep=3 --tries=3\nprocess_name=%(program_name)s_%(process_num)02d\nautostart=true\nautorestart=true\nnumprocs=4\nuser=queueuser\nredirect_stderr=true\nstdout_logfile=\/var\/log\/laravel-queue.log\nstopwaitsecs=600<\/code><\/pre>\n<p>Key points:<\/p>\n<ul>\n<li>We run workers as a separate <code>queueuser<\/code> account (fewer permissions than the web user).<\/li>\n<li><code>numprocs<\/code> is set based on CPU; 4 workers on a 4\u2011vCPU VPS is usually safe if they\u2019re not all CPU\u2011bound.<\/li>\n<li><code>stopwaitsecs<\/code> is long enough for jobs to complete during a graceful restart.<\/li>\n<\/ul>\n<h3><span id=\"4_Or_run_workers_via_systemd_units\">4. Or run workers via systemd units<\/span><\/h3>\n<p>If you prefer systemd, a unit could look like this:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">[Unit]\nDescription=Laravel Queue Worker\nAfter=network.target\n\n[Service]\nUser=queueuser\nGroup=queueuser\nWorkingDirectory=\/var\/www\/app\nExecStart=\/usr\/bin\/php artisan queue:work --sleep=3 --tries=3\nRestart=always\nRestartSec=5\n\n# Resource limits\nNice=5\nIOSchedulingClass=idle\nCPUQuota=60%%\n\n[Install]\nWantedBy=multi-user.target<\/code><\/pre>\n<p>Here we use <code>Nice<\/code> and <code>CPUQuota<\/code> to gently deprioritise queue workers compared to PHP\u2011FPM, which keeps interactive traffic snappy.<\/p>\n<h3><span id=\"5_Observability_separate_logs_and_metrics\">5. Observability: separate logs and metrics<\/span><\/h3>\n<p>Isolation is only truly useful if you can see what\u2019s happening per pool and per worker. We strongly recommend:<\/p>\n<ul>\n<li>Separate access\/error logs for different Nginx locations (public vs checkout vs API)<\/li>\n<li>Separate PHP\u2011FPM slow log files per pool<\/li>\n<li>Dedicated log files for queue workers (Supervisor or systemd)<\/li>\n<li>Basic server metrics (CPU, RAM, IO, network) with alerting<\/li>\n<\/ul>\n<p>If you are not yet monitoring your VPS in a structured way, our guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/vps-izleme-ve-alarm-kurulumu-prometheus-grafana-ve-uptime-kuma-ile-baslangic\/\">VPS monitoring and alerts with Prometheus, Grafana and Uptime Kuma<\/a> is a good starting point.<\/p>\n<h2><span id=\"Relating_This_to_PerSite_PHPFPM_Pool_Architecture\">Relating This to Per\u2011Site PHP\u2011FPM Pool Architecture<\/span><\/h2>\n<h3><span id=\"Persite_and_perrole_pools_together\">Per\u2011site and per\u2011role pools together<\/span><\/h3>\n<p>In many agency or multi\u2011project setups, you already run <strong>one FPM pool per site<\/strong> (or per customer). In that model, you can still apply the same idea, just one level deeper:<\/p>\n<ul>\n<li>Each site has its own <code>example1_web<\/code>, <code>example1_session<\/code>, maybe <code>example1_api<\/code> pools<\/li>\n<li>Another site has <code>example2_web<\/code>, <code>example2_queue<\/code>, etc.<\/li>\n<\/ul>\n<p>We\u2019ve written a whole story about this per\u2011site approach in <a href=\"https:\/\/www.dchost.com\/blog\/en\/ofiste-bir-sabah-php-yukseltmesi-ter-damlalari-ve-kucuk-bir-aydinlanma\/\">how we run per\u2011site Nginx + PHP\u2011FPM pools without drama<\/a>. Combining per\u2011site and per\u2011role isolation gives you a very clean boundary between customers and between traffic types.<\/p>\n<h3><span id=\"When_to_move_beyond_a_single_server\">When to move beyond a single server<\/span><\/h3>\n<p>At some point, even a well\u2011designed pool architecture on one VPS will hit its limits: queue workloads grow, databases need more IOPS, or uptime requirements push you to redundancy. That\u2019s when it starts to make sense to:<\/p>\n<ul>\n<li>Move queues, Redis or the database to a separate VPS or <a href=\"https:\/\/www.dchost.com\/dedicated-server\">dedicated server<\/a><\/li>\n<li>Use GeoDNS \/ multi\u2011region setups for latency and redundancy<\/li>\n<li>Introduce a load balancer and multiple application servers<\/li>\n<\/ul>\n<p>We\u2019ve covered bigger\u2011picture hosting decisions for PHP apps in articles like <a href=\"https:\/\/www.dchost.com\/blog\/en\/wordpress-harici-php-uygulamalari-icin-hosting-secimi-laravel-symfony-ozel-yazilim-ve-kurumsal-paneller\/\">choosing hosting for Laravel, Symfony and custom PHP apps<\/a> and our guide to <a href=\"https:\/\/www.dchost.com\/blog\/en\/geodns-ve-cok-bolgeli-hosting-mimarisi-ile-global-ziyaretcilere-yakinlasmak\/\">GeoDNS and multi\u2011region hosting architecture<\/a>.<\/p>\n<h2><span id=\"Operational_Tips_and_Common_Pitfalls\">Operational Tips and Common Pitfalls<\/span><\/h2>\n<h3><span id=\"Avoiding_deadlocks_and_session_contention\">Avoiding deadlocks and session contention<\/span><\/h3>\n<p>Isolating pools does not magically fix all session issues. Watch out for:<\/p>\n<ul>\n<li>Long\u2011running requests that hold <code>session_start()<\/code> locks<\/li>\n<li>AJAX calls that repeatedly open and close sessions<\/li>\n<li>Misconfigured session.save_path pointing multiple pools to the same directory without proper separation<\/li>\n<\/ul>\n<p>If you see requests stuck in <code>php-fpm<\/code> status pages waiting for session locks, consider:<\/p>\n<ul>\n<li>Shortening the time spent between <code>session_start()<\/code> and <code>session_write_close()<\/code><\/li>\n<li>Storing truly large data outside sessions (e.g. Redis cache)<\/li>\n<li>Using Redis or Memcached for sessions with correct lock and TTL behaviour<\/li>\n<\/ul>\n<h3><span id=\"Graceful_deploys_with_queue_and_pool_isolation\">Graceful deploys with queue and pool isolation<\/span><\/h3>\n<p>Pool and worker isolation also makes deploys safer:<\/p>\n<ul>\n<li>You can drain queue workers (Supervisor\/systemd stops) separately from web traffic.<\/li>\n<li>You can reload PHP\u2011FPM pools gradually (e.g. <code>php-fpm reload<\/code>) while keeping workers running.<\/li>\n<li>You can perform <a href=\"https:\/\/www.dchost.com\/blog\/en\/github-actions-ile-vpse-otomatik-deploy-ve-zero-downtime-yayin\/\">zero\u2011downtime deployments to a VPS using symlink releases<\/a>, while separate pools pick up the new code without killing live sessions abruptly.<\/li>\n<\/ul>\n<p>For more complex Laravel setups, our detailed guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/laravel-uygulamalarini-vpste-nasil-yayinlarim-nginx-php%e2%80%91fpm-horizon-ve-sifir-kesinti-dagitimin-sicacik-yol-haritasi\/\">deploying Laravel on a VPS with Nginx, PHP\u2011FPM, Horizon and zero\u2011downtime releases<\/a> shows exactly how these pieces fit together.<\/p>\n<h3><span id=\"Testing_before_enabling_in_production\">Testing before enabling in production<\/span><\/h3>\n<p>When you introduce new pools and worker processes, always test on staging first:<\/p>\n<ul>\n<li>Simulate load on both web and queue side (e.g. using k6, JMeter or Locust)<\/li>\n<li>Watch PHP\u2011FPM status pages (per pool) for queue length and slow requests<\/li>\n<li>Verify sessions behave as expected across login, cart and checkout flows<\/li>\n<\/ul>\n<p>We\u2019ve written a practical guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/trafik-patlamasindan-once-load-test-yapmak-k6-jmeter-ve-locust-ile-kapasite-olcme-rehberi\/\">load testing your hosting before traffic spikes<\/a> that pairs nicely with this kind of architecture change.<\/p>\n<h2><span id=\"Conclusion_Calm_PHP_Servers_Through_Thoughtful_Isolation\">Conclusion: Calm PHP Servers Through Thoughtful Isolation<\/span><\/h2>\n<p>Isolating PHP sessions and queue workers is not about over\u2011engineering; it\u2019s about making your existing server feel calmer and more predictable. By splitting PHP\u2011FPM into dedicated pools for session\u2011heavy routes, stateless traffic and background workers \u2013 and by managing workers with Supervisor or systemd \u2013 you protect the user experience from noisy neighbours on the same machine.<\/p>\n<p>Once pools are tuned and Nginx routing is in place, everyday operations become easier: logs are cleaner, slow\u2011log analysis makes more sense, and deploys are less risky. When it\u2019s time to scale beyond a single VPS, the same boundaries you drew between pools and workers become natural boundaries between servers.<\/p>\n<p>At dchost.com, we design our VPS, dedicated and colocation setups with exactly this kind of separation in mind, so your PHP applications can grow without constant firefighting. If you\u2019re planning a new Laravel, WooCommerce or custom PHP deployment, or you want to refactor an existing \u201cone big pool\u201d server into something calmer, you can start with a VPS sized for your workload and apply the patterns in this guide step by step. The result is a stack that feels faster, breaks less, and is much nicer to operate.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>On a busy PHP application server, using a single PHP\u2011FPM pool for everything \u2013 web requests, admin panel, API calls, queue workers and cron jobs \u2013 quickly turns into a bottleneck. A long\u2011running report or a stuck queue worker can block precious PHP\u2011FPM children, increase response times and make the whole stack feel unstable. The [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":3711,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-3710","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-teknoloji"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/3710","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=3710"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/3710\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/3711"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=3710"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=3710"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=3710"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}