<?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: Elias Ahmed</title>
    <description>The latest articles on DEV Community by Elias Ahmed (@eliasahmeddev).</description>
    <link>https://dev.to/eliasahmeddev</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%2F3990675%2Fd517547d-d0a4-4108-be88-6b9e3cd6af75.png</url>
      <title>DEV Community: Elias Ahmed</title>
      <link>https://dev.to/eliasahmeddev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eliasahmeddev"/>
    <language>en</language>
    <item>
      <title>Your Midnight Billing Cron Job Will Eventually Fail (Here's the Fix)</title>
      <dc:creator>Elias Ahmed</dc:creator>
      <pubDate>Wed, 08 Jul 2026 08:54:01 +0000</pubDate>
      <link>https://dev.to/eliasahmeddev/-your-midnight-billing-cron-job-will-eventually-fail-heres-the-fix-2d1h</link>
      <guid>https://dev.to/eliasahmeddev/-your-midnight-billing-cron-job-will-eventually-fail-heres-the-fix-2d1h</guid>
      <description>&lt;p&gt;It's 12:00 AM. Your cron job begins, like it does every night.&lt;/p&gt;

&lt;p&gt;Except tonight, you have 40,000 active subscribers instead of 400.&lt;/p&gt;

&lt;p&gt;The job begins charging every subscription that's due to expire that day, and in a few seconds, your database connections get maxed out. Stripe may begin returning &lt;code&gt;429 Too Many Requests&lt;/code&gt; errors. Then PHP times out. Now you don't know who actually got charged and who didn't.&lt;/p&gt;

&lt;p&gt;This is the midnight cron nightmare. It's one of the most common yet avoidable flaws in subscription billing systems, and almost every self-built backend hits it at one point. Here's exactly why it happens, and how to build it correctly in Laravel 13.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Straightforward Approach (And Why It Feels Fine at First)
&lt;/h2&gt;

&lt;p&gt;Most subscription systems start out with something like this:&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="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Schedule&lt;/span&gt; &lt;span class="nv"&gt;$schedule&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="nv"&gt;$schedule&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;call&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="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Subscription&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ends_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;='&lt;/span&gt;&lt;span class="p"&gt;,&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;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'active'&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;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;each&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;Subscription&lt;/span&gt; &lt;span class="nv"&gt;$subscription&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chargeSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$subscription&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;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dailyAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'00:00'&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;It's very simple, readable, and works perfectly fine in local dev with 10 test subscriptions. It'll probably survive your first few hundred users too. But this approach doesn't fail because it's wrong on day one. It fails because it's a ticking clock tied directly to your growth curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Breaks at Scale
&lt;/h2&gt;

&lt;p&gt;There are four massive issues hiding behind that simple loop:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The Chokepoint&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If every subscription is set to renew "at expiration," and most of your users signed up around the same time (which happens naturally with growth spikes, promotions, or seasonal signups), you get a massive cluster of renewals landing on the exact same timestamp. This creates one enormous batch with zero distribution. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Synchronous Charging Inside a Loop&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Calling a payment API synchronously inside a &lt;code&gt;foreach&lt;/code&gt;, inside a scheduled command, means your total runtime is &lt;code&gt;(number of subscriptions) × (Stripe API latency)&lt;/code&gt;. At 40,000 subscriptions and ~300ms per charge, that's over three hours in a single PHP process. That process holds a database connection open the whole time, has a &lt;code&gt;max_execution_time&lt;/code&gt; limit, and has no way to resume if it crashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Stripe (and your database) Will Rate-Limit You&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Payment providers cap requests per second for a reason. Hit the API synchronously from one process and you'll get throttled. Throttled requests with no backoff just fail. And if you retry without idempotency, those failures can turn into duplicate charges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. No Idempotency = No Safety Net&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the job crashes at subscription #22,000 and you simply re-run it, you now risk re-charging the first 22,000 users who already succeeded. If you aren't tracking which renewals already ran, every retry risks double-charging your users.&lt;/p&gt;

&lt;p&gt;None of these are edge cases. They're the default behavior of the naive cron approach once you cross a few thousand active subscriptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Professional Fix: Space-out, Queue, and Track
&lt;/h2&gt;

&lt;p&gt;The fix isn't a bigger server or a longer timeout. It's rethinking three things: &lt;em&gt;when&lt;/em&gt; renewals happen, &lt;em&gt;how&lt;/em&gt; they're executed, and &lt;em&gt;what proof&lt;/em&gt; you keep that they happened. Here's the shape of it end to end:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm74avlxutm930t3hzgzy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm74avlxutm930t3hzgzy.png" alt="Subscription Billing Workflow Sequence" width="800" height="824"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Stagger Billing Windows — Don't Renew Everyone at Once
&lt;/h3&gt;

&lt;p&gt;Instead of one giant midnight batch, spread renewal &lt;em&gt;attempts&lt;/em&gt; across a window, and separate the renewal attempt from the actual expiration:&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;// Two distinct timestamps instead of one&lt;/span&gt;
&lt;span class="n"&gt;renewal_due_at&lt;/span&gt;  &lt;span class="c1"&gt;// when we should attempt a renew&lt;/span&gt;
&lt;span class="n"&gt;ends_at&lt;/span&gt;         &lt;span class="c1"&gt;// when access actually ends if renewal fails&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attempting renewal a day or two &lt;em&gt;before&lt;/em&gt; &lt;code&gt;ends_at&lt;/code&gt; rather than exactly at expiration gives you room to retry failed charges, notify the user, and smoothly kill access if the payment fails, without giving away free unpaid access or charging everyone at the same time.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Never Charge Synchronously Inside the Scheduler
&lt;/h3&gt;

&lt;p&gt;The scheduler's only job should be to find &lt;em&gt;who&lt;/em&gt; needs billing and dispatch work — not to do the billing itself:&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="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Schedule&lt;/span&gt; &lt;span class="nv"&gt;$schedule&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="nv"&gt;$schedule&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;call&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="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Subscription&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&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;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'renewal_due_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;='&lt;/span&gt;&lt;span class="p"&gt;,&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;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auto_renew'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&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;chunkById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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="nv"&gt;$subscriptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$subscriptions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$subscription&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;ProcessRenewal&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;$subscription&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;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dailyAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"00:00"&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;Two important changes here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;chunkById()&lt;/code&gt; instead of &lt;code&gt;get()&lt;/code&gt;&lt;/strong&gt; — you never load 40,000 Eloquent models into memory at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dispatch a queued job per subscription instead of charging inline&lt;/strong&gt; — the scheduler's job finishes in milliseconds. The actual charging happens in the queue, where Laravel's worker pool naturally throttles concurrency, retries failed jobs, and won't take your whole app down if one charge hangs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice there's no &lt;code&gt;-&amp;gt;onQueue('billing')&lt;/code&gt; at the call site. Laravel 13 added &lt;code&gt;Queue::route()&lt;/code&gt;, which lets you define the queue and connection in one place instead of repeating it every time you dispatch a 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="nc"&gt;Queue&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProcessRenewal&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'billing'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;php artisan queue:work&lt;/code&gt; with a sane &lt;code&gt;--tries&lt;/code&gt; and backoff strategy turns "one giant fragile loop" into "many small, retryable, independently-failing units of work", which is the more reliable and stable approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Make Every Renewal Attempt Idempotent
&lt;/h3&gt;

&lt;p&gt;This is the piece most naive implementations skip entirely, and it's the one that actually saves you from double-charging customers on retry:&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;ProcessRenewal&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&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;// Bail if this cycle already resolved — either the renewal&lt;/span&gt;
        &lt;span class="c1"&gt;// already succeeded (renewal_due_at moved forward) or it&lt;/span&gt;
        &lt;span class="c1"&gt;// already failed (auto_renew got turned off).&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;auto_renew&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;renewal_due_at&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isFuture&lt;/span&gt;&lt;span class="p"&gt;())&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="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chargeCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;successful&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="s1"&gt;'renewal_due_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;addMonth&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="s1"&gt;'ends_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;addMonth&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;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'auto_renew'&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="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&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;queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RenewalFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;subscription&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This checks real state, not a timestamp: if the renewal succeeded, &lt;code&gt;renewal_due_at&lt;/code&gt; is already in the future. If it failed, &lt;code&gt;auto_renew&lt;/code&gt; is already off. Either way, the job bails. If neither happened yet, it means the last attempt didn't reach an outcome, so a retry from Laravel's &lt;code&gt;--tries&lt;/code&gt;/backoff mechanism can still fire and actually try the charge again. Pair this with treating the webhook as the real source of truth for a successful charge, and you've closed off the two biggest sources of billing bugs: duplicate charges and false-positive activations.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Keep Payment History, Don't Overwrite State
&lt;/h3&gt;

&lt;p&gt;One more habit worth adopting: never delete or overwrite a payment record. Every attempt (successful, failed, or pending) should be its own row. When a customer emails asking "why was I charged twice" or "why did my renewal fail," you want a full audit trail, not a single &lt;code&gt;status&lt;/code&gt; column that only remembers the most recent outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;The shift is simple to state and easy to underestimate: &lt;strong&gt;stop treating billing as a loop, and start treating it as a distributed, idempotent, queue-driven sequence.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stagger renewal windows so you're not billing everyone at once&lt;/li&gt;
&lt;li&gt;Let the scheduler dispatch, not execute&lt;/li&gt;
&lt;li&gt;Queue the actual charge with retries and backoff&lt;/li&gt;
&lt;li&gt;Track idempotency so retries are always safe&lt;/li&gt;
&lt;li&gt;Trust webhooks over synchronous responses&lt;/li&gt;
&lt;li&gt;Keep full payment history for every attempt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic. Separating &lt;em&gt;scheduling&lt;/em&gt; from &lt;em&gt;doing&lt;/em&gt;, and &lt;em&gt;attempting&lt;/em&gt; from &lt;em&gt;confirming&lt;/em&gt;. But it's the difference between a billing system that quietly scales past 40,000 users and one that wakes you up at 3 AM.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Reference Implementation
&lt;/h2&gt;

&lt;p&gt;I ended up building this system in an open-source Laravel 13 backend called &lt;strong&gt;&lt;a href="https://github.com/EliasAhmedDev/SubEngine" rel="noopener noreferrer"&gt;SubEngine&lt;/a&gt;&lt;/strong&gt; — a webhook-driven subscription billing engine that implements strategic renewal timing, idempotent scheduler jobs, Stripe webhook verification, and full payment history preservation, with 31 passing tests covering the lifecycle end to end.&lt;/p&gt;

&lt;p&gt;It's lightweight, focused on the backend billing logic that tends to break in the real world. If you're building (or rebuilding) subscription billing in Laravel, it's worth a look as a reference or a starting template.&lt;/p&gt;

&lt;p&gt;If this saved you from a future midnight crash, a star on the repo goes a long way.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>backend</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
