<?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: Ayesha</title>
    <description>The latest articles on DEV Community by Ayesha (@ayesha_921).</description>
    <link>https://dev.to/ayesha_921</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%2F4180638%2F20681e69-52a2-45f2-8759-55b49e311ed8.jpg</url>
      <title>DEV Community: Ayesha</title>
      <link>https://dev.to/ayesha_921</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ayesha_921"/>
    <language>en</language>
    <item>
      <title>How to Hire a Python Developer in 2026 Without Hiring the Wrong One</title>
      <dc:creator>Ayesha</dc:creator>
      <pubDate>Mon, 02 Nov 2026 05:15:57 +0000</pubDate>
      <link>https://dev.to/ayesha_921/how-to-hire-a-python-developer-in-2026-without-hiring-the-wrong-one-5780</link>
      <guid>https://dev.to/ayesha_921/how-to-hire-a-python-developer-in-2026-without-hiring-the-wrong-one-5780</guid>
      <description>&lt;h1&gt;
  
  
  How to Hire a Python Developer in 2026 Without Hiring the Wrong One
&lt;/h1&gt;

&lt;p&gt;If you need to hire a Python developer in 2026, you have already run into the central problem of this market: every resume says "Python." A bootcamp graduate who wrote three automation scripts says Python. A data scientist who has never deployed a web app says Python. An engineer who has run Django in production for six years also says Python. These are not the same hire, they do not cost the same money, and picking the wrong one costs you weeks of onboarding before the mismatch becomes obvious. This guide gives you a hiring process that filters for what you actually need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Python Hiring Mistake Almost Everyone Makes
&lt;/h2&gt;

&lt;p&gt;Most job posts read like this: "Looking for a Python developer. Must know Python, Django, Flask, FastAPI, Pandas, TensorFlow, AWS, Docker." That post will attract everyone and qualify no one. It signals to strong engineers that you do not know what you need, and the best candidates — the ones with options — skip it.&lt;/p&gt;

&lt;p&gt;Python is not one job. It is a language stretched across at least six different professions, and a developer who is excellent in one is often mediocre in another. Hiring starts with naming which Python you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  There Is No Generic Python Developer — Only Clusters
&lt;/h2&gt;

&lt;p&gt;Here are the clusters you will actually encounter:&lt;/p&gt;

&lt;p&gt;Django developers build full web applications — admin panels, user accounts, billing pages. If your product is a classic web app with a database behind it, this is your cluster.&lt;/p&gt;

&lt;p&gt;FastAPI and Flask developers build APIs and backend services. FastAPI development has become the default for new API work in 2026, and this cluster is what most "we need a backend developer" posts actually mean.&lt;/p&gt;

&lt;p&gt;Data engineers and analysts live in Pandas, NumPy, and SQL. They move and clean data. They usually cannot build you a web app, and you should not ask them to.&lt;/p&gt;

&lt;p&gt;Machine learning engineers train and serve models. This cluster commands a real premium in 2026 because every product roadmap now has an AI feature in it.&lt;/p&gt;

&lt;p&gt;Automation and scraping developers write scripts with BeautifulSoup, Scrapy, Playwright, and Selenium. This is honest, useful, cheaper work — I built a production screenshot API on Python, Flask, Selenium, and Docker myself, and it is a completely different skill from training models.&lt;/p&gt;

&lt;p&gt;DevOps-adjacent Python developers write the glue: CI/CD scripts, Lambda functions, infrastructure tooling.&lt;/p&gt;

&lt;p&gt;Before you write a single word of your job post, pick one cluster. If you genuinely need two, hire for the harder one first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Write a Job Post That Names the Cluster
&lt;/h2&gt;

&lt;p&gt;A job post that attracts the right people names four things: the framework, the production surface, the deployment target, and what the hire will own in the first 90 days. Compare these two openings for the same role:&lt;/p&gt;

&lt;p&gt;Weak: "Python developer needed for backend work. Must know Django, REST APIs, and AWS."&lt;/p&gt;

&lt;p&gt;Strong: "We need a FastAPI developer to own our billing API — roughly 2 million requests a month, Postgres on RDS, deployed with Docker on AWS. First 90 days: take ownership of the webhook pipeline, add idempotency, and get our test coverage from 40 percent to 80 percent."&lt;/p&gt;

&lt;p&gt;The strong post does three things: it names the cluster (FastAPI APIs), it names the scale (so seniors self-select in and juniors self-select out), and it names the actual work (so the candidate can picture the job). Strong engineers apply to specific problems, not to the word Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Screen for Production Experience, Not Tutorials
&lt;/h2&gt;

&lt;p&gt;Resumes lie by omission. Git history tells the truth. When you hire a Python developer, spend twenty minutes on their public work before any call.&lt;/p&gt;

&lt;p&gt;Look for repositories with migrations, tests, a Dockerfile, and CI configuration. That combination means the code was meant to run somewhere real. A repo with only Jupyter notebooks or forked tutorial projects means the code was meant to be looked at.&lt;/p&gt;

&lt;p&gt;A quick check you can run on any candidate repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"candidate-name"&lt;/span&gt; &lt;span class="nt"&gt;--since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"12 months ago"&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="nb"&gt;ls &lt;/span&gt;Dockerfile docker-compose.yml requirements.txt pyproject.toml 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it works: sustained commits over months signal real maintenance, and the presence of Docker plus dependency files signals the code was deployed, not just written.&lt;/p&gt;

&lt;p&gt;Then, in the interview, ask for one production story: "Walk me through the last time a Python service you owned went down. What did the logs say, and what did you change?" You are listening for specifics — a real traceback, a real query that locked a table, a real config mistake. Vague answers like "we restarted the server and it was fine" tell you this person has never owned a system at 2 a.m. Follow up with: "Where do secrets live, and how do they get into the running container?" Anyone who has shipped will answer in one sentence. Anyone who has not will wander.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Use a Paid Trial Task That Mirrors Real Work
&lt;/h2&gt;

&lt;p&gt;Unpaid take-home tests filter out every senior with options. Pay for four to six hours of the candidate's time at their rate, and make the task mirror the actual job. A paid trial is the single most reliable filter when you hire a Python developer sight unseen. For a FastAPI development role, a task that works looks like this: build one endpoint that accepts a signup payload, validates it, rejects duplicate emails with a proper 409, and is covered by at least one test.&lt;/p&gt;

&lt;p&gt;Show the candidate what "good" looks like without writing their solution for them. This is the shape of a production-grade FastAPI endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SignupPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user_store&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Injected dependency: trivial to swap for a test double.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;UserStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/signup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_201_CREATED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;signup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SignupPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_user_store&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Email already registered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it works: validation happens at the boundary with Pydantic, the database is hidden behind an injected dependency so the route is testable, error cases return explicit status codes, and no ORM internals leak into the HTTP layer. When you review the submission, do not grade on cleverness. Grade on exactly these four things: does it validate input, can you test it without a database, are errors explicit, and would you let this run on your infrastructure?&lt;/p&gt;

&lt;p&gt;One more rule for the trial: give every candidate the same task and the same time box. You are comparing engineers, not tasks.&lt;/p&gt;

&lt;p&gt;If the role is really about building an API rather than hiring long-term, read how custom REST API development is scoped and priced first: &lt;a href="https://webixyhub.com/blog/custom-rest-api-development-cost-hiring-guide" rel="noopener noreferrer"&gt;https://webixyhub.com/blog/custom-rest-api-development-cost-hiring-guide&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Django vs FastAPI vs Flask — Match the Hire to Your Stack
&lt;/h2&gt;

&lt;p&gt;Clients often ask which framework their hire should know, as if there is a right answer. There is not — there is only the answer that matches your codebase. Django is the right choice for admin-heavy products where the built-in admin, ORM, and auth save months. FastAPI is the right choice for modern APIs, especially when type safety and automatic docs matter. Flask is the right choice for small services and embedded tooling where Django would be overhead.&lt;/p&gt;

&lt;p&gt;The hiring implication is simple: do not hire a Django specialist to build your FastAPI service because FastAPI is trending, and do not reject a strong Flask developer for a Django role if they have shipped real products. Frameworks are learnable in weeks. Production judgment is not.&lt;/p&gt;

&lt;p&gt;If you are weighing Python against PHP, my guide to hiring a Laravel developer covers the same vetting process from the other side of the stack: &lt;a href="https://webixyhub.com/blog/hire-laravel-developer-guide" rel="noopener noreferrer"&gt;https://webixyhub.com/blog/hire-laravel-developer-guide&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Costs to Hire a Python Developer in 2026
&lt;/h2&gt;

&lt;p&gt;Rates vary by cluster, seniority, and geography, but the structure of pricing is stable. Freelance rate guides for 2026 report a global average of roughly 61 to 80 dollars per hour for a Python freelancer, with senior offshore engineers typically landing between 2,500 and 3,500 dollars per month for dedicated remote work.&lt;/p&gt;

&lt;p&gt;Three things move the price: the AI overlap premium (anyone who can both build the backend and integrate or serve models costs more, because they replace two hires), the depth of production experience (someone who has owned uptime costs more than someone who has written features), and the engagement model (a dedicated monthly developer is cheaper per hour than the same person on short contracts).&lt;/p&gt;

&lt;p&gt;Do not optimize for the lowest rate. A mid-priced developer who ships in three weeks is cheaper than a cheap developer who ships in three months. If a quote is dramatically below the ranges above, you are not getting a bargain — you are getting someone else's learning curve on your budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red Flags That Should End the Conversation
&lt;/h2&gt;

&lt;p&gt;"I can learn it" at senior prices. Willingness to learn is great in a junior. In a senior billing senior rates, it means you are funding their education.&lt;/p&gt;

&lt;p&gt;No production story. If a candidate cannot describe a real outage they debugged — the traceback, the dead end, the fix — they have never owned a system.&lt;/p&gt;

&lt;p&gt;Tutorial-only GitHub. Forked courses with no original commits, no tests, no deployment artifacts. Everyone starts somewhere, but you are not hiring potential at senior rates.&lt;/p&gt;

&lt;p&gt;Secrets in code. If their sample project commits a .env file or hardcodes an API key, their security habits are disqualifying no matter how clean the code looks.&lt;/p&gt;

&lt;p&gt;Cannot explain deployment. "I push to GitHub and it works" is not a deployment story. You want to hear about the pipeline: build, test, image, deploy, migrate, roll back.&lt;/p&gt;

&lt;p&gt;Fixed price for undefined scope, no questions asked. A developer who quotes without interrogating your requirements has not thought about where the work actually is.&lt;/p&gt;

&lt;p&gt;For AI and ML claims, one extra check: ask how they would serve the model — batch or realtime, and what latency budget they would design for. Hand-waving here means the "ML experience" was a notebook, not a system.&lt;/p&gt;

&lt;p&gt;If your product takes payments, the integration work deserves its own vetting — webhook handling alone has ended launches: &lt;a href="https://webixyhub.com/blog/fix-stripe-webhook-signature-verification-failed-laravel" rel="noopener noreferrer"&gt;https://webixyhub.com/blog/fix-stripe-webhook-signature-verification-failed-laravel&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The One-Week Interview Loop
&lt;/h2&gt;

&lt;p&gt;Day one and two: review their public work asynchronously using the checks above. Day three: a 45-minute call built around one production story and the secrets question. Days four and five: the paid trial task, same task for every candidate. Day six: review the submission against the four grading points and make the decision.&lt;/p&gt;

&lt;p&gt;This loop respects the candidate's time, which matters because the engineers you want are evaluating you at the same time. Slow, multi-round gauntlets with unpaid homework select for candidates with no better options. A fast, paid, realistic process selects for professionals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Checklist Before You Send the Offer
&lt;/h2&gt;

&lt;p&gt;You named the cluster in the job post. You verified production experience in their git history. You heard a real outage story. You ran the same paid trial for every finalist and graded it on validation, testability, explicit errors, and deployability. You checked the rate against the market instead of chasing the lowest bid.&lt;/p&gt;

&lt;p&gt;If that sounds like more process than you want to run to hire a Python developer, there is a simpler option: work with someone who has already done it. I build production Python and Laravel systems — custom Python API development, automation, payment integrations — for clients who would rather ship than interview. You can see the work and get in touch at &lt;a href="https://webixyhub.com" rel="noopener noreferrer"&gt;https://webixyhub.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>fastapi</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Fix 429 Too Many Requests in Laravel: Throttle and Rate Limiting Done Right</title>
      <dc:creator>Ayesha</dc:creator>
      <pubDate>Sun, 01 Nov 2026 05:18:19 +0000</pubDate>
      <link>https://dev.to/ayesha_921/how-to-fix-429-too-many-requests-in-laravel-throttle-and-rate-limiting-done-right-1fp8</link>
      <guid>https://dev.to/ayesha_921/how-to-fix-429-too-many-requests-in-laravel-throttle-and-rate-limiting-done-right-1fp8</guid>
      <description>&lt;p&gt;You push to production, traffic grows, and suddenly your logs fill with 429 Too Many Requests in Laravel. Legitimate users see "Too Many Attempts", your mobile app starts failing, and your first instinct is to just turn the throttle off. That fixes it for a day. Then the bots find the endpoint, and you are debugging a very different problem at 3 a.m.&lt;/p&gt;

&lt;p&gt;This guide walks through every real cause of the 429 Too Many Requests error in Laravel, how to diagnose which one is biting you, and working code that fixes it without leaving your app unprotected. Everything below is tested against Laravel 11 and 12, where the api throttle middleware and named rate limiters work the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 429 Error Actually Means in Laravel
&lt;/h2&gt;

&lt;p&gt;The 429 Too Many Requests response comes from Laravel's throttle middleware. Every request that passes through it gets counted against a named limit — for example, 60 requests per minute. When the count for a key exceeds the limit, Laravel throws a ThrottleRequestsException and responds with 429.&lt;/p&gt;

&lt;p&gt;The important part is the key. Laravel counts attempts per key, and that key is built from the limiter you configured: usually the user's ID for logged-in users, or the IP address for guests. Most 429 Too Many Requests problems in Laravel are not about traffic being too high — they are about the key or the limiter being wrong.&lt;/p&gt;

&lt;p&gt;Every 429 response also carries a Retry-After header telling the client how many seconds to wait. If your custom error handling drops that header, API clients have no way to back off gracefully, and they hammer the endpoint again the moment they retry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cause 1 — The Default API Throttle Is Too Strict
&lt;/h3&gt;

&lt;p&gt;Fresh Laravel apps ship with the api limiter set to 60 requests per minute per user or IP. That is fine for a toy project. A real dashboard with auto-refresh, polling, or a chatty mobile client can burn through 60 requests in seconds.&lt;/p&gt;

&lt;p&gt;Diagnose it first. Before changing anything, confirm the throttle middleware is actually attached to the route you think it is:&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 route:list &lt;span class="nt"&gt;-v&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;throttle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the 429 responses carry an X-RateLimit-Remaining header that bottoms out at exactly 60, you are hitting the default api limiter. The fix is to define your own limiter with a sane budget instead of accepting the default. In Laravel 11 and 12, you define named limiters in your application's bootstrap:&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\Http\Request&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\RateLimiter&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\Cache\RateLimiting\Limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;// app/Providers/AppServiceProvider.php (or bootstrap/app.php)&lt;br&gt;
RateLimiter::for('api', function (Request $request) {&lt;br&gt;
    return Limit::perMinute(300)-&amp;gt;by($request-&amp;gt;user()?-&amp;gt;id ?: $request-&amp;gt;ip());&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;This raises the budget to 300 per minute and, crucially, keys it on the user ID when someone is logged in. IP-keyed limits are dangerous for authenticated users: one office behind a NAT gateway shares a single IP, and five employees can exhaust a guest budget before any one of them looks abusive.&lt;/p&gt;
&lt;h3&gt;
  
  
  Cause 2 — Two Endpoints Sharing One Limiter
&lt;/h3&gt;

&lt;p&gt;Inline limits like throttle:60,1 are easy to write and easy to regret. When you apply throttle:api to two different endpoints, both draw from the same budget. A webhook receiver and your search endpoint should never share a limit — one bursty integration drains the budget and the other starts 429-ing real users.&lt;/p&gt;

&lt;p&gt;The fix is one named limiter per behavior:&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;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'webhooks'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;600&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;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RateLimiter::for('search', fn (Request $request) =&amp;gt; Limit::perMinute(120)-&amp;gt;by($request-&amp;gt;user()?-&amp;gt;id ?: $request-&amp;gt;ip()));&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;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/webhooks/stripe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;WebhookController&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="s1"&gt;'store'&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;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'throttle:webhooks'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Route::get('/search', [SearchController::class, 'index'])&lt;br&gt;
    -&amp;gt;middleware('throttle:search');&lt;/p&gt;

&lt;p&gt;This is the same isolation principle behind good API design: if you are building endpoints that third parties will hit at unpredictable rates, it pays to design them as a proper custom REST API from the start (&lt;a href="https://webixyhub.com/blog/custom-rest-api-development-cost-hiring-guide" rel="noopener noreferrer"&gt;https://webixyhub.com/blog/custom-rest-api-development-cost-hiring-guide&lt;/a&gt;) — isolated limits, versioned routes, and documented Retry-After behavior included.&lt;/p&gt;
&lt;h3&gt;
  
  
  Cause 3 — Behind a Proxy, Every Request Has the Same IP
&lt;/h3&gt;

&lt;p&gt;This is the most common 429 Too Many Requests cause in production Laravel apps and the least obvious. When your app sits behind a load balancer, Cloudflare, or a reverse proxy, every request arrives from the proxy's IP. If your limiter keys on IP, all of your traffic — every user, every bot — counts as one person.&lt;/p&gt;

&lt;p&gt;The symptom: 429s spike immediately after a deploy or a traffic surge, the X-Forwarded-For header looks fine, but the throttled IP in your logs is always your proxy. The fix is to tell Laravel which proxies to trust. In Laravel 11+, add this to your bootstrap:&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withMiddleware&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;Middleware&lt;/span&gt; &lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;trustProxies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&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;In production, never use at: ['*'] blindly — list your actual proxy addresses or CIDR ranges so clients cannot spoof X-Forwarded-For. Trusting the right proxies restores the real client IP, and your per-IP limits start working the way you intended.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cause 4 — File Cache Across Multiple Servers
&lt;/h3&gt;

&lt;p&gt;Laravel's throttle middleware stores its counters in the cache. If CACHE_DRIVER is set to file and you run two or more app servers, each server counts independently. Your effective limit is the configured limit multiplied by the number of servers, and worse, counters are never shared, so behavior looks random: a request fails on server A and succeeds on server B.&lt;/p&gt;

&lt;p&gt;The fix: use Redis (or Memcached) as the cache driver in any environment with more than one server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CACHE_DRIVER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Redis the counters are atomic, fast, and shared across all servers. This one config change turns your rate limit from an estimate into an actual guarantee.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cause 5 — The throttleApi Error on Every Request
&lt;/h3&gt;

&lt;p&gt;In Laravel 11+, if you enable the api throttle with a named limiter that you never defined — for example, calling $middleware-&amp;gt;throttleApi() and then attaching throttle:api to routes without a RateLimiter::for('api') definition — every API request 429s or errors from the first call. The symptom is unmistakable: a brand-new user makes one request and gets 429.&lt;/p&gt;

&lt;p&gt;The fix is to always define the limiter you reference. If you do not need per-user nuance, the simplest correct definition is:&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;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No reference to a limiter anywhere means no mystery errors. Run php artisan route:list -v after wiring everything up and read the middleware column line by line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cause 6 — Login and Auth Routes Keyed Wrong
&lt;/h3&gt;

&lt;p&gt;Auth endpoints deserve their own strict limit, keyed on more than just the email. A common setup limits logins per email, but then one attacker brute-forcing a single account burns through legitimate users' budgets, or worse, the lockout becomes a tool for denying real users access.&lt;/p&gt;

&lt;p&gt;A robust login limiter keys on the email plus the IP, and the whole flow clears the counter on success so a legitimate user starts fresh:&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;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'login:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'|'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&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="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;tooManyAttempts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&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;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Too many attempts. Please try again later.'&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="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;availableIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;);&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="o"&gt;!&lt;/span&gt; &lt;span class="nc"&gt;Auth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&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;response&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;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Invalid credentials.'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;session&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;regenerate&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;response&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;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Authenticated.'&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;Four methods do all the work here: RateLimiter::hit records an attempt, tooManyAttempts checks the count, clear resets it after a successful login, and availableIn returns the seconds until the limit resets so you can feed a honest Retry-After to the client. Never reveal how many attempts were made or whether the account exists — a generic message plus retry timing keeps brute-force attackers from enumerating users. Apply the same discipline to password resets, registration, OTP resends, and email verification: these are the endpoints bots actually attack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling 429 Gracefully Instead of Swallowing It
&lt;/h2&gt;

&lt;p&gt;Whatever you do, keep the Retry-After header in your 429 responses. If you customize the exception rendering, pass the headers through:&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\Http\Exceptions\ThrottleRequestsException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;public function render($request, Throwable $exception)&lt;br&gt;
{&lt;br&gt;
    if ($exception instanceof ThrottleRequestsException) {&lt;br&gt;
        Log::warning('Rate limit exceeded', [&lt;br&gt;
            'ip' =&amp;gt; $request-&amp;gt;ip(),&lt;br&gt;
            'url' =&amp;gt; $request-&amp;gt;url(),&lt;br&gt;
        ]);&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    return response()-&amp;gt;json([
        'message' =&amp;gt; 'Too many requests. Please slow down and try again.',
    ], 429, $exception-&amp;gt;getHeaders());
}

return parent::render($request, $exception);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Logging every 429 with the IP and URL gives you the evidence trail you need. When a legitimate integration starts failing, you will see the pattern in minutes instead of guessing. And on the client side, respect the header: exponential backoff on 429 and 5xx responses is the difference between a retry that works and a retry storm.&lt;/p&gt;
&lt;h2&gt;
  
  
  Testing Your Limits Before You Ship
&lt;/h2&gt;

&lt;p&gt;Do not trust a limiter you have never triggered. The cheapest verification is a curl loop from two machines against your real environment — your local setup cannot reproduce proxy IPs, shared caches, or real client behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 70&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code} %{header_json} &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    https://webixyhub.com/api/search?q&lt;span class="o"&gt;=&lt;/span&gt;demo
&lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You are looking for the first 60 responses to be 200 and the 61st onward to be 429 with a Retry-After header present. If 429s arrive early, your key is wrong (check trusted proxies). If they never arrive, your limiter is not attached or your cache driver is array in a real environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 429 Checklist
&lt;/h2&gt;

&lt;p&gt;One named limiter per behavior, never shared across unrelated endpoints. Limits keyed on user ID for authenticated traffic, IP only for guests. Trusted proxies configured with real addresses before launch, not after the first incident. Redis or another shared cache driver in production. Auth endpoints limited on email plus IP, with the counter cleared on successful login. Retry-After preserved in every custom 429 response. And a curl test against the real environment before you call it done.&lt;/p&gt;

&lt;p&gt;Rate limiting is one of those invisible engineering decisions that separates a side project from production software — like fixing the Stripe webhook signature verification flow (&lt;a href="https://webixyhub.com/blog/fix-stripe-webhook-signature-verification-failed-laravel" rel="noopener noreferrer"&gt;https://webixyhub.com/blog/fix-stripe-webhook-signature-verification-failed-laravel&lt;/a&gt;) or the N+1 query problem in Laravel (&lt;a href="https://webixyhub.com/blog/fix-n1-query-problem-laravel-eager-loading" rel="noopener noreferrer"&gt;https://webixyhub.com/blog/fix-n1-query-problem-laravel-eager-loading&lt;/a&gt;), it only matters when it breaks, and then it matters a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Need a Developer Who Gets This Right the First Time?
&lt;/h2&gt;

&lt;p&gt;I am Ayesha Khan, a full-stack web developer building Laravel applications, REST APIs, and integrations for clients worldwide. If rate limiting, API design, or a tricky Laravel bug is eating your week, I can take it off your plate. Reach out through &lt;a href="https://webixyhub.com" rel="noopener noreferrer"&gt;https://webixyhub.com&lt;/a&gt; and tell me what is breaking — or see how I evaluate Laravel expertise when teams ask me to vet one (&lt;a href="https://webixyhub.com/blog/hire-laravel-developer-guide" rel="noopener noreferrer"&gt;https://webixyhub.com/blog/hire-laravel-developer-guide&lt;/a&gt;).&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Fix PHP "Allowed Memory Size Exhausted" Errors in Laravel and WordPress</title>
      <dc:creator>Ayesha</dc:creator>
      <pubDate>Sat, 31 Oct 2026 05:15:13 +0000</pubDate>
      <link>https://dev.to/ayesha_921/how-to-fix-php-allowed-memory-size-exhausted-errors-in-laravel-and-wordpress-4k0l</link>
      <guid>https://dev.to/ayesha_921/how-to-fix-php-allowed-memory-size-exhausted-errors-in-laravel-and-wordpress-4k0l</guid>
      <description>&lt;p&gt;If you ship PHP for a living, you will meet this fatal error sooner or later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;PHP Fatal error: Allowed memory size of 134217728 bytes exhausted &lt;span class="o"&gt;(&lt;/span&gt;tried to allocate 4096 bytes&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; /var/www/app/Services/ReportBuilder.php on line 88
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Laravel, the "allowed memory size exhausted" error usually hides inside &lt;code&gt;storage/logs/laravel.log&lt;/code&gt; while your users just see a 500 page. In WordPress it is a white screen of death, or a 500 on admin-ajax during a backup, import, or page-builder save. In Composer it kills dependency installs mid-resolution. The error means one PHP process asked for more RAM than its &lt;code&gt;memory_limit&lt;/code&gt; allows — and the fix is almost never just "raise the number and hope". Here is the method I use on client projects to kill the "allowed memory size exhausted" error for good: find the real limit, raise it in the right place for your runtime, then fix the code that blows through it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Allowed Memory Size Exhausted" Actually Means
&lt;/h2&gt;

&lt;p&gt;PHP gives every single process its own memory ceiling, set by the &lt;code&gt;memory_limit&lt;/code&gt; directive. The default is 128M. When a script's allocations cross that line, PHP kills it immediately with a fatal error — there is no catching it, no retry, no graceful fallback. The "tried to allocate X bytes" part tells you how much the script wanted at the moment of death, and the file/line points at the allocation that pushed it over — which is often not the real culprit, just the last straw.&lt;/p&gt;

&lt;p&gt;Two important truths shape every fix below.&lt;/p&gt;

&lt;p&gt;First, PHP has several different runtimes on one machine, and each reads its own config. Your web app (PHP-FPM) and your command line (CLI) often use completely separate &lt;code&gt;php.ini&lt;/code&gt; files. Raising the limit for the web SAPI does nothing for a Composer install or an Artisan command, and vice versa. Most "I raised it but it still fails" stories come from editing the wrong file.&lt;/p&gt;

&lt;p&gt;Second, raising the limit treats the symptom. Legitimate workloads — big imports, report generation, image pipelines — sometimes do need more headroom. But an unbounded query, an infinite recursion, or a queue worker leaking a little on every job will eat any limit you give it. Give a leak 1GB and it dies at 1GB instead of 128MB, at 3 a.m. instead of 2 p.m. The durable fix is almost always in the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Find the Limit You Are Actually Hitting
&lt;/h2&gt;

&lt;p&gt;Before changing anything, confirm the current limit for the runtime that failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php &lt;span class="nt"&gt;-i&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"memory_limit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a web request, drop this into a scratch route or script (never leave it on production):&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="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'memory_limit = '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;ini_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'memory_limit'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'peak so far = '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;memory_get_peak_usage&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;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' MB'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;php -i&lt;/code&gt; says 512M but your Laravel queue job still dies at 128M, you are probably reading the CLI config while the worker runs under supervisor with a different &lt;code&gt;php.ini&lt;/code&gt;, or the web config while the failure is on CLI. Run &lt;code&gt;php --ini&lt;/code&gt; to see exactly which files the CLI loads, and use &lt;code&gt;phpinfo()&lt;/code&gt; or a route for the web side. Also check for overrides: &lt;code&gt;ini_set()&lt;/code&gt; calls in bootstrap files, &lt;code&gt;.htaccess&lt;/code&gt; &lt;code&gt;php_value&lt;/code&gt; lines, and host panels (cPanel MultiPHP INI Editor, Plesk, RunCloud) can all beat &lt;code&gt;php.ini&lt;/code&gt; in different ways.&lt;/p&gt;

&lt;p&gt;In Laravel, start in &lt;code&gt;storage/logs/laravel.log&lt;/code&gt; — the same log you would check for database connection failures (my walkthrough of the SQLSTATE[HY000] [2002] "Connection refused" error lives at &lt;a href="https://webixyhub.com/blog/fix-sqlstate-hy000-2002-connection-refused-laravel" rel="noopener noreferrer"&gt;https://webixyhub.com/blog/fix-sqlstate-hy000-2002-connection-refused-laravel&lt;/a&gt;). Confirm the exact byte count and the file/line before touching any config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Raise the Limit in the Right Place
&lt;/h2&gt;

&lt;p&gt;Once you know which runtime failed, here is how to increase the PHP memory limit in exactly that place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;php.ini (server-wide):&lt;/strong&gt; &lt;code&gt;memory_limit = 512M&lt;/code&gt;, then restart PHP-FPM or Apache. The cleanest fix when you own the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PHP-FPM pool config:&lt;/strong&gt; &lt;code&gt;php_admin_value[memory_limit] = 512M&lt;/code&gt; — per-site, survives &lt;code&gt;php.ini&lt;/code&gt; resets on managed hosts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apache .htaccess:&lt;/strong&gt; &lt;code&gt;php_value memory_limit 512M&lt;/code&gt; — works only on mod_php, not FPM. If you get a 500 after adding it, your host blocks the directive; remove the line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single script:&lt;/strong&gt; &lt;code&gt;ini_set('memory_limit', '512M');&lt;/code&gt; at the top of a CLI script or Artisan command — the safest, affects nothing else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single command:&lt;/strong&gt; &lt;code&gt;php -d memory_limit=512M artisan reports:generate&lt;/code&gt; — no permanent change at all.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="p"&gt;;&lt;/span&gt; php.ini — pick a sane production value, not &lt;span class="nt"&gt;-1&lt;/span&gt;
memory_limit &lt;span class="o"&gt;=&lt;/span&gt; 512M
&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;# reload the right service afterwards&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart php8.3-fpm
&lt;span class="c"&gt;# or Apache:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A note on &lt;code&gt;-1&lt;/code&gt; (unlimited): it is fine for a Composer install or a one-off migration on your own machine. In production it is a loaded gun — one bad query and PHP eats all server RAM until the OOM killer shoots the process (or worse, takes MySQL down with it). If your first instinct is &lt;code&gt;-1&lt;/code&gt; in production, keep reading: you need the next section more than a bigger number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: WordPress — Use WP_MEMORY_LIMIT Correctly
&lt;/h2&gt;

&lt;p&gt;WordPress has its own layer: &lt;code&gt;WP_MEMORY_LIMIT&lt;/code&gt; in &lt;code&gt;wp-config.php&lt;/code&gt;. This raises the limit for front-end requests only; admin pages use &lt;code&gt;WP_MAX_MEMORY_LIMIT&lt;/code&gt; (default 256M), which needs its own define if you want higher:&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="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// wp-config.php — above "/* That's all, stop editing! */"&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WP_MEMORY_LIMIT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'256M'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WP_MAX_MEMORY_LIMIT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'512M'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical detail most guides skip: &lt;code&gt;WP_MEMORY_LIMIT&lt;/code&gt; can never exceed the server's &lt;code&gt;memory_limit&lt;/code&gt;. If your host sets PHP's &lt;code&gt;memory_limit&lt;/code&gt; to 128M, defining 512M in &lt;code&gt;wp-config.php&lt;/code&gt; does absolutely nothing. Check both, and on shared hosting you usually have to raise PHP's limit first via the host panel or &lt;code&gt;.user.ini&lt;/code&gt; — or accept 256M and fix the plugin instead.&lt;/p&gt;

&lt;p&gt;The usual WordPress offenders: backup plugins on big media libraries, page builders saving huge layouts, WooCommerce with tens of thousands of products, and bulk importers. Deactivate suspects one at a time and re-run the failing action; the Query Monitor plugin will show you exactly which plugin's query or hook burns the memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Composer and Artisan Have Their Own Memory Ceiling
&lt;/h2&gt;

&lt;p&gt;The 1610612736-bytes error during &lt;code&gt;composer install&lt;/code&gt; is so common it deserves its own fix. Composer runs as a CLI process with its own memory handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# one-off: unlimited memory for this install only&lt;/span&gt;
&lt;span class="nv"&gt;COMPOSER_MEMORY_LIMIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt; composer &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# or inline per-command for artisan&lt;/span&gt;
php &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;memory_limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1024M artisan migrate &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not set &lt;code&gt;memory_limit = -1&lt;/code&gt; globally in the CLI &lt;code&gt;php.ini&lt;/code&gt; just to make Composer happy — every future cron and queue worker inherits it. The per-command flags are the professional move: they fix the install and leave production boundaries intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Fix the Code That Actually Leaks
&lt;/h2&gt;

&lt;p&gt;This is the section that permanently solves the "allowed memory size exhausted" problem. Three patterns cover most Laravel memory exhaustion I see on client projects.&lt;/p&gt;

&lt;p&gt;Pattern 1: unbounded queries. &lt;code&gt;Model::all()&lt;/code&gt; or &lt;code&gt;-&amp;gt;get()&lt;/code&gt; on a big table hydrates every row into an object. Ten thousand users is a slow page; a million rows is a fatal error. Use &lt;code&gt;chunk()&lt;/code&gt; for batch updates and &lt;code&gt;cursor()&lt;/code&gt; for streaming exports:&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="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="c1"&gt;// BAD: loads every user into memory at once&lt;/span&gt;
&lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&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;$users&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notify&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;MonthlyReport&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// GOOD: constant memory no matter how big the table gets&lt;/span&gt;
&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&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;$users&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;$users&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notify&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;MonthlyReport&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="c1"&gt;// For exports and reports: cursor() streams one row at a time&lt;/span&gt;
&lt;span class="nv"&gt;$handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;fopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'php://output'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'w'&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="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;fputcsv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;fclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;chunk()&lt;/code&gt; supports eager loading per batch (see my walkthrough of fixing the N+1 query problem in Laravel at &lt;a href="https://webixyhub.com/blog/fix-n1-query-problem-laravel-eager-loading" rel="noopener noreferrer"&gt;https://webixyhub.com/blog/fix-n1-query-problem-laravel-eager-loading&lt;/a&gt;); &lt;code&gt;cursor()&lt;/code&gt; is leaner but cannot eager-load. Pick &lt;code&gt;chunk()&lt;/code&gt; for updates with relations, &lt;code&gt;cursor()&lt;/code&gt; for plain row streaming.&lt;/p&gt;

&lt;p&gt;Pattern 2: fetching more columns than you need. Select only what you use:&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="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="c1"&gt;// Fetches every column, including longtext blobs you never read&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&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;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'published'&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="c1"&gt;// Fetches 3 columns — often 10x less memory&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&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;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'published'&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;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'slug'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pattern 3: debug and cache data that grows. Laravel Debugbar, &lt;code&gt;DB::enableQueryLog()&lt;/code&gt; left on, and log channels that buffer stack traces all accumulate per request. In long Artisan commands, call &lt;code&gt;DB::disableQueryLog()&lt;/code&gt;, &lt;code&gt;unset()&lt;/code&gt; large temp variables, and avoid holding collections you already wrote out. And if you store big structures in cache or session, remember that unserializing them costs the same memory all over again on every read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Queue Workers and Image Processing Are the Silent Killers
&lt;/h2&gt;

&lt;p&gt;Two production patterns trigger "allowed memory size exhausted" without any obvious bug.&lt;/p&gt;

&lt;p&gt;Long-running queue workers accumulate memory across hundreds of jobs — Eloquent models, logs, and container bindings pile up. This is the same class of problem I cover when webhooks misbehave (my guide to fixing Stripe webhook signature verification failures in Laravel: &lt;a href="https://webixyhub.com/blog/fix-stripe-webhook-signature-verification-failed-laravel" rel="noopener noreferrer"&gt;https://webixyhub.com/blog/fix-stripe-webhook-signature-verification-failed-laravel&lt;/a&gt;). Cap workers so they restart before leaks accumulate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# restart the worker before leaks accumulate&lt;/span&gt;
php artisan queue:work &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;256 &lt;span class="nt"&gt;--max-jobs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;500 &lt;span class="nt"&gt;--max-time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3600
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Image processing is the other classic: GD loads the full decompressed bitmap into memory, so a 10MB JPEG can need 200MB+ of RAM. Process uploads on the queue — never in the web request — cap worker memory, and for heavy pipelines prefer the libvips driver (Intervention Image supports it), which streams instead of loading whole images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls and Their Fixes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;"I raised memory_limit but nothing changed." You edited the wrong &lt;code&gt;php.ini&lt;/code&gt; (CLI vs FPM), forgot to restart the service, or a &lt;code&gt;.user.ini&lt;/code&gt;, pool config, or &lt;code&gt;ini_set()&lt;/code&gt; is overriding you. Verify with the Step 1 snippet after the change.&lt;/li&gt;
&lt;li&gt;Limit written as &lt;code&gt;512MB&lt;/code&gt;. PHP reads the trailing B as bytes — your limit becomes 2MB, not 512MB. Use &lt;code&gt;512M&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;White screen in WordPress with no error. Error display is off and the log is elsewhere. Enable &lt;code&gt;WP_DEBUG&lt;/code&gt; and &lt;code&gt;WP_DEBUG_LOG&lt;/code&gt; in &lt;code&gt;wp-config.php&lt;/code&gt; and read &lt;code&gt;wp-content/debug.log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Error only during imports or cron. Those run on CLI or a different worker with a different limit — check that runtime specifically, and consider chunking the import.&lt;/li&gt;
&lt;li&gt;Error right after a plugin or theme update. Something new is allocating. Roll back or deactivate to confirm, then profile before raising limits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Quick Checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Reproduce and note which runtime failed (web, CLI, cron, worker).&lt;/li&gt;
&lt;li&gt;Read the real limit for that runtime — &lt;code&gt;php -i&lt;/code&gt;, &lt;code&gt;php --ini&lt;/code&gt;, or &lt;code&gt;ini_get()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Raise it in the right place, for that runtime only.&lt;/li&gt;
&lt;li&gt;Find the allocation pattern: unbounded &lt;code&gt;get()&lt;/code&gt;, missing chunk/cursor, image processing in the request, leaking worker.&lt;/li&gt;
&lt;li&gt;Fix the pattern; keep the raised limit as headroom, not as the fix.&lt;/li&gt;
&lt;li&gt;Monitor peak usage (&lt;code&gt;memory_get_peak_usage(true)&lt;/code&gt; in logs or your APM) so the next regression shows up before users do.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;If a memory problem — or anything else in your stack — keeps eating your week, I debug and harden Laravel and WordPress applications for clients worldwide. Tell me what is breaking at &lt;a href="https://webixyhub.com" rel="noopener noreferrer"&gt;https://webixyhub.com&lt;/a&gt; and I will take a look.&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>wordpress</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Fix the N+1 Query Problem in Laravel (Eager Loading Explained)</title>
      <dc:creator>Ayesha</dc:creator>
      <pubDate>Mon, 19 Oct 2026 05:13:22 +0000</pubDate>
      <link>https://dev.to/ayesha_921/how-to-fix-the-n1-query-problem-in-laravel-eager-loading-explained-2cjp</link>
      <guid>https://dev.to/ayesha_921/how-to-fix-the-n1-query-problem-in-laravel-eager-loading-explained-2cjp</guid>
      <description>&lt;p&gt;If your Laravel application feels slow and you cannot figure out why, the &lt;strong&gt;N+1 query problem&lt;/strong&gt; is one of the most common culprits. It silently multiplies your database queries, turning a page that should run 2 queries into one that runs 101. In this guide, you will learn exactly what the N+1 problem is, how to spot it, and how to fix it with &lt;strong&gt;eager loading&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is the N+1 Query Problem?
&lt;/h2&gt;

&lt;p&gt;The N+1 query problem happens when your code runs &lt;strong&gt;one query to fetch a list of records&lt;/strong&gt;, then runs &lt;strong&gt;one additional query for each record&lt;/strong&gt; to fetch a related record. If you have N records, you end up running N+1 queries total.&lt;/p&gt;

&lt;p&gt;Here is the classic example. Imagine you want to display a list of users along with their posts:&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="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&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;$users&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&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 looks innocent, but here is what actually hits the database:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One query to fetch all users: &lt;code&gt;select * from users&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;One query &lt;strong&gt;per user&lt;/strong&gt; to fetch their posts: &lt;code&gt;select * from posts where user_id = ?&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you have 100 users, Laravel runs &lt;strong&gt;101 queries&lt;/strong&gt;. That is the N+1 problem — 1 query plus N additional queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the N+1 Problem Is So Dangerous
&lt;/h2&gt;

&lt;p&gt;The N+1 problem is dangerous because &lt;strong&gt;it works perfectly in development&lt;/strong&gt; and only hurts you in production. With 5 test users, you will never notice 6 queries. With 10,000 real users, your page runs 10,001 queries and times out.&lt;/p&gt;

&lt;p&gt;Three reasons it catches developers off guard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eloquent makes relationships feel free.&lt;/strong&gt; Accessing &lt;code&gt;$user-&amp;gt;posts&lt;/code&gt; looks like reading a property, not running a database query. There is no visual signal that a query just happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It scales with your data.&lt;/strong&gt; The more successful your application becomes, the worse the problem gets. Growth literally makes it slower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It hides inside loops.&lt;/strong&gt; The extra queries are scattered across loop iterations, so they do not show up as one obvious slow query in your logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Detect the N+1 Problem
&lt;/h2&gt;

&lt;p&gt;Before fixing anything, confirm you actually have the problem. Laravel gives you several ways to see your queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 1: Laravel Telescope
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Laravel Telescope&lt;/strong&gt; is the fastest way to spot N+1 queries. Install it in your development environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require laravel/telescope &lt;span class="nt"&gt;--dev&lt;/span&gt;
php artisan telescope:install
php artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open Telescope in your browser and click the &lt;strong&gt;Queries&lt;/strong&gt; tab. If you see dozens of nearly identical queries like &lt;code&gt;select * from posts where user_id = ?&lt;/code&gt; with different IDs, you have an N+1 problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: Query Log
&lt;/h3&gt;

&lt;p&gt;You can log all queries for a single request without installing anything:&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="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;enableQueryLog&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&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;$users&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;dd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getQueryLog&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Count the queries in the output. If the number grows with the number of users, you have confirmed the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 3: Laravel Debugbar
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Laravel Debugbar&lt;/strong&gt; package shows a query counter right in your browser toolbar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require barryvdh/laravel-debugbar &lt;span class="nt"&gt;--dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load the slow page and check the query count. Anything above 20–30 queries for a simple listing page deserves investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Eager Loading with &lt;code&gt;with()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The solution is &lt;strong&gt;eager loading&lt;/strong&gt; — telling Laravel to fetch the related records up front in a single query, instead of lazily loading them one by one inside the loop.&lt;/p&gt;

&lt;p&gt;Add the &lt;code&gt;with()&lt;/code&gt; method to your query:&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="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts'&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="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&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;Now Laravel runs exactly &lt;strong&gt;2 queries&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;select * from users&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;select * from posts where user_id in (1, 2, 3, ...)&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second query uses a &lt;code&gt;WHERE IN&lt;/code&gt; clause to fetch all related posts at once. Eloquent then matches them to the correct users in PHP memory. No matter how many users you have, it stays at 2 queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eager Loading Nested Relationships
&lt;/h2&gt;

&lt;p&gt;Real applications have deeper relationship chains. Suppose each post has comments, and you want to display users, their posts, and each post's comments.&lt;/p&gt;

&lt;p&gt;Without eager loading, this is an N+1 inside an N+1 — potentially thousands of queries. Fix it with &lt;strong&gt;nested eager loading&lt;/strong&gt; using dot notation:&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="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts.comments'&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="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$user&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;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&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 runs just &lt;strong&gt;3 queries&lt;/strong&gt;: one for users, one for posts, one for comments. The dot notation tells Laravel to eager load the entire chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constraining Eager Loads
&lt;/h2&gt;

&lt;p&gt;Sometimes you do not want every related record — only a filtered subset. You can pass a closure to &lt;code&gt;with()&lt;/code&gt; to constrain the eager load:&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="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'posts'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$query&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;'published'&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;latest&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;limit&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fetches only the 5 latest published posts per user. The constraint applies at the database level, so you are not pulling unnecessary data into memory.&lt;/p&gt;

&lt;p&gt;You can also select specific columns to reduce memory usage:&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="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts:id,user_id,title'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; when selecting specific columns, always include the foreign key (&lt;code&gt;user_id&lt;/code&gt; in this case). Without it, Eloquent cannot match the posts back to their users, and the relationship will come back empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eager Loading After the Fact with &lt;code&gt;load()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Sometimes you already have a model instance and realize you need its relationships. Use &lt;code&gt;load()&lt;/code&gt; to eager load on an existing model or collection:&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="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;find&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="c1"&gt;// Later, when you need the posts:&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For collections, &lt;code&gt;load()&lt;/code&gt; works the same way:&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="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$users&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful when the decision to load a relationship happens conditionally, after the initial query has already run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting Related Models Without Loading Them
&lt;/h2&gt;

&lt;p&gt;A common variation: you only need the &lt;strong&gt;count&lt;/strong&gt; of related records, not the records themselves. Loading full models just to count them wastes memory. Use &lt;code&gt;withCount()&lt;/code&gt; instead:&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="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;withCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts'&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="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts_count&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 adds a &lt;code&gt;posts_count&lt;/code&gt; attribute to each user with a single efficient &lt;code&gt;COUNT&lt;/code&gt; query. No post models are loaded into memory at all.&lt;/p&gt;

&lt;p&gt;You can combine &lt;code&gt;withCount()&lt;/code&gt; with constraints:&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="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;withCount&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'posts'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$query&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;'published'&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Pitfalls to Avoid
&lt;/h2&gt;

&lt;p&gt;Even developers who know about eager loading make these mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eager loading in the wrong place.&lt;/strong&gt; Adding &lt;code&gt;with()&lt;/code&gt; to a query that is never used with the relationship wastes a query. Only eager load relationships you actually access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting nested relationships.&lt;/strong&gt; &lt;code&gt;User::with('posts')&lt;/code&gt; does not eager load &lt;code&gt;posts.comments&lt;/code&gt;. If your view accesses comments, add the nested load explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;with()&lt;/code&gt; on already-loaded models.&lt;/strong&gt; &lt;code&gt;with()&lt;/code&gt; only works on the query builder. If you already called &lt;code&gt;get()&lt;/code&gt;, use &lt;code&gt;load()&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selecting columns without the foreign key.&lt;/strong&gt; As mentioned above, &lt;code&gt;User::with('posts:title')&lt;/code&gt; without &lt;code&gt;user_id&lt;/code&gt; silently returns empty relationships. Always include the key Eloquent needs to match records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-eager loading.&lt;/strong&gt; Loading 10 relationships "just in case" can be slower than a few lazy queries. Profile with Telescope and eager load only what the page actually renders.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Prevent N+1 Queries in the Future
&lt;/h2&gt;

&lt;p&gt;Fixing N+1 queries one by one is reactive. Here is how to catch them before they reach production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add query count assertions to your feature tests.&lt;/strong&gt; Laravel lets you assert the number of queries a request runs. If a code change introduces an N+1, the test fails immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review Telescope during development.&lt;/strong&gt; Make it a habit to glance at the query tab when building listing pages. Spikes in query count are easy to spot early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the &lt;code&gt;preventLazyLoading()&lt;/code&gt; method in development.&lt;/strong&gt; Laravel can throw an exception whenever code triggers a lazy load, forcing you to add explicit eager loads:
&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="c1"&gt;// In AppServiceProvider::boot(), development only:&lt;/span&gt;
&lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;preventLazyLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;app&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;isProduction&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this enabled, any accidental lazy load crashes loudly in development instead of silently slowing down production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;N+1 query problem&lt;/strong&gt; is one of the highest-impact performance fixes you can make in a Laravel application. A single &lt;code&gt;with()&lt;/code&gt; call can take a page from 1,001 queries down to 2. The key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accessing relationships inside loops without eager loading causes the N+1 problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eager loading&lt;/strong&gt; with &lt;code&gt;with()&lt;/code&gt; fetches related records in bulk using &lt;code&gt;WHERE IN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;dot notation&lt;/strong&gt; for nested relationships (&lt;code&gt;posts.comments&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;withCount()&lt;/code&gt; when you only need counts, not full models&lt;/li&gt;
&lt;li&gt;Detect problems early with &lt;strong&gt;Telescope&lt;/strong&gt;, &lt;strong&gt;Debugbar&lt;/strong&gt;, or &lt;code&gt;preventLazyLoading()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your Laravel application is slower than it should be, check your query count first. The fix is often a single method call away.&lt;/p&gt;

&lt;p&gt;Need help optimizing a slow Laravel application? &lt;a href="https://webixyhub.com" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt; — I specialize in Laravel performance, API development, and fixing the bottlenecks that slow real applications down.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
    <item>
      <title>How to Fix "Stripe Webhook Signature Verification Failed" in Laravel</title>
      <dc:creator>Ayesha</dc:creator>
      <pubDate>Sat, 17 Oct 2026 05:14:15 +0000</pubDate>
      <link>https://dev.to/ayesha_921/how-to-fix-stripe-webhook-signature-verification-failed-in-laravel-4oa0</link>
      <guid>https://dev.to/ayesha_921/how-to-fix-stripe-webhook-signature-verification-failed-in-laravel-4oa0</guid>
      <description>&lt;p&gt;If your Laravel application suddenly throws &lt;strong&gt;"Stripe webhook signature verification failed"&lt;/strong&gt; — the Stripe PHP SDK's &lt;code&gt;SignatureVerificationException&lt;/code&gt;, usually worded as &lt;em&gt;"No signatures found matching the expected signature for payload"&lt;/em&gt; — every payment event your app depends on is silently dying. &lt;code&gt;checkout.session.completed&lt;/code&gt; never fires, subscriptions never activate, and customers pay for something your system never delivers. I have debugged this exact failure in production Stripe integrations more than once, and the cause is almost never what developers suspect first. This guide walks through the five real causes, in order of likelihood, with the exact fix for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "signature verification failed" actually means
&lt;/h2&gt;

&lt;p&gt;Every webhook Stripe sends is signed with HMAC-SHA256 using your endpoint's signing secret (the &lt;code&gt;whsec_...&lt;/code&gt; value). The signature arrives in the &lt;code&gt;Stripe-Signature&lt;/code&gt; header as a timestamp plus one or more &lt;code&gt;v1=&lt;/code&gt; hashes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t=1728991234,v1=5257a869e7ecebeda32affa62cdca3eeba9f567d...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you call &lt;code&gt;Stripe\Webhook::constructEvent()&lt;/code&gt;, the SDK recomputes the HMAC over the payload and compares it to the header using a timing-safe comparison. If the bytes differ by even a single character — one extra space, one reordered key — verification fails. The SDK also enforces a timestamp tolerance (default 300 seconds) so old events cannot be replayed.&lt;/p&gt;

&lt;p&gt;Understanding this is the key to everything below: &lt;strong&gt;Stripe signs bytes, not data.&lt;/strong&gt; Your job is to hand the SDK the exact bytes Stripe sent, nothing more, nothing less.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix #1: Verify the raw request body, never a re-encoded one
&lt;/h2&gt;

&lt;p&gt;This is the single most common cause I see. The controller decodes the JSON into an array and then re-encodes it before verifying:&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;// BROKEN — do not do this&lt;/span&gt;
&lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// or $request-&amp;gt;input()&lt;/span&gt;
&lt;span class="nv"&gt;$event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;\Stripe\Webhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Stripe-Signature'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.stripe.webhook_secret'&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;A JSON round-trip through PHP's encoder is not guaranteed to reproduce Stripe's original bytes. Whitespace, key order, and number formatting can all change — and any change breaks the HMAC. The fix is one line:&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;// CORRECT — verify the exact bytes Stripe sent&lt;/span&gt;
&lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// raw, unmanipulated body&lt;/span&gt;
&lt;span class="nv"&gt;$sigHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Stripe-Signature'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.stripe.webhook_secret'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;\Stripe\Webhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$sigHeader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify &lt;strong&gt;before&lt;/strong&gt; you decode anything, and decode only after verification succeeds. The safe sequence is: raw body → signature verification → JSON decoding → event processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it works: HMAC signs bytes, not data
&lt;/h2&gt;

&lt;p&gt;HMAC-SHA256 takes a key and a message and produces a fixed-size tag. The "message" here is the literal HTTP body. Stripe computes the tag on its side; the SDK recomputes it on yours. If your framework decoded the JSON into a PHP array and re-encoded it, you are computing the tag over a &lt;em&gt;different message&lt;/em&gt; — similar data, different bytes. Two JSON strings can represent identical data while being different byte sequences (&lt;code&gt;{"a":1}&lt;/code&gt; vs &lt;code&gt;{ "a": 1 }&lt;/code&gt;), and HMAC will produce completely different tags for each. That is why "but the data is the same!" is never a valid defense against this error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix #2: Keep Laravel middleware away from the webhook body
&lt;/h2&gt;

&lt;p&gt;Even with &lt;code&gt;$request-&amp;gt;getContent()&lt;/code&gt;, anything upstream that mutates the request can change what the body looks like by the time your controller runs. Two classic traps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trap A: route placed in &lt;code&gt;routes/web.php&lt;/code&gt;.&lt;/strong&gt; The web group applies CSRF protection, and Stripe does not send your CSRF token — so Laravel returns &lt;strong&gt;419 Page Expired&lt;/strong&gt; before your controller ever executes. Your webhook "fails" and you never even reach signature verification. Put the route in &lt;code&gt;routes/api.php&lt;/code&gt;, where CSRF does not apply:&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/api.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Controllers\StripeWebhookController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/webhooks/stripe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;StripeWebhookController&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="s1"&gt;'handle'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the route must live behind the web group, exempt it explicitly. In Laravel 11+, configure this in &lt;code&gt;bootstrap/app.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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withMiddleware&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;Middleware&lt;/span&gt; &lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validateCsrfTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;except&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'webhooks/stripe'&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;&lt;strong&gt;Trap B: global middleware that alters the body.&lt;/strong&gt; &lt;code&gt;TrimStrings&lt;/code&gt; and &lt;code&gt;ConvertEmptyStringsToNull&lt;/code&gt; mutate string values in the request. If any custom middleware transforms the payload before your controller reads the raw content, the bytes no longer match Stripe's signature. Keep the webhook route clear of body-altering middleware — check what applies with &lt;code&gt;php artisan route:list -v&lt;/code&gt; and look at the middleware column for your webhook route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix #3: Use the right webhook secret (the Stripe CLI trap)
&lt;/h2&gt;

&lt;p&gt;This one burns developers during local testing. When you run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe listen &lt;span class="nt"&gt;--forward-to&lt;/span&gt; localhost:8000/api/webhooks/stripe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the CLI prints an &lt;strong&gt;ephemeral&lt;/strong&gt; signing secret, something like &lt;code&gt;whsec_3a8f...&lt;/code&gt;. That secret is different from the one shown in your Stripe Dashboard for the production endpoint. If your &lt;code&gt;.env&lt;/code&gt; holds the dashboard secret while you test through the CLI, every local event fails signature verification — and everything looks correct because the secret &lt;em&gt;is&lt;/em&gt; correct, just for the wrong endpoint.&lt;/p&gt;

&lt;p&gt;Fixes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the &lt;code&gt;whsec_...&lt;/code&gt; from the &lt;code&gt;stripe listen&lt;/code&gt; output into your local &lt;code&gt;.env&lt;/code&gt; as &lt;code&gt;STRIPE_WEBHOOK_SECRET&lt;/code&gt; while testing.&lt;/li&gt;
&lt;li&gt;Remember that &lt;strong&gt;each endpoint has its own secret&lt;/strong&gt;. Rotating or re-creating the endpoint in the Dashboard invalidates the old secret.&lt;/li&gt;
&lt;li&gt;After changing the secret, clear the config cache — a stale cached secret is a maddening variant of this bug:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan config:clear
php artisan config:cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you cache config in production (and you should), a secret rotated in &lt;code&gt;.env&lt;/code&gt; but not re-cached will keep failing until the cache is rebuilt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix #4: Fail closed when the secret is missing
&lt;/h2&gt;

&lt;p&gt;This is not a verification failure — it is worse. If &lt;code&gt;STRIPE_WEBHOOK_SECRET&lt;/code&gt; is empty and your code skips verification as a "fallback," your endpoint accepts unsigned payloads. Anyone can POST a fake &lt;code&gt;invoice.paid&lt;/code&gt; event and grant themselves a subscription. The failure mode is invisible: everything works in staging, someone forgets the env var in production, and your billing endpoint is open to the internet.&lt;/p&gt;

&lt;p&gt;Make a missing secret loud and fatal:&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="nv"&gt;$secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.stripe.webhook_secret'&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="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$secret&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;'STRIPE_WEBHOOK_SECRET is not set — rejecting all webhook events.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Webhook secret not configured.'&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;Never do 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="c1"&gt;// NEVER — turns a config mistake into a security hole&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// skipping verification&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fix #5: Build the production-ready handler (verify, queue, respond fast)
&lt;/h2&gt;

&lt;p&gt;Stripe expects a &lt;code&gt;2xx&lt;/code&gt; response quickly. If your controller generates an invoice PDF or updates five tables before responding, Stripe assumes a timeout and retries the event — and retries of a slow handler can double-charge or double-provision. The correct pattern: verify, dispatch to a queue, respond &lt;code&gt;200&lt;/code&gt; immediately.&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="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers&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;App\Jobs\ProcessStripeWebhookJob&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\Http\Request&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;Stripe\Exception\SignatureVerificationException&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;Stripe\Webhook&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StripeWebhookController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&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;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.stripe.webhook_secret'&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="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$secret&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;'STRIPE_WEBHOOK_SECRET is not set — rejecting webhook.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Webhook secret not configured.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Webhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;              &lt;span class="c1"&gt;// raw bytes, untouched&lt;/span&gt;
                &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Stripe-Signature'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nv"&gt;$secret&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\UnexpectedValueException&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Payload is not valid JSON at all&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;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Invalid Stripe webhook payload.'&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;response&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;json&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="s1"&gt;'Invalid payload'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SignatureVerificationException&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Bytes do not match the signature&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Stripe webhook signature verification 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;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$e&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&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;json&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="s1"&gt;'Invalid signature'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Heavy lifting happens in the background; Stripe gets its 200 now&lt;/span&gt;
        &lt;span class="nc"&gt;ProcessStripeWebhookJob&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;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&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;response&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;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'received'&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="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 details worth noting. First, catch &lt;code&gt;UnexpectedValueException&lt;/code&gt; separately: it means the body was not even valid JSON (a proxy mangled it, or someone is probing the endpoint), which is a different problem from a bad signature. Second, make the job idempotent — check the event ID against a &lt;code&gt;processed_webhook_events&lt;/code&gt; table before acting, because Stripe will legitimately re-deliver events and your queue worker may process a job more than once.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to diagnose it in five minutes
&lt;/h2&gt;

&lt;p&gt;When the error appears, work through this checklist instead of guessing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce locally.&lt;/strong&gt; Run &lt;code&gt;stripe listen --forward-to&lt;/code&gt; with the CLI's own &lt;code&gt;whsec_...&lt;/code&gt; in your &lt;code&gt;.env&lt;/code&gt;, then &lt;code&gt;stripe trigger checkout.session.completed&lt;/code&gt;. If local works but production fails, the problem is the production secret or middleware, not your code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log what you actually received.&lt;/strong&gt; Temporarily log the &lt;code&gt;Stripe-Signature&lt;/code&gt; header and a hash of &lt;code&gt;$request-&amp;gt;getContent()&lt;/code&gt; before verification. Compare against the event in the Dashboard — if the body hash differs from what Stripe sent, something upstream mutated it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the route's middleware.&lt;/strong&gt; &lt;code&gt;php artisan route:list --path=webhooks&lt;/code&gt; shows every middleware on the route. Anything that touches the body does not belong there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirm which secret the endpoint uses.&lt;/strong&gt; In the Dashboard, open Developers → Webhooks → your endpoint → "Signing secret". That exact value (or the CLI's, when testing locally) must be the one in your environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule out replays.&lt;/strong&gt; Re-sending an old event from Postman or the Dashboard will fail the timestamp tolerance check even with a valid signature. Always test with fresh events.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common pitfalls that still catch experienced developers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decoding before verifying.&lt;/strong&gt; &lt;code&gt;$request-&amp;gt;json()-&amp;gt;all()&lt;/code&gt; followed by &lt;code&gt;json_encode()&lt;/code&gt; is the re-encoding bug from Fix #1 wearing a different hat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing with a stale config cache.&lt;/strong&gt; You fixed the secret in &lt;code&gt;.env&lt;/code&gt;, but &lt;code&gt;config:cache&lt;/code&gt; still serves the old one. Rebuild it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One secret for multiple endpoints.&lt;/strong&gt; Each endpoint gets its own &lt;code&gt;whsec_...&lt;/code&gt;. Copying the wrong one into the wrong environment fails silently until the first real event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming the 419 is a Stripe problem.&lt;/strong&gt; A 419 means Laravel rejected the request before your code ran — it is a routing/middleware issue, not a signature issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow handlers causing retries.&lt;/strong&gt; A controller that takes 25 seconds looks like a failure to Stripe, which retries — and the retry storm looks like duplicate charges. Queue everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Nine times out of ten, "Stripe webhook signature verification failed" in Laravel comes down to one of three things: you verified a re-encoded payload instead of the raw body, the route sits behind CSRF or body-mutating middleware, or you are using the wrong endpoint's secret. Work the five-minute checklist above, apply the fix that matches, and your &lt;code&gt;checkout.session.completed&lt;/code&gt; events will start flowing again.&lt;/p&gt;

&lt;p&gt;If you would rather not debug payment plumbing at 2 a.m., this is exactly the kind of integration work I do — &lt;a href="https://webixyhub.com" rel="noopener noreferrer"&gt;Stripe integrations and custom REST APIs built right the first time&lt;/a&gt;. If you are evaluating who should build yours, my &lt;a href="https://webixyhub.com/blog/hire-laravel-developer-guide" rel="noopener noreferrer"&gt;guide to hiring a Laravel developer&lt;/a&gt; and my breakdown of &lt;a href="https://webixyhub.com/blog/custom-rest-api-development-cost-hiring-guide" rel="noopener noreferrer"&gt;custom REST API development costs&lt;/a&gt; are good places to start.&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Hire a Laravel Developer Without Hiring the Wrong One</title>
      <dc:creator>Ayesha</dc:creator>
      <pubDate>Fri, 16 Oct 2026 05:11:56 +0000</pubDate>
      <link>https://dev.to/ayesha_921/how-to-hire-a-laravel-developer-without-hiring-the-wrong-one-3ljd</link>
      <guid>https://dev.to/ayesha_921/how-to-hire-a-laravel-developer-without-hiring-the-wrong-one-3ljd</guid>
      <description>&lt;p&gt;If you need to hire a Laravel developer, the most expensive mistake you can make is hiring on vibes: a polished portfolio, confident interview answers, and a low hourly rate. Laravel projects don't fail because the framework is hard. They fail because the developer wrote code that works on day one and becomes unmaintainable by day ninety — tangled controllers, no tests, queries that slow to a crawl as data grows, and authentication held together with hope.&lt;/p&gt;

&lt;p&gt;This guide gives you a concrete, code-level checklist to evaluate any Laravel developer or Laravel development service before you sign a contract. You don't need to be a programmer to use it. Every check below comes with a sample answer and working code, so you know exactly what "good" looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why hiring a Laravel developer is a technical decision
&lt;/h2&gt;

&lt;p&gt;Laravel is a mature, opinionated PHP framework. That opinionation is its superpower: it ships with blessed solutions for routing, validation, authentication, queues, caching, and testing. A strong Laravel developer leans on those solutions. A weak one reinvents them — badly.&lt;/p&gt;

&lt;p&gt;When you hire a Laravel developer, you are not really buying hours of typing. You are buying judgment about which of Laravel's built-in tools to use and where the sharp edges are. The interview questions below are designed to surface that judgment quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 1: Ask how they would structure a REST API
&lt;/h2&gt;

&lt;p&gt;Most business projects need an API — for a mobile app, a Vue or React frontend, or third-party integrations. Ask the candidate: "Walk me through how you'd build a REST API for products with listing, creation, and updates."&lt;/p&gt;

&lt;p&gt;A good answer mentions API resource routes, API Resources for consistent JSON output, and versioning from day one. Something like this is what you want to hear:&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/api.php&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'v1'&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;group&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;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;apiResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'products'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProductController&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="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ProductResource keeps the JSON shape stable&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductResource&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JsonResource&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;toArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&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="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'price'&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;float&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;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'created_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;created_at&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toIso8601String&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;Why it matters: API Resources decouple your database schema from your API contract. Without them, every database change breaks your mobile app. If the candidate says "just return the model as JSON," that's a red flag — it leaks internal fields like password hashes and timestamps you never intended to expose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 2: Ask about API authentication
&lt;/h2&gt;

&lt;p&gt;Ask: "How would you secure this API?" The correct modern answer for most projects is Laravel Sanctum — lightweight token authentication built into Laravel:&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;// Issue a token on login&lt;/span&gt;
&lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api-token'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plainTextToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Protect routes&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth:sanctum'&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;group&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;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;apiResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'products'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProductController&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="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Revoke on logout&lt;/span&gt;
&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&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;currentAccessToken&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;delete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it matters: A developer who reaches for a hand-rolled token system, or who stores JWT secrets in the codebase, is introducing security debt you will pay for later. Sanctum handles token hashing, abilities, and revocation out of the box. Bonus points if they mention rate limiting on the login endpoint to block brute-force attacks:&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;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'throttle:10,1'&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AuthController&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="s1"&gt;'login'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Check 3: The N+1 question — the classic tell
&lt;/h2&gt;

&lt;p&gt;Ask: "Your product listing page loads fine with 50 products but takes 8 seconds with 5,000. What do you check first?" A senior Laravel developer will immediately say N+1 queries — the most common Laravel performance bug in existence.&lt;/p&gt;

&lt;p&gt;The broken version looks innocent:&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;// BAD: one query for products, then one query PER product for its category&lt;/span&gt;
&lt;span class="nv"&gt;$products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&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;$products&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// +1 query each loop&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is one word — eager loading:&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;// GOOD: two queries total, no matter how many products&lt;/span&gt;
&lt;span class="nv"&gt;$products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'category'&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;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it matters: If a candidate cannot explain N+1 and eager loading, they have never debugged a real production performance problem. This single concept separates developers whose apps scale from developers whose apps fall over at 10,000 rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 4: Ask where slow work goes
&lt;/h2&gt;

&lt;p&gt;Ask: "A user places an order and we need to send three emails, generate a PDF invoice, and sync with the CRM. Where does that code run?"&lt;/p&gt;

&lt;p&gt;The right answer: not in the HTTP request. Slow work goes into queued jobs so the user gets an instant response:&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;// Controller stays fast&lt;/span&gt;
&lt;span class="nc"&gt;ProcessOrder&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&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;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Order received'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The heavy lifting runs in the background&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessOrder&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="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;order&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;send&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;OrderConfirmation&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;order&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="nc"&gt;InvoiceService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;generate&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;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;CrmService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;sync&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;order&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;Why it matters: Developers who do everything synchronously build apps where checkout takes 12 seconds and timeouts multiply under load. Queues are a core Laravel feature — a Laravel API developer who doesn't reach for them is fighting the framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 5: Ask about validation and error handling
&lt;/h2&gt;

&lt;p&gt;Ask: "How do you validate incoming data and handle errors?" You want to hear Form Request classes and consistent JSON error responses — never raw request data passed straight into the database:&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;StoreProductRequest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;FormRequest&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;rules&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="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|string|max:255'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'price'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|numeric|min:0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'sku'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|string|unique:products,sku'&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;Common pitfall to watch for: candidates who skip the &lt;code&gt;unique&lt;/code&gt; rule and handle duplicates with try/catch, or who return HTML error pages from API routes. Both signal a developer who hasn't shipped a real API consumed by a frontend team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 6: Ask about testing
&lt;/h2&gt;

&lt;p&gt;Ask: "How do you know your code works before it reaches production?" The minimum acceptable answer: feature tests for the API endpoints using Laravel's built-in testing tools:&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;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_can_create_product&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;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&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;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$response&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;actingAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&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;postJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/v1/products'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Test Widget'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'price'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;29.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'sku'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'WIDGET-001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertCreated&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;assertDatabaseHas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'products'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sku'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'WIDGET-001'&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;Why it matters: Untested Laravel codebases become "don't touch it" codebases within months. Every change risks breaking something invisible. A developer who writes tests is a developer you can safely hire again for phase two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red flags: when to walk away
&lt;/h2&gt;

&lt;p&gt;Beyond the technical checks, watch for these engagement-level red flags when you hire a Laravel developer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No questions about your business logic. If they quote a price before understanding what the app must do, they will build the wrong thing efficiently.&lt;/li&gt;
&lt;li&gt;"I don't need Laravel's built-in auth, I'll write my own." Rebuilding solved problems is how security holes are born.&lt;/li&gt;
&lt;li&gt;Business logic buried in controllers. Ask to see a sample controller. If it's 300 lines of if-statements with database queries inline, expect a maintenance nightmare.&lt;/li&gt;
&lt;li&gt;No mention of git, environments, or deployment. Professional Laravel development means version control, staging environments, and documented deployment — not FTP uploads.&lt;/li&gt;
&lt;li&gt;Can't explain a past production bug. Every senior developer has a war story about a 3 AM outage. A candidate with none hasn't operated real software.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What a healthy engagement looks like
&lt;/h2&gt;

&lt;p&gt;Once you've found the right person, structure the work so it stays healthy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with a technical spec — endpoints, data model, and acceptance criteria written down before code starts.&lt;/li&gt;
&lt;li&gt;Milestone-based payments tied to working, tested features — never 100% upfront.&lt;/li&gt;
&lt;li&gt;Code in your repository from day one, with pull requests you (or a reviewer) can see.&lt;/li&gt;
&lt;li&gt;A handover document at the end: how to deploy, where secrets live, how to run the test suite.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A developer who resists any of these is telling you how the engagement will go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Learning how to hire a Laravel developer is really about learning what good Laravel work looks like: resourceful routing, API Resources, Sanctum tokens, eager loading, queued jobs, request validation, and tests. A candidate fluent in these fundamentals will build you software that survives contact with real users and real data. One who isn't will build you a demo that expires on launch day.&lt;/p&gt;

&lt;p&gt;If you'd rather skip the vetting process entirely, work with a developer at webixyhub.com who already holds herself to this checklist — custom Laravel development, secure REST APIs, and integrations built to last.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://webixyhub.com/blog/hire-laravel-developer-guide" rel="noopener noreferrer"&gt;webixyhub.com/blog/hire-laravel-developer-guide&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Custom REST API Development: Cost, Process, and How to Hire the Right Developer</title>
      <dc:creator>Ayesha</dc:creator>
      <pubDate>Thu, 15 Oct 2026 05:14:48 +0000</pubDate>
      <link>https://dev.to/ayesha_921/custom-rest-api-development-cost-process-and-how-to-hire-the-right-developer-345n</link>
      <guid>https://dev.to/ayesha_921/custom-rest-api-development-cost-process-and-how-to-hire-the-right-developer-345n</guid>
      <description>&lt;h1&gt;
  
  
  Custom REST API Development: Cost, Process, and How to Hire the Right Developer
&lt;/h1&gt;

&lt;p&gt;Every few months, a business owner messages me with the same story. They paid a freelancer a few hundred dollars for a custom REST API development project, and six months later the API collapses under real traffic, has no documentation, and nobody can add a single new endpoint without breaking the mobile app. The routes were the cheap part. Everything around them — authentication, validation, rate limiting, versioning, documentation — was missing.&lt;/p&gt;

&lt;p&gt;That is the concrete problem with most API projects: buyers pay for "endpoints" while what they actually need is a stable contract between systems that survives growth, staff changes, and third-party deprecations. This guide explains what custom REST API development really includes, what it costs in 2026, what a production-ready build process looks like, and how to vet a developer before you hand them your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What custom REST API development actually includes
&lt;/h2&gt;

&lt;p&gt;A custom REST API is a bespoke HTTP interface — usually JSON over HTTPS — that lets your frontend, mobile app, or partner systems talk to your backend. But the endpoints themselves are maybe 30% of the work. A professional build includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource modelling&lt;/strong&gt; — deciding what the resources are (orders, users, invoices) and how they relate, so URLs stay clean and predictable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication and authorisation&lt;/strong&gt; — typically token-based auth (Laravel Sanctum or Passport, JWT, or OAuth2), with per-user permissions, not just a shared API key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input validation&lt;/strong&gt; — every parameter checked server-side, with consistent error responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting and throttling&lt;/strong&gt; — so one abusive client cannot take the whole API down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioning&lt;/strong&gt; — /v1/, /v2/ namespaces so you can evolve the API without breaking existing integrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt; — OpenAPI/Swagger specs or a Postman collection, so another developer can integrate without guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring and logging&lt;/strong&gt; — request logs, error tracking, and uptime alerts from day one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a quote only mentions "CRUD endpoints", you are buying the 30% and will pay for the other 70% later, usually at emergency rates. Ask every candidate what their quote covers beyond the endpoints — error handling, retries, documentation, and handover all matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much does custom REST API development cost in 2026?
&lt;/h2&gt;

&lt;p&gt;Pricing depends on scope, seniority, and region, but these are realistic freelance ranges based on current market data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single endpoint or third-party integration — $150–$500.&lt;/strong&gt; One well-built endpoint or a connection to an existing service (Stripe, Twilio, a CRM), with basic documentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API for a mobile or web app — $500–$2,000.&lt;/strong&gt; Multiple endpoints with authentication, validation, and testing — the typical MVP backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom API with database and business logic — $2,000–$6,000.&lt;/strong&gt; Scalable architecture, secure data handling, queued jobs, and full documentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ongoing maintenance and support — $1,000–$3,000 per engagement.&lt;/strong&gt; Monitoring, versioning, performance tuning, and bug fixes as traffic grows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hourly rates for freelance API developers range from roughly $15–$45/hour in South and Southeast Asia to $45–$100/hour in North America and Western Europe. The biggest cost driver is not the code itself — it is the layers around each API call: data mapping, error handling, security, testing, and documentation. A vague scope produces a vague price; a developer who prices before asking about rate limits, data quality, and failure handling is guessing.&lt;/p&gt;

&lt;p&gt;For context on hiring backend specialists generally, I have written a separate guide on &lt;a href="https://webixyhub.com/blog/hire-laravel-developer-guide" rel="noopener noreferrer"&gt;how to hire a Laravel developer without hiring the wrong one&lt;/a&gt; — the vetting principles there apply directly to API work too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The custom REST API development process, step by step
&lt;/h2&gt;

&lt;p&gt;A reliable build follows the same five phases whether it takes one week or two months:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Discovery and scoping
&lt;/h3&gt;

&lt;p&gt;The developer maps every system the API must talk to, the exact data each endpoint moves, expected traffic, and what "done" means. Output: an endpoint list and a fixed scope. If this phase is skipped, the project will be re-scoped mid-build — the most common cause of blown budgets.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. API design
&lt;/h3&gt;

&lt;p&gt;Before any code, the developer designs the contract: resource names, HTTP methods, status codes, request/response shapes, auth scheme, and error format. A good design doc fits on a few pages and can be reviewed by a non-technical founder.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Build
&lt;/h3&gt;

&lt;p&gt;Endpoints are implemented with validation, auth, and tests. Database queries are optimised early — N+1 query problems are far cheaper to prevent than to fix in production (&lt;a href="https://webixyhub.com/blog/fix-sqlstate-hy000-2002-connection-refused-laravel" rel="noopener noreferrer"&gt;fixing SQLSTATE[HY000] [2002] connection refused in Laravel&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Testing and hardening
&lt;/h3&gt;

&lt;p&gt;Feature tests for every endpoint, rate-limit checks, auth edge cases, and load testing at 2–3x expected traffic. Documentation is written from the actual implementation, not from memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Deploy and monitor
&lt;/h3&gt;

&lt;p&gt;CI/CD deployment, error tracking, uptime monitoring, and a documented versioning policy. The developer hands over credentials, docs, and a runbook — not just a zip file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a production-ready endpoint looks like
&lt;/h2&gt;

&lt;p&gt;Here is the difference between a tutorial endpoint and one that survives production. First, routes stay thin and versioned, with auth and throttling applied at the group level:&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/api.php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Controllers\Api\V1\OrderController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'v1'&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;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'auth:sanctum'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'throttle:api'&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;group&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;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;apiResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OrderController&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it works: versioning (v1) means you can ship breaking changes later without breaking existing clients; &lt;code&gt;auth:sanctum&lt;/code&gt; rejects unauthenticated traffic before it reaches your code; &lt;code&gt;throttle:api&lt;/code&gt; caps requests per user so one client cannot starve the others.&lt;/p&gt;

&lt;p&gt;Second, validation lives in a dedicated Form Request — never inline in the controller:&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;// app/Http/Requests/StoreOrderRequest.php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Requests&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\Foundation\Http\FormRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StoreOrderRequest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;FormRequest&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;authorize&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&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;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;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'create'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Order&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="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;rules&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="s1"&gt;'customer_id'&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;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'exists:customers,id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'items'&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;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'min:1'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'items.*.sku'&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;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:64'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'items.*.qty'&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;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'min:1'&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;Why it works: validation is reusable, testable, and returns consistent 422 responses with structured error messages automatically. The &lt;code&gt;authorize()&lt;/code&gt; method keeps permission logic out of the controller.&lt;/p&gt;

&lt;p&gt;Third, the controller delegates to a service, wraps writes in a transaction, and returns a proper API Resource with the correct status code and Location header:&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;// app/Http/Controllers/Api/V1/OrderController.php&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;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;StoreOrderRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;orders&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validated&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'items'&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;response&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;setStatusCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&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;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api.v1.orders.show'&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it works: the transaction guarantees an order is never half-created; the 201 status and Location header follow HTTP semantics so client developers can rely on the contract; the API Resource controls exactly which fields are exposed, so internal columns never leak.&lt;/p&gt;

&lt;p&gt;Finally, rate limiting is defined explicitly rather than left at framework defaults:&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;// bootstrap/app.php (Laravel 11+)&lt;/span&gt;

&lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api'&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;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&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="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&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;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&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="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ip&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;Why it works: per-user limits are fairer than per-IP limits behind shared NATs, and a named limiter makes the policy visible and adjustable in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  5 pitfalls that sink API projects (and the fixes)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No versioning.&lt;/strong&gt; The developer ships /orders and six months later needs to change the response shape — every mobile app in the wild breaks. Fix: namespace from day one (/v1/), even if v2 never ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth as an afterthought.&lt;/strong&gt; A shared API key in a config file, no per-user scoping. Fix: token-based auth with explicit permissions from the first endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;N+1 queries.&lt;/strong&gt; An endpoint returns 50 orders and fires 200 database queries because relationships are lazy-loaded. Fix: eager load (&lt;code&gt;-&amp;gt;with('items')&lt;/code&gt;) and check query counts in tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No documentation.&lt;/strong&gt; The original developer leaves; the new one reverse-engineers endpoints from the mobile app's network tab. Fix: OpenAPI/Swagger generated from the code, updated with every release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent failures.&lt;/strong&gt; A third-party webhook fails and nobody notices for a week. Fix: structured logging, error tracking, and uptime alerts from launch day — part of the quote, not an add-on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to vet a REST API developer before you hire
&lt;/h2&gt;

&lt;p&gt;Ask these five questions. The answers reveal more than any portfolio:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Walk me through how you would version this API when the response shape has to change." (You want: URL versioning, deprecation policy, backwards compatibility.)&lt;/li&gt;
&lt;li&gt;"How do you prevent N+1 queries, and how would I see the proof?" (You want: eager loading plus query-count assertions in tests.)&lt;/li&gt;
&lt;li&gt;"What does your error response format look like?" (You want: a consistent envelope — code, message, details — not raw exceptions.)&lt;/li&gt;
&lt;li&gt;"What is included in your handover?" (You want: docs, credentials, runbook, and a walkthrough — not a zip file.)&lt;/li&gt;
&lt;li&gt;"How do you handle a third-party API that goes down?" (You want: retries with backoff, queues, idempotency keys, and alerting.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Red flags: no mention of testing, "we add docs later", .env files committed to the repo, no experience with queues, and pricing given before any scoping questions. If you are hiring more broadly for backend work, the &lt;a href="https://webixyhub.com/blog/hire-laravel-developer-guide" rel="noopener noreferrer"&gt;Laravel developer hiring guide&lt;/a&gt; covers the full interview process in depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ready to build?
&lt;/h2&gt;

&lt;p&gt;A well-built custom API becomes the foundation your product grows on — new frontends, partner integrations, and mobile apps all plug into the same contract. A rushed one becomes the ceiling. If you are planning an API project and want a senior engineer to scope it properly — endpoints, auth, docs, and a fixed price before any code is written — &lt;a href="https://webixyhub.com" rel="noopener noreferrer"&gt;get in touch through webixyhub.com&lt;/a&gt; and we will plan it together.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>php</category>
      <category>laravel</category>
    </item>
    <item>
      <title>How to Fix SQLSTATE[HY000] [2002] Connection Refused in Laravel</title>
      <dc:creator>Ayesha</dc:creator>
      <pubDate>Wed, 14 Oct 2026 05:51:14 +0000</pubDate>
      <link>https://dev.to/ayesha_921/how-to-fix-sqlstatehy000-2002-connection-refused-in-laravel-2epb</link>
      <guid>https://dev.to/ayesha_921/how-to-fix-sqlstatehy000-2002-connection-refused-in-laravel-2epb</guid>
      <description>&lt;h1&gt;
  
  
  How to Fix SQLSTATE[HY000] [2002] Connection Refused in Laravel
&lt;/h1&gt;

&lt;p&gt;You run &lt;code&gt;php artisan migrate&lt;/code&gt;, hit refresh in the browser, or deploy to staging — and instead of your app you get the dreaded wall of red:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SQLSTATE[HY000] [2002] Connection refused (Connection: mysql, SQL: select * from information_schema.tables ...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQLSTATE[HY000] [2002] connection refused in Laravel means one thing and one thing only: PHP tried to open a network connection to your database server, and nothing answered at that host and port. Your SQL is fine. Your migrations are fine. The database simply is not reachable from where Laravel is running. This guide walks through every real-world cause I have seen in production and local development, in the order you should check them, with the exact fix for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "SQLSTATE[HY000] [2002] Connection Refused" Actually Means
&lt;/h2&gt;

&lt;p&gt;Break the error down piece by piece: &lt;code&gt;SQLSTATE[HY000]&lt;/code&gt; is a generic PDO driver error code — "general error". &lt;code&gt;[2002]&lt;/code&gt; is the MySQL client error code for "can't connect to server". "Connection refused" is the operating system telling you the TCP connection was actively rejected — or that nothing was listening on that address and port. The key insight: &lt;strong&gt;this error is thrown by the MySQL client library before a single byte of SQL is sent&lt;/strong&gt;. So don't waste time reviewing your queries. The problem is always in the connection path: the host, the port, the transport (socket vs TCP), the credentials handshake, or the server simply being down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: Reproduce It Outside Laravel
&lt;/h2&gt;

&lt;p&gt;Before touching your &lt;code&gt;.env&lt;/code&gt; file, confirm where the failure actually is. Run this from the server or machine running your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Is anything listening on the MySQL port?&lt;/span&gt;
nc &lt;span class="nt"&gt;-zv&lt;/span&gt; 127.0.0.1 3306

&lt;span class="c"&gt;# 2. Can the MySQL client connect with the same credentials Laravel uses?&lt;/span&gt;
mysql &lt;span class="nt"&gt;-h&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;-P&lt;/span&gt; 3306 &lt;span class="nt"&gt;-u&lt;/span&gt; your_db_user &lt;span class="nt"&gt;-p&lt;/span&gt; your_database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;nc&lt;/code&gt; says the connection was refused, your database server is down or listening elsewhere — that is a server problem, not a Laravel problem. If &lt;code&gt;nc&lt;/code&gt; succeeds but the &lt;code&gt;mysql&lt;/code&gt; client fails with access denied, the server is up and your credentials or user host permissions are wrong. Either way, you have just narrowed the search dramatically before editing a single line of application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: The Database Server Is Not Running
&lt;/h2&gt;

&lt;p&gt;The most common cause, and the most embarrassing. After a server reboot, a Docker restart, or a fresh Homestead/Vagrant provision, MySQL/MariaDB may simply not be started — or started but not enabled to start on boot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check the status&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status mysql    &lt;span class="c"&gt;# Ubuntu/Debian&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status mariadb  &lt;span class="c"&gt;# if you run MariaDB&lt;/span&gt;

&lt;span class="c"&gt;# Start it now AND enable it for the next reboot&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; mysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;enable&lt;/code&gt; flag is the part everyone misses: &lt;code&gt;start&lt;/code&gt; fixes it right now, &lt;code&gt;enable&lt;/code&gt; fixes it after the next reboot too. If you are on macOS with a tool like DBngin or MAMP, open the app and confirm the server indicator is green — those tools don't always restart servers after a system update. Then confirm Laravel sees it:&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 migrate:status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fix 2: localhost vs 127.0.0.1 — The Socket Trap
&lt;/h2&gt;

&lt;p&gt;This is the number one cause of a Laravel MySQL "connection refused" error on local machines, and it confuses everyone at least once. When you set &lt;code&gt;DB_HOST=localhost&lt;/code&gt;, the MySQL client doesn't use TCP. It tries to connect through a &lt;strong&gt;Unix socket file&lt;/strong&gt; (like &lt;code&gt;/var/run/mysqld/mysqld.sock&lt;/code&gt;). When you set &lt;code&gt;DB_HOST=127.0.0.1&lt;/code&gt;, it uses TCP over the loopback interface. If the socket file path the client expects doesn't match where the server actually created it, you get connection refused — even though the server is running perfectly.&lt;/p&gt;

&lt;p&gt;The fix: in your &lt;code&gt;.env&lt;/code&gt;, use the IP address to force TCP, which removes the socket path from the equation entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=your_db_user
DB_PASSWORD=your_password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After editing &lt;code&gt;.env&lt;/code&gt;, clear the config cache (more on that in Fix 5) and retry. If you specifically want socket connections, set &lt;code&gt;DB_SOCKET&lt;/code&gt; to the real socket path — find it with &lt;code&gt;mysqladmin variables | grep socket&lt;/code&gt; — but for 95% of apps, TCP on &lt;code&gt;127.0.0.1&lt;/code&gt; is simpler and more predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: The Port Does Not Match
&lt;/h2&gt;

&lt;p&gt;MAMP defaults to port &lt;strong&gt;8889&lt;/strong&gt;, not 3306. Docker Compose mappings can publish MySQL on any host port. Laravel Sail uses 3306 on the container but your host might map something else. If your database tool connects fine on a custom port but Laravel refuses, your &lt;code&gt;DB_PORT&lt;/code&gt; is almost certainly wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_PORT=8889  # MAMP default — adjust to whatever your server actually uses
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the real port: check your DBngin/MAMP/Docker settings, or ask the server directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'mysql|maria'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That shows you exactly which port the server is listening on. Match &lt;code&gt;DB_PORT&lt;/code&gt; to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 4: Docker and Sail Hostnames
&lt;/h2&gt;

&lt;p&gt;Inside Docker, &lt;code&gt;127.0.0.1&lt;/code&gt; means "this container", not your laptop and not the MySQL container. This is the classic Laravel database "connection refused" scenario in Docker Compose and Laravel Sail setups. The rule is simple: from &lt;strong&gt;inside&lt;/strong&gt; a container (your app container, queue workers, &lt;code&gt;sail artisan&lt;/code&gt;) use the service name as the host, e.g. &lt;code&gt;DB_HOST=mysql&lt;/code&gt;. From your &lt;strong&gt;host&lt;/strong&gt; machine (running &lt;code&gt;php artisan&lt;/code&gt; locally, or a GUI client) use &lt;code&gt;127.0.0.1&lt;/code&gt; with the published port.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env for the app running INSIDE docker (Sail default)
DB_HOST=mysql
DB_PORT=3306

# .env for the app running on your HOST talking to a dockerized database
DB_HOST=127.0.0.1
DB_PORT=3306  # the published/host port from docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also give the database container a moment to be ready. Containers start in parallel, and Laravel can boot before MySQL finishes initializing. In CI pipelines, add a health check or a wait step; locally, just retry after 10–20 seconds. If your queue workers run in a separate long-lived container, restart them after fixing &lt;code&gt;.env&lt;/code&gt; — they cache the old config in memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 5: Your Config Is Cached (The Silent Killer)
&lt;/h2&gt;

&lt;p&gt;You fixed &lt;code&gt;.env&lt;/code&gt;, restarted everything, triple-checked the port — and the error persists. Nine times out of ten, Laravel is not reading your &lt;code&gt;.env&lt;/code&gt; at all because the configuration is cached. &lt;code&gt;php artisan config:cache&lt;/code&gt; bakes every value into &lt;code&gt;bootstrap/cache/config.php&lt;/code&gt;, and after that, &lt;code&gt;.env&lt;/code&gt; edits are silently ignored.&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 config:clear
php artisan cache:clear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then verify what Laravel actually sees:&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 tinker &lt;span class="nt"&gt;--execute&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"print_r(config('database.connections.mysql'));"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the output doesn't match your current &lt;code&gt;.env&lt;/code&gt;, the cache was the problem. Two rules to live by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never run &lt;code&gt;config:cache&lt;/code&gt; in local development.&lt;/strong&gt; It exists for production performance.&lt;/li&gt;
&lt;li&gt;In production, your deploy script should run &lt;code&gt;config:clear&lt;/code&gt; then &lt;code&gt;config:cache&lt;/code&gt; (in that order) on every deploy, and restart queue workers with &lt;code&gt;php artisan queue:restart&lt;/code&gt; so they pick up the fresh config.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This one cause alone accounts for a huge share of "but I already fixed the .env!" support threads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 6: Remote Databases — Bind Address, Users, and Firewalls
&lt;/h2&gt;

&lt;p&gt;If your database lives on a different server (a managed database, a separate droplet, RDS), connection refused usually means the network path is blocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On the DATABASE server, check what MySQL is bound to&lt;/span&gt;
&lt;span class="nb"&gt;sudo grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"bind-address"&lt;/span&gt; /etc/mysql/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;bind-address = 127.0.0.1&lt;/code&gt;, the server only accepts local connections. For remote access it needs the server's private IP or &lt;code&gt;0.0.0.0&lt;/code&gt; (then restart MySQL). Next, check the MySQL user's allowed host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user created as &lt;code&gt;'app'@'localhost'&lt;/code&gt; can never connect from another machine — you need &lt;code&gt;'app'@'%'&lt;/code&gt; or &lt;code&gt;'app'@'your-app-server-ip'&lt;/code&gt;. Finally, check the firewall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw status

&lt;span class="c"&gt;# allow MySQL from your app server only, never the whole internet&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 203.0.113.10 to any port 3306
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opening port 3306 to the world is a security incident waiting to happen. Restrict it to your app server's IP, or better, keep the database on a private network.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2-Minute Diagnostic Sequence
&lt;/h2&gt;

&lt;p&gt;When this error appears and you don't know the cause yet, run these in order. Most cases resolve by step 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Step 1: what does Laravel THINK the connection settings are?&lt;/span&gt;
php artisan tinker &lt;span class="nt"&gt;--execute&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"print_r(config('database.connections.mysql'));"&lt;/span&gt;

&lt;span class="c"&gt;# Step 2: is anything listening where Laravel is pointing?&lt;/span&gt;
nc &lt;span class="nt"&gt;-zv&lt;/span&gt; 127.0.0.1 3306

&lt;span class="c"&gt;# Step 3: can the mysql client connect with those same settings?&lt;/span&gt;
mysql &lt;span class="nt"&gt;-h&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;-P&lt;/span&gt; 3306 &lt;span class="nt"&gt;-u&lt;/span&gt; your_db_user &lt;span class="nt"&gt;-p&lt;/span&gt; your_database &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT 1;"&lt;/span&gt;

&lt;span class="c"&gt;# Step 4: still failing? nuke the caches and restart long-lived processes&lt;/span&gt;
php artisan config:clear &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; php artisan cache:clear &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; php artisan queue:restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this as a snippet to your team's runbook. It turns a 45-minute panic into a 2-minute routine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Editing &lt;code&gt;.env&lt;/code&gt; on the wrong server.&lt;/strong&gt; In Forge/Vapor/CI setups, make sure you edited the environment of the machine actually running the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DB_HOST&lt;/code&gt; set to a container name on the host, or &lt;code&gt;127.0.0.1&lt;/code&gt; inside a container.&lt;/strong&gt; Match the host value to where the code runs (Fix 4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typos in &lt;code&gt;DB_DATABASE&lt;/code&gt;.&lt;/strong&gt; A database that doesn't exist gives a different error ("unknown database"), but a typo'd username gives access denied — read the message carefully instead of assuming connection refused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SELinux or AppArmor&lt;/strong&gt; blocking the web server's outbound connections on hardened servers — check &lt;code&gt;audit.log&lt;/code&gt; if everything else looks right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running &lt;code&gt;php artisan serve&lt;/code&gt; in one terminal and editing &lt;code&gt;.env&lt;/code&gt; in another&lt;/strong&gt; — the dev server does pick up &lt;code&gt;.env&lt;/code&gt; changes per request, but queue workers and Octane do not. Restart them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming the fix worked without re-running the failing command.&lt;/strong&gt; Always re-run &lt;code&gt;php artisan migrate:status&lt;/code&gt; or reload the page to confirm.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prevent It From Coming Back
&lt;/h2&gt;

&lt;p&gt;Add database connectivity to your deployment and monitoring so you catch this before your users do:&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 — a scheduled health check&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Illuminate\Support\Facades\DB&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\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;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="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;connection&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;getPdo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// throws on connection failure&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;everyFiveMinutes&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;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'db-health-check'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair that with a deploy script that clears and rebuilds the config cache on every release, and this error becomes something you read about in other people's blog posts instead of debugging at 2 AM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Connection refused is Laravel telling you the truth: nothing answered at the address you gave it. Work the connection path — server, host, port, cache — and the fix is almost always one config line or one service start away. If your team is losing hours to Laravel database issues, deployment headaches, or an API that keeps falling over, I help founders and teams ship stable Laravel applications. See my work and get in touch at &lt;a href="https://webixyhub.com" rel="noopener noreferrer"&gt;WebixyHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://webixyhub.com/blog/fix-sqlstate-hy000-2002-connection-refused-laravel" rel="noopener noreferrer"&gt;https://webixyhub.com/blog/fix-sqlstate-hy000-2002-connection-refused-laravel&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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