<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sanad Kit</title>
    <description>The latest articles on DEV Community by Sanad Kit (@sanadkit).</description>
    <link>https://dev.to/sanadkit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4189618%2F704f12af-7be8-4e90-9d06-533f502132b5.png</url>
      <title>DEV Community: Sanad Kit</title>
      <link>https://dev.to/sanadkit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sanadkit"/>
    <language>en</language>
    <item>
      <title>Laravel queues in production: 9 settings that stop jobs from failing silently (or running twice)</title>
      <dc:creator>Sanad Kit</dc:creator>
      <pubDate>Sun, 25 Oct 2026 21:23:23 +0000</pubDate>
      <link>https://dev.to/sanadkit/laravel-queues-in-production-9-settings-that-stop-jobs-from-failing-silently-or-running-twice-40on</link>
      <guid>https://dev.to/sanadkit/laravel-queues-in-production-9-settings-that-stop-jobs-from-failing-silently-or-running-twice-40on</guid>
      <description>&lt;p&gt;Queues are the part of a Laravel app that breaks without telling you. A request that throws a 500 shows up in your error tracker within seconds. A job that times out, gets retried after its worker was killed, or runs before its database row exists can just... not do its work. Days later, a customer asks why their invoice email never came.&lt;/p&gt;

&lt;p&gt;Below are nine settings I check on every production Laravel app that uses queues. Everything here is verified against the official Laravel queue docs (12.x and 13.x) and the default &lt;code&gt;config/queue.php&lt;/code&gt; in the Laravel skeleton. Nothing needs a package beyond the framework.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Laravel 13 note:&lt;/strong&gt; the 13.x docs now show PHP attributes for job settings: &lt;code&gt;#[Tries(10)]&lt;/code&gt;, &lt;code&gt;#[Timeout(120)]&lt;/code&gt;, &lt;code&gt;#[Backoff([1, 5, 10])]&lt;/code&gt;, &lt;code&gt;#[FailOnTimeout]&lt;/code&gt;, &lt;code&gt;#[DeleteWhenMissingModels]&lt;/code&gt; (from &lt;code&gt;Illuminate\Queue\Attributes&lt;/code&gt;). The public properties used below still work. In 13.x the framework reads the attribute first and falls back to the property.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;retry_after&lt;/code&gt; must be longer than your worker &lt;code&gt;--timeout&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;These two settings sound similar but aren't the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;retry_after&lt;/code&gt; (per connection, in &lt;code&gt;config/queue.php&lt;/code&gt;) is how many seconds the queue waits before it assumes a reserved job is lost and hands it to another worker.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--timeout&lt;/code&gt; (on &lt;code&gt;queue:work&lt;/code&gt;, default &lt;strong&gt;60s&lt;/strong&gt;) is how long a worker lets a job run before killing it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;--timeout&lt;/code&gt; is &lt;strong&gt;longer&lt;/strong&gt; than &lt;code&gt;retry_after&lt;/code&gt;, a slow job gets handed to a second worker while the first one is still processing it, so it runs twice. The docs are explicit: &lt;code&gt;--timeout&lt;/code&gt; should always be at least several seconds shorter than &lt;code&gt;retry_after&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/queue.php (skeleton default for the database driver)&lt;/span&gt;
&lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'database'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'connection'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_QUEUE_CONNECTION'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'table'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_QUEUE_TABLE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'jobs'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'queue'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_QUEUE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'retry_after'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_QUEUE_RETRY_AFTER'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'after_commit'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# retry_after = 90, so keep the worker timeout clearly below it&lt;/span&gt;
php artisan queue:work database &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;75
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If one job legitimately needs 10 minutes, don't raise &lt;code&gt;retry_after&lt;/code&gt; for everything. Put it on its own connection or queue with its own &lt;code&gt;retry_after&lt;/code&gt; and a dedicated worker.&lt;/p&gt;

&lt;p&gt;SQS is the exception. It has no &lt;code&gt;retry_after&lt;/code&gt; and uses the queue's visibility timeout from the AWS console instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Set &lt;code&gt;$timeout&lt;/code&gt; on the job itself (and install PCNTL)
&lt;/h2&gt;

&lt;p&gt;A per-job timeout overrides the CLI flag, which is useful for the one slow export job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GenerateMonthlyReport&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// seconds; must stay below retry_after&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$failOnTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// don't burn retries on a job that will just time out again&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two gotchas from the docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Job timeouts require the &lt;strong&gt;PCNTL&lt;/strong&gt; PHP extension. Without it, the timeout simply isn't enforced.&lt;/li&gt;
&lt;li&gt;Blocking I/O (sockets, outgoing HTTP) may ignore the job timeout, so set timeouts on the client too, e.g. &lt;code&gt;Http::timeout(10)-&amp;gt;connectTimeout(3)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Decide &lt;code&gt;tries&lt;/code&gt; deliberately, because the default is one attempt
&lt;/h2&gt;

&lt;p&gt;By default Laravel attempts a job &lt;strong&gt;once&lt;/strong&gt;. That's fine for many jobs, but it bites in two places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When you use &lt;code&gt;WithoutOverlapping&lt;/code&gt; or &lt;code&gt;RateLimited&lt;/code&gt; middleware, a release back onto the queue &lt;strong&gt;counts as an attempt&lt;/strong&gt;. With &lt;code&gt;tries = 1&lt;/code&gt;, a rate-limited job fails instead of waiting.&lt;/li&gt;
&lt;li&gt;Jobs that call flaky third-party APIs.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SyncCustomerToCrm&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$tries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// allow releases from middleware&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$maxExceptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// but fail after 3 real exceptions&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For "keep trying for 30 minutes, however many attempts that takes", use &lt;code&gt;retryUntil()&lt;/code&gt;. It takes precedence over &lt;code&gt;tries&lt;/code&gt; when both are defined:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;retryUntil&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;DateTime&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Add &lt;code&gt;backoff&lt;/code&gt; so retries don't hammer a failing dependency
&lt;/h2&gt;

&lt;p&gt;Without backoff, a job that throws is released for &lt;strong&gt;immediate&lt;/strong&gt; retry. When a payment gateway or SMTP server is down, that burns all your attempts in a few seconds. An array gives you escalating delays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1s before the 2nd attempt, 5s before the 3rd, then 10s for every attempt after that&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a worker-wide default: &lt;code&gt;php artisan queue:work --tries=3 --backoff=3&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Dispatch after the transaction commits
&lt;/h2&gt;

&lt;p&gt;This is the classic "silent" one. You create an order inside &lt;code&gt;DB::transaction()&lt;/code&gt; and dispatch &lt;code&gt;SendOrderConfirmation&lt;/code&gt; inside the closure. A fast worker can pick the job up &lt;strong&gt;before the transaction commits&lt;/strong&gt;, so the order isn't in the database yet and the job fails with &lt;code&gt;ModelNotFoundException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It gets worse if the job has &lt;code&gt;public $deleteWhenMissingModels = true;&lt;/code&gt;. Laravel then quietly discards the job, by design, and you never hear about it.&lt;/p&gt;

&lt;p&gt;Fix it globally per connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'redis'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="s1"&gt;'after_commit'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or per dispatch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;SendOrderConfirmation&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;afterCommit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;after_commit&lt;/code&gt; enabled, jobs dispatched inside a transaction that rolls back are discarded too, which is usually what you want. The setting also applies to queued listeners, mailables, notifications and broadcast events.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Make Supervisor wait for your longest job
&lt;/h2&gt;

&lt;p&gt;On deploy or restart, Supervisor sends a stop signal and waits &lt;code&gt;stopwaitsecs&lt;/code&gt; before force-killing the worker. If that's shorter than your longest job, the job is killed mid-way. It then sits reserved until &lt;code&gt;retry_after&lt;/code&gt; passes, and with &lt;code&gt;tries = 1&lt;/code&gt; it ends up failed.&lt;/p&gt;

&lt;p&gt;Adapted from the docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[program:laravel-worker]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;process_name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%(program_name)s_%(process_num)02d&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;php /var/www/app/artisan queue:work redis --sleep=3 --tries=3 --timeout=75 --max-time=3600&lt;/span&gt;
&lt;span class="py"&gt;autostart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;autorestart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;stopasgroup&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;killasgroup&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;www-data&lt;/span&gt;
&lt;span class="py"&gt;numprocs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;
&lt;span class="py"&gt;redirect_stderr&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;stdout_logfile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/www/app/storage/logs/worker.log&lt;/span&gt;
&lt;span class="py"&gt;stopwaitsecs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule from the docs: &lt;code&gt;stopwaitsecs&lt;/code&gt; must be greater than the runtime of your longest-running job.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Restart workers on every deploy, and recycle them
&lt;/h2&gt;

&lt;p&gt;Workers are long-lived processes. They keep running the code they booted with until they're restarted. Add this to every deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan queue:restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workers finish their current job and exit gracefully, then Supervisor starts them on the new code. The restart signal is stored in the &lt;strong&gt;cache&lt;/strong&gt;, so a misconfigured cache driver means workers never see it.&lt;/p&gt;

&lt;p&gt;Also recycle workers so slow memory growth doesn't build up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan queue:work &lt;span class="nt"&gt;--max-time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3600   &lt;span class="c"&gt;# exit after an hour&lt;/span&gt;
php artisan queue:work &lt;span class="nt"&gt;--max-jobs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000   &lt;span class="c"&gt;# or after 1000 jobs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One Redis-specific trap: &lt;code&gt;'block_for' =&amp;gt; 0&lt;/code&gt; makes workers block indefinitely waiting for a job, and that also stops them handling &lt;code&gt;SIGTERM&lt;/code&gt; until the next job arrives. Use a small positive value like &lt;code&gt;5&lt;/code&gt; if you set it at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Keep failed jobs, react to them, prune them
&lt;/h2&gt;

&lt;p&gt;A failed job you can't see is a lost job. Check three things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage.&lt;/strong&gt; The skeleton's default &lt;code&gt;failed&lt;/code&gt; driver is &lt;code&gt;database-uuids&lt;/code&gt;, which writes to &lt;code&gt;failed_jobs&lt;/code&gt;. Make sure the table migration has run in production, and don't set the driver to &lt;code&gt;null&lt;/code&gt; unless you mean it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A &lt;code&gt;failed()&lt;/code&gt; hook&lt;/strong&gt; for cleanup or user-facing compensation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;?Throwable&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// mark the export as failed, notify the user, release a reserved slot...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One global listener&lt;/strong&gt; that alerts you, in &lt;code&gt;AppServiceProvider::boot()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Queue\Events\JobFailed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Queue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;failing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;JobFailed&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;critical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Queue job failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'connection'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;connectionName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'job'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;resolveName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then retry with &lt;code&gt;php artisan queue:retry all&lt;/code&gt; (or by UUID) once the cause is fixed, and keep the table small. In Laravel 11+ that goes in &lt;code&gt;routes/console.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Schedule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'queue:prune-failed --hours=168'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;daily&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'queue:prune-batches --hours=48'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;daily&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// if you use batches&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. Alert on queue depth, not just failures
&lt;/h2&gt;

&lt;p&gt;Sometimes nothing fails, the backlog just grows: a worker died, Supervisor wasn't enabled after a reboot, or a burst of jobs arrived. Laravel ships &lt;code&gt;queue:monitor&lt;/code&gt;, which fires a &lt;code&gt;QueueBusy&lt;/code&gt; event when a queue crosses a threshold. Schedule it every minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/console.php&lt;/span&gt;
&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'queue:monitor redis:default,redis:emails --max=500'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;everyMinute&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AppServiceProvider::boot()&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Queue\Events\QueueBusy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;QueueBusy&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;critical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Queue &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; jobs waiting"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// or Notification::route('mail', 'ops@yourcompany.com')-&amp;gt;notify(...)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick &lt;code&gt;--max&lt;/code&gt; from your normal peak, not a guess: look at a busy day's queue size and alert well above it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 2-minute checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;--timeout&lt;/code&gt; (and every job's &lt;code&gt;$timeout&lt;/code&gt;) &amp;lt; &lt;code&gt;retry_after&lt;/code&gt;, by a clear margin&lt;/li&gt;
&lt;li&gt;[ ] PCNTL installed on worker servers&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;tries&lt;/code&gt; / &lt;code&gt;retryUntil&lt;/code&gt; set for jobs behind rate limits or overlap locks&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;backoff&lt;/code&gt; on anything that talks to an external service&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;after_commit =&amp;gt; true&lt;/code&gt; (or &lt;code&gt;-&amp;gt;afterCommit()&lt;/code&gt;) where jobs are dispatched in transactions&lt;/li&gt;
&lt;li&gt;[ ] Supervisor &lt;code&gt;stopwaitsecs&lt;/code&gt; &amp;gt; longest job&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;queue:restart&lt;/code&gt; in the deploy script, workers recycled with &lt;code&gt;--max-time&lt;/code&gt; / &lt;code&gt;--max-jobs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;failed_jobs&lt;/code&gt; exists, a &lt;code&gt;Queue::failing&lt;/code&gt; alert is set up, pruning is scheduled&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;queue:monitor&lt;/code&gt; scheduled with a real threshold&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you use Horizon, the same ideas apply, but the worker options live in &lt;code&gt;config/horizon.php&lt;/code&gt; instead of Supervisor command flags.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write short, practical notes from running Laravel in production. Follow me here on dev.to to get one every week. Next up: making scheduled tasks safe on multi-server deploys.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
    <item>
      <title>Mapping a CodeIgniter 3 app to Laravel 12: the 12 patterns that cover most of the code</title>
      <dc:creator>Sanad Kit</dc:creator>
      <pubDate>Sat, 17 Oct 2026 01:10:27 +0000</pubDate>
      <link>https://dev.to/sanadkit/mapping-a-codeigniter-3-app-to-laravel-12-the-12-patterns-that-cover-most-of-the-code-4g2n</link>
      <guid>https://dev.to/sanadkit/mapping-a-codeigniter-3-app-to-laravel-12-the-12-patterns-that-cover-most-of-the-code-4g2n</guid>
      <description>&lt;p&gt;CodeIgniter 3 still runs a lot of production apps. TechLeads counts ~124k CodeIgniter sites, and HTTP Archive saw 234k pages in Sept 2026, down 16% year on year. CI4 is effectively a rewrite, so many teams jump straight to Laravel.&lt;/p&gt;

&lt;p&gt;I've been cataloguing what a CI3 → Laravel move actually touches. Here are the mechanical mappings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CodeIgniter 3&lt;/th&gt;
&lt;th&gt;Laravel 12&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;application/controllers/Users.php&lt;/code&gt; + implicit &lt;code&gt;/users/edit/5&lt;/code&gt; routing&lt;/td&gt;
&lt;td&gt;Controller + explicit &lt;code&gt;Route::get('users/edit/{id}', ...)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$this-&amp;gt;load-&amp;gt;model('user_model')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Constructor injection / Eloquent model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$this-&amp;gt;db-&amp;gt;where('id',$id)-&amp;gt;get('users')-&amp;gt;row()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DB::table('users')-&amp;gt;where('id',$id)-&amp;gt;first()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$this-&amp;gt;input-&amp;gt;post('name')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$request-&amp;gt;input('name')&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$this-&amp;gt;form_validation-&amp;gt;set_rules(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$request-&amp;gt;validate([...])&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$this-&amp;gt;session-&amp;gt;set_flashdata()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;session()-&amp;gt;flash()&lt;/code&gt; / &lt;code&gt;-&amp;gt;with()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$this-&amp;gt;load-&amp;gt;view('x', $data)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;view('x', $data)&lt;/code&gt; + Blade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;base_url()&lt;/code&gt;, &lt;code&gt;site_url()&lt;/code&gt;, &lt;code&gt;anchor()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;url()&lt;/code&gt;, &lt;code&gt;route()&lt;/code&gt;, &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MY_Controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Base controller + middleware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hooks&lt;/td&gt;
&lt;td&gt;middleware / event listeners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;config/database.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.env&lt;/code&gt; + &lt;code&gt;config/database.php&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;$this-&amp;gt;upload&lt;/code&gt; library&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$request-&amp;gt;file()-&amp;gt;store()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The hard parts aren't in the table: HMVC modules, a custom &lt;code&gt;system/&lt;/code&gt; core, and logic hidden in hooks.&lt;/p&gt;

&lt;p&gt;I'm building a tool that does the mechanical part automatically and outputs a PR plus a report of what it couldn't convert. If you have a CI3 app you'd like scanned for free, the form is here: &lt;a href="https://ci2laravel.netlify.app/?src=devto" rel="noopener noreferrer"&gt;ci2laravel.netlify.app&lt;/a&gt;. Which patterns hurt you most? Tell me in the comments.&lt;/p&gt;

</description>
      <category>legacy</category>
      <category>laravel</category>
      <category>php</category>
      <category>codeigniter</category>
    </item>
  </channel>
</rss>
