{"id":1405,"date":"2025-11-06T14:26:50","date_gmt":"2025-11-06T11:26:50","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/the-laravel-production-tune%e2%80%91up-i-do-on-every-server-php%e2%80%91fpm-pools-opcache-octane-queues-horizon-and-redis\/"},"modified":"2025-11-06T14:26:50","modified_gmt":"2025-11-06T11:26:50","slug":"the-laravel-production-tune%e2%80%91up-i-do-on-every-server-php%e2%80%91fpm-pools-opcache-octane-queues-horizon-and-redis","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/the-laravel-production-tune%e2%80%91up-i-do-on-every-server-php%e2%80%91fpm-pools-opcache-octane-queues-horizon-and-redis\/","title":{"rendered":"The Laravel Production Tune\u2011Up I Do on Every Server: PHP\u2011FPM Pools, OPcache, Octane, Queues\/Horizon, and Redis"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>So I\u2019m staring at the server graphs on a sleepy Tuesday, coffee in hand, wondering why a perfectly fine Laravel app suddenly feels like it\u2019s running through molasses. The code was clean, the database wasn\u2019t screaming, and yet requests were dragging their feet. Ever had that moment when you\u2019re sure the problem isn\u2019t the code\u2026 but something beneath it? That was me. And that little rabbit hole led to a routine I now run on every Laravel production server: tune PHP\u2011FPM pools so they don\u2019t choke, set OPcache like a grown\u2011up, switch queues to Horizon with sensible limits, lean hard on Redis without letting it hoard memory, and\u2014when the app needs it\u2014spin up Octane for a real boost.<\/p>\n<p>Here\u2019s the thing: Laravel is fast, but the stack around it decides whether your users feel that speed. Your process manager can starve, your OPcache can waste memory, Redis can keep zombie keys forever, and your worker setup can quietly snowball until it flattens the box. None of this is flashy. All of it is fixable.<\/p>\n<p>In this guide, I\u2019ll walk you through the production tune\u2011up I rely on: practical PHP\u2011FPM pool sizing, OPcache settings that survive deploys, a Horizon layout that won\u2019t collapse under a heavy queue, Redis tweaks that keep memory honest, and where Octane really makes sense. I\u2019ll share the little checks that save me during incident calls, and the playbook I wish I had years ago. Grab a coffee; let\u2019s make your Laravel production fly.<\/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=\"#The_PHPFPM_Pool_Mindset_Fewer_Surprises_More_Throughput\"><span class=\"toc_number toc_depth_1\">1<\/span> The PHP\u2011FPM Pool Mindset: Fewer Surprises, More Throughput<\/a><ul><li><a href=\"#Start_with_one_pool_earn_your_second\"><span class=\"toc_number toc_depth_2\">1.1<\/span> Start with one pool, earn your second<\/a><\/li><li><a href=\"#How_I_size_workers_without_guesswork\"><span class=\"toc_number toc_depth_2\">1.2<\/span> How I size workers without guesswork<\/a><\/li><li><a href=\"#Timeouts_and_slow_logs_are_your_smoke_alarms\"><span class=\"toc_number toc_depth_2\">1.3<\/span> Timeouts and slow logs are your smoke alarms<\/a><\/li><li><a href=\"#Nginx_and_pool_routing_without_drama\"><span class=\"toc_number toc_depth_2\">1.4<\/span> Nginx and pool routing without drama<\/a><\/li><li><a href=\"#Watch_adjust_dont_guess\"><span class=\"toc_number toc_depth_2\">1.5<\/span> Watch, adjust, don\u2019t guess<\/a><\/li><\/ul><\/li><li><a href=\"#OPcache_Your_Code_But_PreWarmed_and_Ready\"><span class=\"toc_number toc_depth_1\">2<\/span> OPcache: Your Code, But Pre\u2011Warmed and Ready<\/a><ul><li><a href=\"#The_settings_I_reach_for_first\"><span class=\"toc_number toc_depth_2\">2.1<\/span> The settings I reach for first<\/a><\/li><li><a href=\"#Deploys_that_dont_trip_over_OPcache\"><span class=\"toc_number toc_depth_2\">2.2<\/span> Deploys that don\u2019t trip over OPcache<\/a><\/li><li><a href=\"#Should_you_enable_JIT\"><span class=\"toc_number toc_depth_2\">2.3<\/span> Should you enable JIT?<\/a><\/li><\/ul><\/li><li><a href=\"#Queues_and_Horizon_Calm_Workflows_Happier_Users\"><span class=\"toc_number toc_depth_1\">3<\/span> Queues and Horizon: Calm Workflows, Happier Users<\/a><ul><li><a href=\"#Separate_what_you_can_constrain_what_you_cant\"><span class=\"toc_number toc_depth_2\">3.1<\/span> Separate what you can, constrain what you can\u2019t<\/a><\/li><li><a href=\"#A_Horizon_layout_I_reach_for_often\"><span class=\"toc_number toc_depth_2\">3.2<\/span> A Horizon layout I reach for often<\/a><\/li><li><a href=\"#Backoff_retries_and_idempotency\"><span class=\"toc_number toc_depth_2\">3.3<\/span> Backoff, retries, and idempotency<\/a><\/li><li><a href=\"#Horizon_vs_queuework\"><span class=\"toc_number toc_depth_2\">3.4<\/span> Horizon vs. queue:work<\/a><\/li><\/ul><\/li><li><a href=\"#Redis_Tuning_That_Wont_Bite_You_Later\"><span class=\"toc_number toc_depth_1\">4<\/span> Redis Tuning That Won\u2019t Bite You Later<\/a><ul><li><a href=\"#Give_it_a_memory_budget_and_an_eviction_story\"><span class=\"toc_number toc_depth_2\">4.1<\/span> Give it a memory budget and an eviction story<\/a><\/li><li><a href=\"#Dont_let_queues_and_cache_fight\"><span class=\"toc_number toc_depth_2\">4.2<\/span> Don\u2019t let queues and cache fight<\/a><\/li><li><a href=\"#TTL_discipline_and_key_hygiene\"><span class=\"toc_number toc_depth_2\">4.3<\/span> TTL discipline and key hygiene<\/a><\/li><li><a href=\"#Latency_and_disk_safety\"><span class=\"toc_number toc_depth_2\">4.4<\/span> Latency and disk safety<\/a><\/li><\/ul><\/li><li><a href=\"#When_Octane_Is_Worth_It_And_When_Its_Not\"><span class=\"toc_number toc_depth_1\">5<\/span> When Octane Is Worth It (And When It\u2019s Not)<\/a><ul><li><a href=\"#What_you_gain_what_you_must_change\"><span class=\"toc_number toc_depth_2\">5.1<\/span> What you gain, what you must change<\/a><\/li><li><a href=\"#A_gentle_starting_point\"><span class=\"toc_number toc_depth_2\">5.2<\/span> A gentle starting point<\/a><\/li><li><a href=\"#Database_connections_caches_and_events\"><span class=\"toc_number toc_depth_2\">5.3<\/span> Database connections, caches, and events<\/a><\/li><li><a href=\"#Octanes_sweet_spot\"><span class=\"toc_number toc_depth_2\">5.4<\/span> Octane\u2019s sweet spot<\/a><\/li><\/ul><\/li><li><a href=\"#Safe_Deploys_Real_Monitoring_and_the_Little_Habits_That_Keep_You_Fast\"><span class=\"toc_number toc_depth_1\">6<\/span> Safe Deploys, Real Monitoring, and the Little Habits That Keep You Fast<\/a><ul><li><a href=\"#Warmth_before_traffic\"><span class=\"toc_number toc_depth_2\">6.1<\/span> Warmth before traffic<\/a><\/li><li><a href=\"#Drain_Horizon_dont_drop_it\"><span class=\"toc_number toc_depth_2\">6.2<\/span> Drain Horizon, don\u2019t drop it<\/a><\/li><li><a href=\"#Track_the_signals_that_matter\"><span class=\"toc_number toc_depth_2\">6.3<\/span> Track the signals that matter<\/a><\/li><li><a href=\"#Fast_storage_and_honest_capacity\"><span class=\"toc_number toc_depth_2\">6.4<\/span> Fast storage and honest capacity<\/a><\/li><li><a href=\"#Database_the_quiet_backbone\"><span class=\"toc_number toc_depth_2\">6.5<\/span> Database: the quiet backbone<\/a><\/li><\/ul><\/li><li><a href=\"#RealWorld_Scenarios_and_the_Fixes_I_Reach_For\"><span class=\"toc_number toc_depth_1\">7<\/span> Real\u2011World Scenarios and the Fixes I Reach For<\/a><ul><li><a href=\"#CPU_is_fine_but_response_times_spike_randomly\"><span class=\"toc_number toc_depth_2\">7.1<\/span> \u201cCPU is fine but response times spike randomly\u201d<\/a><\/li><li><a href=\"#Queues_are_always_behind_even_offpeak\"><span class=\"toc_number toc_depth_2\">7.2<\/span> \u201cQueues are always behind, even off\u2011peak\u201d<\/a><\/li><li><a href=\"#Octane_made_our_app_fast_and_weird\"><span class=\"toc_number toc_depth_2\">7.3<\/span> \u201cOctane made our app fast\u2026 and weird\u201d<\/a><\/li><li><a href=\"#After_deploy_the_first_few_requests_are_slow\"><span class=\"toc_number toc_depth_2\">7.4<\/span> \u201cAfter deploy, the first few requests are slow\u201d<\/a><\/li><li><a href=\"#Memory_leak_or_just_busy\"><span class=\"toc_number toc_depth_2\">7.5<\/span> \u201cMemory leak or just busy?\u201d<\/a><\/li><\/ul><\/li><li><a href=\"#A_Quick_Word_on_CDN_Caching_Layers_and_When_to_Offload\"><span class=\"toc_number toc_depth_1\">8<\/span> A Quick Word on CDN, Caching Layers, and When to Offload<\/a><\/li><li><a href=\"#Putting_It_All_Together\"><span class=\"toc_number toc_depth_1\">9<\/span> Putting It All Together<\/a><\/li><\/ul><\/div>\n<h2 id=\"section-1\"><span id=\"The_PHPFPM_Pool_Mindset_Fewer_Surprises_More_Throughput\">The PHP\u2011FPM Pool Mindset: Fewer Surprises, More Throughput<\/span><\/h2>\n<p>I used to think of PHP\u2011FPM as a mysterious black box. Then I watched a single noisy API endpoint eat every worker and force the rest of the app to queue behind it. That\u2019s when it clicked: your <strong>pool is your lane discipline<\/strong>. If everyone piles into one lane, traffic stops. Split lanes wisely, and you cruise.<\/p>\n<h3><span id=\"Start_with_one_pool_earn_your_second\">Start with one pool, earn your second<\/span><\/h3>\n<p>If you\u2019re running a single Laravel app, you can absolutely start with one pool. Keep it simple: a dedicated Unix socket for Nginx, a reasonable number of workers, and clear limits. If you have separate concerns\u2014like a public site and a heavy admin, or a \/api that runs expensive requests\u2014consider a second pool with its own worker cap and timeout. Think of it like giving your most demanding route its own lane so it can\u2019t block the rest.<\/p>\n<h3><span id=\"How_I_size_workers_without_guesswork\">How I size workers without guesswork<\/span><\/h3>\n<p>Here\u2019s the mental model I use. Each PHP\u2011FPM child is a fully loaded PHP process. Give each child a budget\u2014say 60\u2013120 MB depending on extensions, OPcache, and workload. Then look at your RAM, keep cushion for the OS, Nginx, Redis, MySQL, and background workers. If you can safely afford ten workers at 100 MB each, set <strong>pm.max_children<\/strong> to ten and resist the urge to max it out. More workers aren\u2019t always better; they\u2019re just more mouths to feed.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">; \/etc\/php\/8.2\/fpm\/pool.d\/laravel.conf\n[laravel]\nuser = www-data\ngroup = www-data\nlisten = \/run\/php-fpm-laravel.sock\nlisten.owner = www-data\nlisten.group = www-data\n\npm = dynamic\npm.max_children = 10\npm.start_servers = 3\npm.min_spare_servers = 2\npm.max_spare_servers = 5\nrequest_terminate_timeout = 60s\nrequest_slowlog_timeout = 5s\nslowlog = \/var\/log\/php8.2-fpm\/slow.log\n\n; Turn on status page for quick checks\npm.status_path = \/fpm-status\n<\/code><\/pre>\n<p>Dynamic works great for most apps. On very traffic\u2011heavy servers where workload is predictable, <strong>pm = static<\/strong> can add stability because you know exactly how many children run. If you have low traffic most of the day but sudden bursts, <strong>pm = ondemand<\/strong> avoids idle processes. Don\u2019t obsess; pick one that matches your traffic pattern and check real metrics a day later.<\/p>\n<h3><span id=\"Timeouts_and_slow_logs_are_your_smoke_alarms\">Timeouts and slow logs are your smoke alarms<\/span><\/h3>\n<p><strong>request_terminate_timeout<\/strong> stops runaway requests. I prefer 60 seconds to catch upstream issues without nuking legitimate long tasks (which should be in queues anyway). <strong>request_slowlog_timeout<\/strong> is my favorite: five or eight seconds writes a stack trace of slow PHP requests. It has saved me from too many \u201crandom slowness\u201d mysteries to count. Enable it and check the slow log after deploys. You\u2019ll thank yourself.<\/p>\n<h3><span id=\"Nginx_and_pool_routing_without_drama\">Nginx and pool routing without drama<\/span><\/h3>\n<p>Keep Nginx upstreams short and direct. If you split pools (say, \/api and web), route paths to different sockets and give the API a lower <strong>pm.max_children<\/strong> if it tends to be heavy. That way, a bad actor or heavy report can\u2019t starve your homepage.<\/p>\n<h3><span id=\"Watch_adjust_dont_guess\">Watch, adjust, don\u2019t guess<\/span><\/h3>\n<p>When I bring a new Laravel app online, I deploy, baseline traffic, then spend an afternoon watching workers in htop and the PHP\u2011FPM status page. Are children constantly maxed? Drop the concurrency of your queue workers or bump max_children slightly. Is RAM tight? Trim workers and cache more. If you want a simple, durable monitoring stack that never lets me down, I\u2019ve shared my playbook in <a href=\"https:\/\/www.dchost.com\/blog\/en\/vps-izleme-ve-uyari-nasil-kurulur-prometheus-grafana-ve-node-exporter-ile-sessiz-alarmlari-konusturmak\/\">the Prometheus + Grafana setup I use to keep a VPS calm<\/a>. It\u2019s friendly, and it works.<\/p>\n<h2 id=\"section-2\"><span id=\"OPcache_Your_Code_But_PreWarmed_and_Ready\">OPcache: Your Code, But Pre\u2011Warmed and Ready<\/span><\/h2>\n<p>Laravel without OPcache is like driving a sports car in first gear. You\u2019ll move, sure, but why would you? OPcache compiles your PHP files once and serves bytecode from memory. The trick is giving it just enough room and setting it up to behave during deploys.<\/p>\n<h3><span id=\"The_settings_I_reach_for_first\">The settings I reach for first<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">; \/etc\/php\/8.2\/fpm\/conf.d\/10-opcache.ini\nopcache.enable=1\nopcache.enable_cli=0\nopcache.memory_consumption=256\nopcache.interned_strings_buffer=16\nopcache.max_accelerated_files=20000\nopcache.validate_timestamps=0\nopcache.save_comments=1\nopcache.fast_shutdown=1\n<\/code><\/pre>\n<p>If you have frequent deploys, <strong>opcache.validate_timestamps=0<\/strong> is your friend\u2014just remember to reload FPM during deploys so it clears and reloads memory. On systems where you can\u2019t reload, you can leave timestamps on and set a small <strong>opcache.revalidate_freq<\/strong>, but you\u2019ll pay a tiny check overhead. I prefer clean reloads.<\/p>\n<p>If you\u2019re curious about the why behind each directive, the official configuration notes are a pleasant rabbit hole when you have time: <a href=\"https:\/\/www.php.net\/manual\/en\/opcache.configuration.php\" rel=\"nofollow noopener\" target=\"_blank\">the official OPcache configuration reference<\/a> is clear and practical.<\/p>\n<h3><span id=\"Deploys_that_dont_trip_over_OPcache\">Deploys that don\u2019t trip over OPcache<\/span><\/h3>\n<p>Atomic, symlinked deploys are the way to go: build to a new release directory, update the symlink, warm caches, reload services. Right after the symlink flips, I like to run any cached warmups and then <strong>systemctl reload php-fpm<\/strong> (or the versioned service) so OPcache starts fresh with the new paths. This pairs nicely with Laravel\u2019s own cache warmers:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">php artisan config:cache\nphp artisan route:cache\nphp artisan view:cache\n<\/code><\/pre>\n<p>When someone tells me their site got slower after a deploy, my first nudge is \u201cclear the code path and warm the caches.\u201d Nine times out of ten, OPcache was full, or configs weren\u2019t cached.<\/p>\n<h3><span id=\"Should_you_enable_JIT\">Should you enable JIT?<\/span><\/h3>\n<p>I get this question a lot. In my experience, JIT rarely moves the needle for typical Laravel web requests. If you\u2019re doing numeric heavy lifting or specific algorithms in PHP, it might help. For most apps, keep JIT off and invest that energy in better database queries, caching, and queues.<\/p>\n<h2 id=\"section-3\"><span id=\"Queues_and_Horizon_Calm_Workflows_Happier_Users\">Queues and Horizon: Calm Workflows, Happier Users<\/span><\/h2>\n<p>One of my clients once ran all emails, webhooks, and report generation on a single queue worker. It was fine until a partner dumped a thousand webhook retries at 10:03 AM on a Monday. Everything clogged. The site looked \u201cslow\u201d because pages waited on jobs that should\u2019ve been background\u2011only. The fix that changed their week: <strong>Horizon with focused queues<\/strong> and sane limits.<\/p>\n<h3><span id=\"Separate_what_you_can_constrain_what_you_cant\">Separate what you can, constrain what you can\u2019t<\/span><\/h3>\n<p>Split your jobs by behavior. I like a <strong>default<\/strong> queue for common quick tasks, a <strong>mail<\/strong> queue for emails, a <strong>webhooks<\/strong> queue that uses tighter rate limits, and a <strong>heavy<\/strong> queue for reports and imports. This gives you knobs: if imports go wild, you can cap the heavy queue at two workers while the default and mail queues keep the site snappy.<\/p>\n<h3><span id=\"A_Horizon_layout_I_reach_for_often\">A Horizon layout I reach for often<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">\/\/ config\/horizon.php\n\n'defaults' =&gt; [\n    'tries' =&gt; 3,\n    'timeout' =&gt; 60,\n    'maxTime' =&gt; 300,\n    'balance' =&gt; 'simple',\n],\n\n'environments' =&gt; [\n    'production' =&gt; [\n        'supervisor-default' =&gt; [\n            'connection' =&gt; 'redis',\n            'queue' =&gt; ['default'],\n            'minProcesses' =&gt; 3,\n            'maxProcesses' =&gt; 6,\n            'balanceMaxShift' =&gt; 1,\n            'balanceCooldown' =&gt; 3,\n        ],\n        'supervisor-mail' =&gt; [\n            'connection' =&gt; 'redis',\n            'queue' =&gt; ['mail'],\n            'minProcesses' =&gt; 1,\n            'maxProcesses' =&gt; 2,\n        ],\n        'supervisor-webhooks' =&gt; [\n            'connection' =&gt; 'redis',\n            'queue' =&gt; ['webhooks'],\n            'minProcesses' =&gt; 1,\n            'maxProcesses' =&gt; 2,\n            'timeout' =&gt; 30,\n        ],\n        'supervisor-heavy' =&gt; [\n            'connection' =&gt; 'redis',\n            'queue' =&gt; ['heavy'],\n            'minProcesses' =&gt; 1,\n            'maxProcesses' =&gt; 2,\n            'timeout' =&gt; 300,\n        ],\n    ],\n],\n<\/code><\/pre>\n<p>Two ideas are doing the heavy lifting here. First, each type of work has its own sandbox. Second, the max worker counts stop \u201chelpful\u201d teammates from turning a knob to twelve and starving PHP\u2011FPM. Put a memory limit on your workers, too. And remember to match <strong>timeout<\/strong> to reality; if your webhook provider expects a response in ten seconds, don\u2019t let that worker sit for a minute.<\/p>\n<h3><span id=\"Backoff_retries_and_idempotency\">Backoff, retries, and idempotency<\/span><\/h3>\n<p>Make retries smarter with exponential backoff. A broken third\u2011party shouldn\u2019t spiral your queue. And aim for idempotent jobs\u2014if a webhook fires twice or a job retries, it should be safe. This is less about performance and more about stability, but it\u2019s the difference between recovery and chaos.<\/p>\n<h3><span id=\"Horizon_vs_queuework\">Horizon vs. queue:work<\/span><\/h3>\n<p>I still use <strong>queue:work<\/strong> for tiny projects. But as soon as you\u2019re juggling multiple queues or tuning concurrency, Horizon pays for itself. The dashboard is honest about what\u2019s happening, and the process management is worth its weight in uptime. If you want a reference while you configure it, I like to skim the official docs when people ask me about features: <a href=\"https:\/\/laravel.com\/docs\/horizon\" rel=\"nofollow noopener\" target=\"_blank\">the Horizon documentation<\/a> is straightforward and helpful.<\/p>\n<h2 id=\"section-4\"><span id=\"Redis_Tuning_That_Wont_Bite_You_Later\">Redis Tuning That Won\u2019t Bite You Later<\/span><\/h2>\n<p>Redis is the heartbeat of a busy Laravel app: cache, queues, rate limiters, locks. It\u2019s blazingly fast until it isn\u2019t, and when it runs out of memory or blocks on disk, you feel it instantly. Here\u2019s how I tune it so it stays friendly under load.<\/p>\n<h3><span id=\"Give_it_a_memory_budget_and_an_eviction_story\">Give it a memory budget and an eviction story<\/span><\/h3>\n<p>I always set <strong>maxmemory<\/strong> and pick an eviction policy on non\u2011critical caches. For Laravel cache and queues, <strong>allkeys-lru<\/strong> is a good default: it evicts the least recently used keys when memory fills. If you rely heavily on TTLs, <strong>volatile-lru<\/strong> can work. What matters is that you choose a policy on purpose and never let Redis just keel over. If you\u2019re curious about how these policies behave, the official note on <a href=\"https:\/\/redis.io\/docs\/latest\/develop\/reference\/eviction\/\" rel=\"nofollow noopener\" target=\"_blank\">Redis eviction policies<\/a> is a great explainer.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># \/etc\/redis\/redis.conf\nmaxmemory 1gb\nmaxmemory-policy allkeys-lru\n\n# Safer AOF persistence for most apps\nappendonly yes\nappendfsync everysec\n\n# Keep client buffers bounded (watch pubsub in particular)\nclient-output-buffer-limit normal 0 0 0\nclient-output-buffer-limit pubsub 32mb 8mb 60\n<\/code><\/pre>\n<p>If you push a lot of logs or events through Laravel broadcasting, keep an eye on the pubsub buffer. It can grow quietly and then smack you during peaks. The limits above are a friendly guardrail.<\/p>\n<h3><span id=\"Dont_let_queues_and_cache_fight\">Don\u2019t let queues and cache fight<\/span><\/h3>\n<p>Use separate Redis databases or, better, separate instances for cache vs queues when possible. Cache churn can evict queue metadata under aggressive policies. If you\u2019re on a single instance, at least split DB indexes and keep an eye on memory. On busy systems, I like to run a dedicated Redis for Horizon\/queues and another for cache. It keeps the traffic patterns clean.<\/p>\n<h3><span id=\"TTL_discipline_and_key_hygiene\">TTL discipline and key hygiene<\/span><\/h3>\n<p>Always, always set TTLs for cache keys that don\u2019t need to live forever. Laravel\u2019s cache helpers make this a one\u2011liner. If you\u2019re caching heavy computed results, be intentional about how long they live and refresh them in the background. Stale keys are easy to ignore until they block legitimate data from staying warm.<\/p>\n<h3><span id=\"Latency_and_disk_safety\">Latency and disk safety<\/span><\/h3>\n<p>On virtualized servers or noisy hosts, Redis can spike latency if disk I\/O gets weird. AOF with <strong>everysec<\/strong> is a good tradeoff for many apps. If you care more about speed than persistence for cache data, you can keep AOF on for queues and critical locks while using a separate instance for cache without persistence. And keep huge pages disabled unless you\u2019ve profiled\u2014Redis can be finicky about memory settings under certain kernels.<\/p>\n<p>If you want a friendly dive into why Redis often beats Memcached for app workloads\u2014and when it doesn\u2019t\u2014I shared thoughts in <a href=\"https:\/\/www.dchost.com\/blog\/en\/wordpress-ve-woocommerce-icin-redis-mi-memcached-mi-kalici-nesne-onbellegi-ttl-ve-eviction-ayarlarini-ne-zaman-nasil-yaparsin\/\">my Redis vs Memcached guide with TTL and eviction tuning<\/a>. Even though it\u2019s written for WordPress, the ideas map cleanly to Laravel.<\/p>\n<h2 id=\"section-5\"><span id=\"When_Octane_Is_Worth_It_And_When_Its_Not\">When Octane Is Worth It (And When It\u2019s Not)<\/span><\/h2>\n<p>Octane is the \u201ckeep the framework in memory\u201d button. Instead of booting Laravel on every request, it stays hot using Swoole or RoadRunner. The first time I flipped it on for a read\u2011heavy API, latency dropped so hard the team thought I\u2019d broken the logs. But I\u2019ve also seen folks switch it on without changing anything else, then wonder why their app got weirder.<\/p>\n<h3><span id=\"What_you_gain_what_you_must_change\">What you gain, what you must change<\/span><\/h3>\n<p>The gain is obvious: no repeated bootstrap per request. The change is subtle: <strong>your app is long\u2011running now<\/strong>. That means you have to be careful with state. Static properties, singletons holding per\u2011request data, \u201cjust\u2011for\u2011this\u2011request\u201d caches\u2014these can bleed between users if you\u2019re not thoughtful. It\u2019s not scary, but it demands discipline.<\/p>\n<h3><span id=\"A_gentle_starting_point\">A gentle starting point<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># install one engine, e.g. Swoole\ncomposer require laravel\/octane\nphp artisan octane:install\n\n# config\/octane.php highlights\n'server' =&gt; env('OCTANE_SERVER', 'swoole'),\n'workers' =&gt; env('OCTANE_WORKERS', 4),\n'tasks' =&gt; env('OCTANE_TASK_WORKERS', 2),\n'watch' =&gt; false,\n<\/code><\/pre>\n<p>Start simple: one worker per CPU core is a safe baseline, and a small number of task workers help with out\u2011of\u2011band jobs. Don\u2019t crank it to the moon on day one. Drop Octane behind Nginx, keep PHP\u2011FPM for non\u2011Octane paths if you need them, and watch your logs closely for \u201cleaks\u201d\u2014things that should reset each request but don\u2019t.<\/p>\n<p>Laravel\u2019s docs are a nice companion for this part. If you want to skim what\u2019s new or double\u2011check a detail, keep <a href=\"https:\/\/laravel.com\/docs\/octane\" rel=\"nofollow noopener\" target=\"_blank\">the Octane documentation<\/a> handy while you experiment.<\/p>\n<h3><span id=\"Database_connections_caches_and_events\">Database connections, caches, and events<\/span><\/h3>\n<p>Close and re\u2011open DB connections between requests if you see stale issues, or rely on Laravel\u2019s built\u2011in connection management routines. Avoid caching per\u2011request data in singletons, and prefer request\u2011scoped containers for anything that depends on the authenticated user. Logs are your friend here\u2014if a user sees someone else\u2019s data even once, roll back and review your state surfaces.<\/p>\n<h3><span id=\"Octanes_sweet_spot\">Octane\u2019s sweet spot<\/span><\/h3>\n<p>Octane shines for read\u2011heavy APIs and sites that do a lot of framework bootstrapping per request. If your bottleneck is the database, fix that first. If the problem is cold boot, Octane is the right lever. I treat it like a second\u2011phase optimization after I\u2019ve trimmed PHP\u2011FPM, tuned OPcache, and cleaned up queries.<\/p>\n<h2 id=\"section-6\"><span id=\"Safe_Deploys_Real_Monitoring_and_the_Little_Habits_That_Keep_You_Fast\">Safe Deploys, Real Monitoring, and the Little Habits That Keep You Fast<\/span><\/h2>\n<p>I used to think performance was about big switches. Flip Octane, double workers, crank memory. These days, I win more with small habits: deploys that don\u2019t shock the system, caches that stay warm, queues that drain before I reload anything, and dashboards that actually tell me when I\u2019ve gone too far.<\/p>\n<h3><span id=\"Warmth_before_traffic\">Warmth before traffic<\/span><\/h3>\n<p>On deploy, I like to build in a temp directory, run composer with opcache and autoload optimization flags, cache configs\/routes\/views, run migrations with a maintenance window if needed, then flip the symlink. Right after, I ping a few hot endpoints to warm OPcache and any internal caches. Finally, I reload PHP\u2011FPM so OPcache is fresh. It keeps that \u201cfirst request after deploy\u201d dip away.<\/p>\n<h3><span id=\"Drain_Horizon_dont_drop_it\">Drain Horizon, don\u2019t drop it<\/span><\/h3>\n<p>If you\u2019re moving to a new release that changes job structures, pause Horizon and let workers finish. Then deploy. Then resume. It takes a minute and it saves you from jobs failing because a class moved or a serializer changed. Little rhythm, big payoff.<\/p>\n<h3><span id=\"Track_the_signals_that_matter\">Track the signals that matter<\/span><\/h3>\n<p>For Laravel servers, my priority list is simple: PHP\u2011FPM busy vs idle workers, request duration quantiles, Redis memory and latency, queue wait times, Horizon runtime, and database slow queries. If you want a broader plan for the server itself, this walkthrough is as close to a plug\u2011and\u2011play as it gets: <a href=\"https:\/\/www.dchost.com\/blog\/en\/vps-izleme-ve-uyari-nasil-kurulur-prometheus-grafana-ve-node-exporter-ile-sessiz-alarmlari-konusturmak\/\">the Prometheus + Grafana monitoring guide I rely on<\/a>.<\/p>\n<h3><span id=\"Fast_storage_and_honest_capacity\">Fast storage and honest capacity<\/span><\/h3>\n<p>When I migrate apps that always feel \u201csticky,\u201d storage is often the hidden culprit. If your queues, logs, and cache files are hitting slow disks, everything drags. If you\u2019re choosing a new box and want practical, real\u2011world guidance on storage and CPU, I shared my philosophy in <a href=\"https:\/\/www.dchost.com\/blog\/en\/nvme-vps-hosting-rehberi-hizin-nereden-geldigini-nasil-olculdugunu-ve-gercek-sonuclari-beraber-gorelim\/\">this NVMe VPS hosting deep dive<\/a> and a more Laravel\u2011specific take in <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\/\">the guide I use to choose VPS specs for Laravel<\/a>. Skip the guesswork; it pays off.<\/p>\n<h3><span id=\"Database_the_quiet_backbone\">Database: the quiet backbone<\/span><\/h3>\n<p>Even the best PHP tuning can\u2019t hide a missing index or an N+1 query spree. Keep an eye on slow query logs, add the indexes your app keeps asking for, and lean on caching for expensive reads. If you like checklists, the ideas in <a href=\"https:\/\/www.dchost.com\/blog\/en\/woocommerce-icin-mysql-innodb-tuning-kontrol-listesi-buffer-pool-indeksleme-ve-slow-query-analizi-nasil-akillica-yapilir\/\">my InnoDB tuning checklist<\/a> translate well to Laravel\u2019s database patterns. Clean queries make everything else easier.<\/p>\n<h2 id=\"section-7\"><span id=\"RealWorld_Scenarios_and_the_Fixes_I_Reach_For\">Real\u2011World Scenarios and the Fixes I Reach For<\/span><\/h2>\n<p>Let me stitch this together with a few snapshots I\u2019ve bumped into this year.<\/p>\n<h3><span id=\"CPU_is_fine_but_response_times_spike_randomly\">\u201cCPU is fine but response times spike randomly\u201d<\/span><\/h3>\n<p>I check the PHP\u2011FPM slow log first. If I see a cluster of traces around a route, I profile that controller and its queries. Nine times out of ten, it\u2019s a slow external API call or an eager load that should\u2019ve been a cache. I add timeouts to the external call, move long work to a queue, and cache the heavy read. Response times smooth out immediately.<\/p>\n<h3><span id=\"Queues_are_always_behind_even_offpeak\">\u201cQueues are always behind, even off\u2011peak\u201d<\/span><\/h3>\n<p>I\u2019ve seen this when Horizon\u2019s worker counts are generous but Redis is fighting eviction because cache keys never expire. Jobs end up waiting while the instance thrashes memory. The fix is two\u2011part: give Redis a budget and TTL discipline, then constrain the heavy queue so it can\u2019t hog everything. Suddenly, \u201cbehind\u201d becomes \u201ccaught up.\u201d<\/p>\n<h3><span id=\"Octane_made_our_app_fast_and_weird\">\u201cOctane made our app fast\u2026 and weird\u201d<\/span><\/h3>\n<p>That \u201cweird\u201d is state leak. View a page as user A, then as user B, and you see traces of A\u2014old data in a singleton, a lingering service that cached the wrong thing. I audit singletons, move per\u2011request bits to scoped containers, and add a guard that resets state on each request. The speed stays, the weird goes away.<\/p>\n<h3><span id=\"After_deploy_the_first_few_requests_are_slow\">\u201cAfter deploy, the first few requests are slow\u201d<\/span><\/h3>\n<p>Classic cold cache. Warm OPcache with a few synthetic hits, cache routes\/config\/views, and reload PHP\u2011FPM to clear stale code maps. If you\u2019re shipping containers, build with the caches pre\u2011baked and run a startup probe that hits hot paths before sending traffic.<\/p>\n<h3><span id=\"Memory_leak_or_just_busy\">\u201cMemory leak or just busy?\u201d<\/span><\/h3>\n<p>People call everything a memory leak until you show them a graph. If PHP\u2011FPM memory climbs during a burst and drops after, that\u2019s normal. If it climbs and never falls, check for genuine leaks in extensions or a worker count that\u2019s too high. Drop workers, watch for stability, then add back one at a time. Incremental beats heroic.<\/p>\n<h2 id=\"section-8\"><span id=\"A_Quick_Word_on_CDN_Caching_Layers_and_When_to_Offload\">A Quick Word on CDN, Caching Layers, and When to Offload<\/span><\/h2>\n<p>Laravel can do a lot, but it doesn\u2019t need to do everything. If you\u2019re serving heavy static assets or cacheable pages for logged\u2011out users, let your CDN and Nginx help. Edge caching plus sensible Cache\u2011Control headers can make your app feel instant. If this is unexplored territory for you, I collected a friendly, practical playbook in <a href=\"https:\/\/www.dchost.com\/blog\/en\/cdn-onbellekleme-cache-control-ve-edge-kurallari-wordpress-ve-woocommercede-tam-isabet-ayarlar\/\">this CDN caching guide<\/a>. It\u2019ll give you ideas to steal for Laravel, especially around headers and bypass rules.<\/p>\n<h2 id=\"section-9\"><span id=\"Putting_It_All_Together\">Putting It All Together<\/span><\/h2>\n<p>Performance tuning isn\u2019t a one\u2011time hero moment; it\u2019s a set of habits. Size your PHP\u2011FPM pools with room to breathe and logs that tell you when requests drag. Give OPcache the memory it deserves and pair it with deploys that warm the path. Move heavy work to queues, then give Horizon a layout that keeps each type of job in its own lane. Let Redis be fast without letting it hoard memory\u2014set maxmemory, choose an eviction policy on purpose, and separate queues from caches if you can. And if you\u2019re ready for a bigger jump, bring Octane online with respect for state, not fear.<\/p>\n<p>Here\u2019s my parting advice: change one thing at a time and watch it. A single morning with good dashboards will beat a month of guessing. Baseline before you touch anything, tune, then check again. Keep a notepad of what helped and what didn\u2019t. Over time, your instincts sharpen, and suddenly you\u2019re that person on the team who can look at a graph and say, \u201cI know exactly what to try.\u201d Hope this was helpful! If you want me to dig into a specific setup in a future post, tell me what you\u2019re running and what\u2019s bugging you\u2014I\u2019m all ears.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>So I\u2019m staring at the server graphs on a sleepy Tuesday, coffee in hand, wondering why a perfectly fine Laravel app suddenly feels like it\u2019s running through molasses. The code was clean, the database wasn\u2019t screaming, and yet requests were dragging their feet. Ever had that moment when you\u2019re sure the problem isn\u2019t the code\u2026 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1406,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-1405","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\/1405","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=1405"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1405\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/1406"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=1405"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=1405"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=1405"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}