<?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: Blogs World</title>
    <description>The latest articles on DEV Community by Blogs World (@blogs_world).</description>
    <link>https://dev.to/blogs_world</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%2F3192399%2Fa04c0345-307d-4a03-af6b-610bc69a0776.png</url>
      <title>DEV Community: Blogs World</title>
      <link>https://dev.to/blogs_world</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/blogs_world"/>
    <language>en</language>
    <item>
      <title>JSONDecodeError: Expecting value at char 0? Read the Error Position Before You Touch the JSON</title>
      <dc:creator>Blogs World</dc:creator>
      <pubDate>Wed, 11 Nov 2026 05:26:47 +0000</pubDate>
      <link>https://dev.to/blogs_world/jsondecodeerror-expecting-value-at-char-0-read-the-error-position-before-you-touch-the-json-3g09</link>
      <guid>https://dev.to/blogs_world/jsondecodeerror-expecting-value-at-char-0-read-the-error-position-before-you-touch-the-json-3g09</guid>
      <description>&lt;p&gt;&lt;code&gt;json.load()&lt;/code&gt; fails, the message says &lt;code&gt;Expecting value: line 1 column 1 (char 0)&lt;/code&gt;, and the file &lt;em&gt;looks&lt;/em&gt; fine in your editor. So you paste it into a validator, it passes, and you're stuck.&lt;/p&gt;

&lt;p&gt;The fix is to stop guessing. A &lt;code&gt;JSONDecodeError&lt;/code&gt; tells you &lt;strong&gt;exactly which character&lt;/strong&gt; the parser choked on (&lt;code&gt;e.pos&lt;/code&gt;), and the message itself names the cause. Here are six inputs that break &lt;code&gt;json.load()&lt;/code&gt;, the exact error each one produces on Python 3.13, and a 12-line helper that shows you the bad spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six broken inputs, six different messages
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;codecs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;

&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;
&lt;span class="nf"&gt;p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"empty.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bom.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;codecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;BOM_UTF8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;'{"name": "Asha"}'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"single.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"{'name': 'Asha'}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"trailing.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{"name": "Asha", "city": "Udaipur",}'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"jsonl.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{"id": 1}\n{"id": 2}\n'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"empty.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"bom.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"single.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"trailing.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"jsonl.json"&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="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;JSONDecodeError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output on Python 3.13.5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;empty.json -&amp;gt; Expecting value: line 1 column 1 (char 0)
bom.json -&amp;gt; Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)
single.json -&amp;gt; Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
trailing.json -&amp;gt; Illegal trailing comma before end of object: line 1 column 35 (char 34)
jsonl.json -&amp;gt; Extra data: line 2 column 1 (char 10)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the message, not just the char number. Each one points to a different fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;Expecting value ... (char 0)&lt;/code&gt;: there is no JSON at all
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;char 0&lt;/code&gt; means the parser found nothing it could start with. The usual causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The file is empty.&lt;/strong&gt; Something truncated it, or you opened it with &lt;code&gt;"w"&lt;/code&gt; before reading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's an HTML page.&lt;/strong&gt; This is the big one with APIs. &lt;code&gt;requests.get(url).json()&lt;/code&gt; on a 404, a login redirect, or a Cloudflare page gives you &lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/code&gt;, and &lt;code&gt;json.loads("&amp;lt;!DOCTYPE html&amp;gt;")&lt;/code&gt; fails with the exact same &lt;code&gt;char 0&lt;/code&gt; message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only whitespace.&lt;/strong&gt; &lt;code&gt;json.loads("   \n")&lt;/code&gt; fails at &lt;code&gt;line 2 column 1 (char 4)&lt;/code&gt;, which is the end of the string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For API calls, check the response before parsing:&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="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&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="s2"&gt;"application/json"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Expected JSON, got {r.headers.get('Content-Type')}: {r.text[:200]!r}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. &lt;code&gt;Unexpected UTF-8 BOM&lt;/code&gt;: the file was saved by Windows tooling
&lt;/h2&gt;

&lt;p&gt;Notepad, Excel exports and some PowerShell commands put three invisible bytes (&lt;code&gt;EF BB BF&lt;/code&gt;) at the start. Python even tells you the fix in the message:&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="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bom.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"utf-8-sig"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# {'name': 'Asha'}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;utf-8-sig&lt;/code&gt; strips the BOM if it's there and behaves like plain UTF-8 if it isn't, so it's safe to use everywhere you read files you didn't write yourself.&lt;/p&gt;

&lt;p&gt;A related trap: PowerShell 5's &lt;code&gt;&amp;gt;&lt;/code&gt; redirect writes &lt;strong&gt;UTF-16&lt;/strong&gt;. Opening that with &lt;code&gt;encoding="utf-8"&lt;/code&gt; doesn't even reach the JSON parser. It dies with &lt;code&gt;UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0&lt;/code&gt;. Reading the file as bytes and passing them to &lt;code&gt;json.loads()&lt;/code&gt; works, because &lt;code&gt;json&lt;/code&gt; auto-detects UTF-8, UTF-16 and UTF-32 from raw bytes:&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="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"u16.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"rb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;h2&gt;
  
  
  3. &lt;code&gt;Expecting property name enclosed in double quotes&lt;/code&gt;: it's a Python dict, not JSON
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;{'name': 'Asha'}&lt;/code&gt; is what &lt;code&gt;print(my_dict)&lt;/code&gt; or &lt;code&gt;str(my_dict)&lt;/code&gt; produces. JSON needs double quotes. Fix the writer, not the reader: use &lt;code&gt;json.dump(obj, f)&lt;/code&gt; instead of &lt;code&gt;f.write(str(obj))&lt;/code&gt;. If you're stuck with old files that were written that way, &lt;code&gt;ast.literal_eval()&lt;/code&gt; can read them safely. Don't use &lt;code&gt;eval()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;Illegal trailing comma&lt;/code&gt;: hand-edited config
&lt;/h2&gt;

&lt;p&gt;JSON doesn't allow a comma after the last item. Older Python versions report this as &lt;code&gt;Expecting property name enclosed in double quotes&lt;/code&gt;, which is confusing. Python 3.13 names it directly. The position (&lt;code&gt;char 34&lt;/code&gt;) lands right on the stray comma.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;code&gt;Extra data&lt;/code&gt;: it's JSON Lines, not one JSON document
&lt;/h2&gt;

&lt;p&gt;The first object parsed fine, and then the parser found &lt;em&gt;more&lt;/em&gt; at line 2. That's a &lt;code&gt;.jsonl&lt;/code&gt; / NDJSON file (one object per line), common in logs and data exports. Parse it line by line:&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="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"jsonl.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&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="c1"&gt;# [{'id': 1}, {'id': 2}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. The one that &lt;em&gt;doesn't&lt;/em&gt; fail: &lt;code&gt;NaN&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;json.loads('{"score": NaN}')&lt;/code&gt; happily returns &lt;code&gt;{'score': nan}&lt;/code&gt;. Python accepts it, but strict parsers (JavaScript's &lt;code&gt;JSON.parse&lt;/code&gt;, most APIs) reject it. If you produce JSON for someone else, use &lt;code&gt;json.dumps(obj, allow_nan=False)&lt;/code&gt;. It raises &lt;code&gt;ValueError: Out of range float values are not JSON compliant: nan&lt;/code&gt; instead of quietly shipping invalid JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 12-line helper that shows the bad character
&lt;/h2&gt;

&lt;p&gt;When the file is big, line and column numbers aren't enough. Print the bytes around &lt;code&gt;e.pos&lt;/code&gt;:&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="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_json_debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"rb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{len(raw)} bytes, starts with {raw[:4]!r}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"utf-8-sig"&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;JSONDecodeError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{e.msg} at line {e.lineno}, col {e.colno}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"^"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the trailing-comma file it prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="mi"&gt;36&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;starts&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"na'
Illegal trailing comma before end of object at line 1, col 35
'me"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Asha"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Udaipur"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
                               &lt;span class="err"&gt;^&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line alone catches most cases. &lt;code&gt;0 bytes&lt;/code&gt; means an empty file, &lt;code&gt;b'\xef\xbb\xbf{'&lt;/code&gt; means a BOM, &lt;code&gt;b'\xff\xfe{\x00'&lt;/code&gt; means UTF-16, and &lt;code&gt;b'&amp;lt;!DO'&lt;/code&gt; means HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Read the &lt;strong&gt;message&lt;/strong&gt;: "Expecting value" vs "BOM" vs "property name" vs "Extra data" are different bugs.&lt;/li&gt;
&lt;li&gt;Look at the &lt;strong&gt;first 4 bytes&lt;/strong&gt; of the file or response.&lt;/li&gt;
&lt;li&gt;For APIs, check the status code and &lt;code&gt;Content-Type&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; calling &lt;code&gt;.json()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Read files you didn't write with &lt;code&gt;encoding="utf-8-sig"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Write JSON with &lt;code&gt;json.dump()&lt;/code&gt;, never &lt;code&gt;str(dict)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to see where a large document breaks before writing code, paste it into a &lt;a href="https://www.blogs-world.in/tools/json-formatter/" rel="noopener noreferrer"&gt;JSON formatter and validator&lt;/a&gt;. It points at the failing line. For the basics of &lt;code&gt;json.load&lt;/code&gt; vs &lt;code&gt;json.loads&lt;/code&gt;, nested data and writing files back, there's a longer guide on &lt;a href="https://www.blogs-world.in/blog/how-to-read-a-json-file-in-python-with-examples/" rel="noopener noreferrer"&gt;reading JSON files in Python&lt;/a&gt;, and a roundup of other &lt;a href="https://www.blogs-world.in/blog/common-python-errors/" rel="noopener noreferrer"&gt;common Python errors&lt;/a&gt; and how to fix them.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I write practical debugging guides at &lt;a href="https://www.blogs-world.in/" rel="noopener noreferrer"&gt;Blogs World&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>json</category>
      <category>debugging</category>
      <category>beginners</category>
    </item>
    <item>
      <title>pandas merge() Returned More Rows Than You Started With? Catch Duplicate Keys with validate=</title>
      <dc:creator>Blogs World</dc:creator>
      <pubDate>Wed, 04 Nov 2026 05:28:03 +0000</pubDate>
      <link>https://dev.to/blogs_world/pandas-merge-returned-more-rows-than-you-started-with-catch-duplicate-keys-with-validate-1g30</link>
      <guid>https://dev.to/blogs_world/pandas-merge-returned-more-rows-than-you-started-with-catch-duplicate-keys-with-validate-1g30</guid>
      <description>&lt;p&gt;You left-joined 4 orders onto a customer table and got back &lt;strong&gt;6 rows&lt;/strong&gt;. Worse, the revenue column went from 520 to 830, and nobody noticed until the report reached a manager.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;merge()&lt;/code&gt; did exactly what it was told. A left join keeps every left row, but if a key appears &lt;strong&gt;twice on the right&lt;/strong&gt;, every matching left row is copied once per match. No error, no warning, just quietly inflated numbers.&lt;/p&gt;

&lt;p&gt;Here is how to reproduce it, catch it automatically, and fix the three key problems that cause most "wrong row count" merges.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 6-line reproduction
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
&lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Asha&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Asha&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ravi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Meera&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Udaipur&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Jaipur&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pune&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Delhi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;

&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_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;how&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On pandas 3.0 this prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4 6 520 830
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Customer 10 has two rows (an old address and a new one), so orders 1 and 4 each come back twice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   order_id  customer_id  amount   name     city
0         1           10     250   Asha  Udaipur
1         1           10     250   Asha   Jaipur
2         2           11     120   Ravi     Pune
3         3           12      90  Meera    Delhi
4         4           10      60   Asha  Udaipur
5         4           10      60   Asha   Jaipur
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Find the duplicate keys
&lt;/h2&gt;

&lt;p&gt;Before blaming the join, look at the lookup table:&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="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;value_counts&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;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;# customer_id
# 10    2
&lt;/span&gt;
&lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;duplicated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_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;keep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="c1"&gt;#    customer_id  name     city
# 0           10  Asha  Udaipur
# 1           10  Asha   Jaipur
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;keep=False&lt;/code&gt; marks &lt;strong&gt;every&lt;/strong&gt; copy, so you see all the conflicting rows side by side and can decide which one is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Make pandas refuse the bad merge
&lt;/h2&gt;

&lt;p&gt;This is the part most tutorials skip. &lt;code&gt;merge()&lt;/code&gt; has a &lt;code&gt;validate=&lt;/code&gt; argument that checks the relationship you &lt;em&gt;expect&lt;/em&gt; and raises if the data disagrees:&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="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_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;how&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;many_to_one&lt;/span&gt;&lt;span class="sh"&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 plaintext"&gt;&lt;code&gt;MergeError: Merge keys are not unique in right dataset; not a many-to-one merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The options are &lt;code&gt;"one_to_one"&lt;/code&gt;, &lt;code&gt;"one_to_many"&lt;/code&gt;, &lt;code&gt;"many_to_one"&lt;/code&gt; and &lt;code&gt;"many_to_many"&lt;/code&gt; (which checks nothing). Many orders per customer, one row per customer, is &lt;code&gt;many_to_one&lt;/code&gt;. Put this on every merge in a pipeline that feeds a report: it turns a silent wrong number into a loud failure on the day the lookup table changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Fix the data, not the symptom
&lt;/h2&gt;

&lt;p&gt;Calling &lt;code&gt;drop_duplicates()&lt;/code&gt; on the &lt;strong&gt;result&lt;/strong&gt; hides the problem and can drop legitimate rows. Decide which lookup row wins, dedupe the right side, then merge with the check still on:&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="n"&gt;customers_latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop_duplicates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_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;keep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customers_latest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_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;how&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;many_to_one&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# 4 520
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have an &lt;code&gt;updated_at&lt;/code&gt; column, sort by it first so &lt;code&gt;keep="last"&lt;/code&gt; really means "most recent", rather than "whatever order the CSV happened to be in".&lt;/p&gt;

&lt;h2&gt;
  
  
  The opposite symptom: rows that should match don't
&lt;/h2&gt;

&lt;p&gt;Sometimes the count is right but half the joined columns are &lt;code&gt;NaN&lt;/code&gt;. Add &lt;code&gt;indicator=True&lt;/code&gt; to see which rows actually matched:&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="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B2 &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;c3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;

&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;how&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_merge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;value_counts&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 plaintext"&gt;&lt;code&gt;_merge
left_only     2
both          2
right_only    0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;left_only&lt;/code&gt; rows found no partner. Here it is a trailing space in &lt;code&gt;"B2 "&lt;/code&gt; and lowercase &lt;code&gt;"c3"&lt;/code&gt;. Normalize both sides before joining:&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that every row is &lt;code&gt;both&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more traps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mismatched key types.&lt;/strong&gt; If one side read IDs as numbers and the other as strings (common after &lt;code&gt;read_excel&lt;/code&gt; or a JSON API), pandas 3.0 stops you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValueError: You are trying to merge on int64 and str columns for key 'customer_id'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Convert explicitly with &lt;code&gt;.astype(...)&lt;/code&gt; on one side. Don't just cast both to &lt;code&gt;str&lt;/code&gt;, or &lt;code&gt;10&lt;/code&gt; and &lt;code&gt;"10.0"&lt;/code&gt; will never match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NaN keys match each other.&lt;/strong&gt; Unlike SQL, pandas treats missing keys as equal:&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;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;#     k  x  y
# 0 NaN  2  3
# 1 NaN  2  4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One missing key on the left just turned into two joined rows. Drop or fill null keys (&lt;code&gt;df.dropna(subset=["k"])&lt;/code&gt;) before merging unless you really want them paired.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Result has more rows than the left table after a left join? The right side has duplicate keys.&lt;/li&gt;
&lt;li&gt;Find them with &lt;code&gt;value_counts()&lt;/code&gt; or &lt;code&gt;duplicated(..., keep=False)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;validate="many_to_one"&lt;/code&gt; (or whatever relationship you expect) to every merge feeding a report.&lt;/li&gt;
&lt;li&gt;Dedupe the lookup table deliberately, not the merged result.&lt;/li&gt;
&lt;li&gt;Lots of &lt;code&gt;NaN&lt;/code&gt; after the join? Use &lt;code&gt;indicator=True&lt;/code&gt;, then strip and normalize case on string keys.&lt;/li&gt;
&lt;li&gt;Check that key dtypes match, and drop null keys first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a refresher on how inner, left, right and outer joins differ (with diagrams and row counts for each), I wrote it up in &lt;a href="https://www.blogs-world.in/blog/pandas-merge-inner-left-right-outer-join-examples/" rel="noopener noreferrer"&gt;pandas merge: inner, left, right and outer join examples&lt;/a&gt;. When your data starts life in spreadsheets, &lt;a href="https://www.blogs-world.in/blog/python-read-write-excel-file-openpyxl-example/" rel="noopener noreferrer"&gt;reading and writing Excel files with openpyxl&lt;/a&gt; covers the import side. And if a key column comes from an API, the free &lt;a href="https://www.blogs-world.in/tools/json-formatter/" rel="noopener noreferrer"&gt;JSON formatter&lt;/a&gt; on &lt;a href="https://www.blogs-world.in/tools/" rel="noopener noreferrer"&gt;Blogs World&lt;/a&gt; makes it easy to spot a stray string ID or trailing space before it reaches your join.&lt;/p&gt;

</description>
      <category>python</category>
      <category>pandas</category>
      <category>datascience</category>
      <category>debugging</category>
    </item>
    <item>
      <title>asyncio.gather() Still Takes 2.5 Seconds? Find the Blocking Call with Debug Mode</title>
      <dc:creator>Blogs World</dc:creator>
      <pubDate>Thu, 29 Oct 2026 05:25:02 +0000</pubDate>
      <link>https://dev.to/blogs_world/asynciogather-still-takes-25-seconds-find-the-blocking-call-with-debug-mode-4m7f</link>
      <guid>https://dev.to/blogs_world/asynciogather-still-takes-25-seconds-find-the-blocking-call-with-debug-mode-4m7f</guid>
      <description>&lt;p&gt;You wrapped five calls in &lt;code&gt;asyncio.gather()&lt;/code&gt;, expecting them to finish in about the time of the slowest one. Instead the total is the &lt;strong&gt;sum&lt;/strong&gt; of all five, exactly as if you had written a plain &lt;code&gt;for&lt;/code&gt; loop.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gather()&lt;/code&gt; is almost never the problem. Something inside one of your coroutines is &lt;strong&gt;blocking the event loop&lt;/strong&gt;: a call that does real waiting without ever handing control back with &lt;code&gt;await&lt;/code&gt;. While it runs, no other task can make progress, so your "concurrent" code quietly turns sequential.&lt;/p&gt;

&lt;p&gt;Here is how to prove it in a minute, find the exact culprit, and fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10-line reproduction
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&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;fetch_blocking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# looks harmless, freezes the whole loop
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;i&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;fetch_ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# waits in a worker thread
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;i&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;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch_blocking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocking: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch_ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;to_thread: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Python 3.13 this prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blocking: 2.50s
to_thread: 0.51s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same &lt;code&gt;gather()&lt;/code&gt;, same five tasks. The only difference is whether the waiting happens on the event loop thread or off it. In real code &lt;code&gt;time.sleep&lt;/code&gt; is rarely the offender. It is usually &lt;code&gt;requests.get()&lt;/code&gt;, a sync database driver, &lt;code&gt;open().read()&lt;/code&gt; on a big file, &lt;code&gt;subprocess.run()&lt;/code&gt;, or a CPU-heavy loop such as parsing a large JSON payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Turn on asyncio debug mode
&lt;/h2&gt;

&lt;p&gt;You don't need a profiler to find the blocker. asyncio has a built-in detector: in debug mode it logs every step of a task that holds the loop longer than &lt;code&gt;loop.slow_callback_duration&lt;/code&gt; (100 ms by default).&lt;/p&gt;

&lt;p&gt;Enable it any one of these ways:&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="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONASYNCIODEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 python app.py
&lt;span class="c"&gt;# or&lt;/span&gt;
python &lt;span class="nt"&gt;-X&lt;/span&gt; dev app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure warnings from the &lt;code&gt;asyncio&lt;/code&gt; logger are visible (for a quick script, &lt;code&gt;logging.basicConfig(level=logging.DEBUG)&lt;/code&gt; is enough). Running the example above with &lt;code&gt;debug=True&lt;/code&gt; prints one line per offending step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARNING:asyncio:Executing &amp;lt;Task finished name='Task-2' coro=&amp;lt;fetch_blocking() done, defined at demo.py:4&amp;gt; result=0 created at .../asyncio/tasks.py:748&amp;gt; took 0.500 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line tells you the coroutine name (&lt;code&gt;fetch_blocking&lt;/code&gt;), the file and line where it is defined, and how long it froze the loop. Five of those lines, each about 0.5 s, add up to your mysterious 2.5 s.&lt;/p&gt;

&lt;p&gt;If your real blockers are shorter, lower the threshold so they show up:&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_running_loop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;slow_callback_duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;  &lt;span class="c1"&gt;# 20 ms
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Debug mode adds overhead, so use it locally or in staging, not as a permanent production setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Narrow it down inside the coroutine
&lt;/h2&gt;

&lt;p&gt;The warning points at a coroutine, not a line. If that coroutine is long, time the suspect calls directly:&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="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# suspect
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get took&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule of thumb: &lt;strong&gt;any call that waits on the network, disk, or a subprocess and is not preceded by &lt;code&gt;await&lt;/code&gt; is a blocker.&lt;/strong&gt; So is any pure-Python loop that runs for more than a few milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Pick the right fix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What blocks&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;time.sleep(n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;await asyncio.sleep(n)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;requests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an async client such as &lt;code&gt;httpx.AsyncClient&lt;/code&gt; or &lt;code&gt;aiohttp&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sync DB driver (&lt;code&gt;psycopg2&lt;/code&gt;, &lt;code&gt;pymysql&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;an async driver (&lt;code&gt;asyncpg&lt;/code&gt;, &lt;code&gt;aiomysql&lt;/code&gt;, SQLAlchemy's async engine), or wrap calls in &lt;code&gt;asyncio.to_thread&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a sync library with no async version&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;await asyncio.to_thread(func, *args)&lt;/code&gt; (Python 3.9+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU-heavy work (big parsing, hashing, image work)&lt;/td&gt;
&lt;td&gt;a process pool via &lt;code&gt;loop.run_in_executor(ProcessPoolExecutor(), func, arg)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;asyncio.to_thread&lt;/code&gt; is the quickest unblock for I/O-bound sync code, but remember it runs in a thread pool. Because of the GIL, threads will not speed up pure-Python CPU work, which is why that row goes to a process pool instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The FastAPI version of this bug
&lt;/h2&gt;

&lt;p&gt;This shows up constantly in FastAPI, and it is sneaky because the "fix" people try first makes it worse.&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="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/report&lt;/span&gt;&lt;span class="sh"&gt;"&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;report&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UPSTREAM&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# blocks the event loop
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;async def&lt;/code&gt;, FastAPI runs the endpoint &lt;strong&gt;on the event loop&lt;/strong&gt;. The blocking &lt;code&gt;requests.get()&lt;/code&gt; freezes every other request the worker is handling, so under load one slow upstream makes the whole service look dead.&lt;/p&gt;

&lt;p&gt;You have two honest options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep it sync.&lt;/strong&gt; Declare it as plain &lt;code&gt;def report():&lt;/code&gt;. FastAPI then runs it in a thread pool, and the blocking call only ties up one worker thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go fully async.&lt;/strong&gt; Keep &lt;code&gt;async def&lt;/code&gt; and use an async client with &lt;code&gt;await&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/report&lt;/span&gt;&lt;span class="sh"&gt;"&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;report&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;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UPSTREAM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you should not do is mix them: &lt;code&gt;async def&lt;/code&gt; plus a sync client is the worst of both. If you are still deciding between frameworks for this kind of service, I compared the trade-offs in &lt;a href="https://www.blogs-world.in/blog/fastapi-vs-flask-which-is-better-in-2026/" rel="noopener noreferrer"&gt;FastAPI vs Flask: which is better in 2026&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Once it's unblocked: limit concurrency
&lt;/h2&gt;

&lt;p&gt;When the loop is free, &lt;code&gt;gather()&lt;/code&gt; fires everything at once, and 500 simultaneous requests can trip rate limits or exhaust a connection pool. Cap it with a semaphore:&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="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&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;with&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&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;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&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;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Python 3.11+ you can use &lt;code&gt;asyncio.TaskGroup&lt;/code&gt; instead of &lt;code&gt;gather()&lt;/code&gt; when you want one failing task to cancel the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Total time equals the sum of the tasks? Something is blocking the loop.&lt;/li&gt;
&lt;li&gt;Run with &lt;code&gt;asyncio.run(main(), debug=True)&lt;/code&gt; or &lt;code&gt;PYTHONASYNCIODEBUG=1&lt;/code&gt; and read the &lt;code&gt;Executing &amp;lt;Task ...&amp;gt; took X seconds&lt;/code&gt; warnings.&lt;/li&gt;
&lt;li&gt;Lower &lt;code&gt;slow_callback_duration&lt;/code&gt; if the blockers are short.&lt;/li&gt;
&lt;li&gt;Swap sync clients for async ones, or wrap them in &lt;code&gt;asyncio.to_thread&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Send CPU-heavy work to a process pool.&lt;/li&gt;
&lt;li&gt;In FastAPI, never put a blocking call inside &lt;code&gt;async def&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're newer to &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; and want the fundamentals first (coroutines, &lt;code&gt;create_task&lt;/code&gt;, timeouts and cancellation), there's a beginner walkthrough in this &lt;a href="https://www.blogs-world.in/blog/python-asyncio-tutorial-for-beginners-with-examples/" rel="noopener noreferrer"&gt;Python asyncio tutorial with examples&lt;/a&gt;. And when you're debugging the API payloads themselves, the free browser tools on &lt;a href="https://www.blogs-world.in/tools/" rel="noopener noreferrer"&gt;Blogs World&lt;/a&gt;, like the &lt;a href="https://www.blogs-world.in/tools/json-formatter/" rel="noopener noreferrer"&gt;JSON formatter&lt;/a&gt;, help you inspect responses without pasting them into random sites.&lt;/p&gt;

</description>
      <category>python</category>
      <category>asyncio</category>
      <category>fastapi</category>
      <category>performance</category>
    </item>
    <item>
      <title>pip Says "Successfully installed", Python Says ModuleNotFoundError: Find the Interpreter You're Actually Using</title>
      <dc:creator>Blogs World</dc:creator>
      <pubDate>Fri, 16 Oct 2026 05:29:26 +0000</pubDate>
      <link>https://dev.to/blogs_world/pip-says-successfully-installed-python-says-modulenotfounderror-find-the-interpreter-youre-22ba</link>
      <guid>https://dev.to/blogs_world/pip-says-successfully-installed-python-says-modulenotfounderror-find-the-interpreter-youre-22ba</guid>
      <description>&lt;p&gt;You run &lt;code&gt;pip install requests&lt;/code&gt;. The terminal prints &lt;code&gt;Successfully installed requests-2.32.3&lt;/code&gt;. You run your script and get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ModuleNotFoundError: No module named 'requests'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package isn't broken and the install didn't fail. In almost every case I've debugged, the package went into &lt;strong&gt;one Python&lt;/strong&gt; and your script is running on &lt;strong&gt;another one&lt;/strong&gt;. A typical machine has several: the system Python, a python.org install, a Microsoft Store Python, maybe Anaconda, plus a virtual environment per project. Each has its own &lt;code&gt;site-packages&lt;/code&gt; folder, and a package installed into one is invisible to the others.&lt;/p&gt;

&lt;p&gt;So stop reinstalling and ask a better question: which interpreter is running this code, and which interpreter did pip install into?&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Ask the running Python who it is
&lt;/h2&gt;

&lt;p&gt;Put these two lines at the very top of the failing script, or run them in the same terminal, notebook or editor run button that throws the error:&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;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask pip where it installed the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip show requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the &lt;code&gt;Location:&lt;/code&gt; line. If &lt;code&gt;sys.executable&lt;/code&gt; points to &lt;code&gt;C:\Users\you\project\.venv\Scripts\python.exe&lt;/code&gt; and &lt;code&gt;Location:&lt;/code&gt; points to &lt;code&gt;C:\Users\you\AppData\Local\Programs\Python\Python312\Lib\site-packages&lt;/code&gt;, you've found the bug in thirty seconds. Different interpreters, different packages.&lt;/p&gt;

&lt;p&gt;You can also check from the shell which &lt;code&gt;pip&lt;/code&gt; and which &lt;code&gt;python&lt;/code&gt; your terminal resolves to:&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;# Windows (CMD or PowerShell)&lt;/span&gt;
where python
where pip

&lt;span class="c"&gt;# macOS / Linux&lt;/span&gt;
which &lt;span class="nt"&gt;-a&lt;/span&gt; python3
which &lt;span class="nt"&gt;-a&lt;/span&gt; pip3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those lists start with different folders, &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;python&lt;/code&gt; don't belong to each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Install with the interpreter, not with a bare &lt;code&gt;pip&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The single habit that prevents most of these errors is to never call &lt;code&gt;pip&lt;/code&gt; on its own. Call it through the exact Python you'll run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;python -m pip&lt;/code&gt; guarantees pip installs into the &lt;code&gt;site-packages&lt;/code&gt; of that &lt;code&gt;python&lt;/code&gt;. If you want to be completely explicit, use the path you printed in step 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\y&lt;/span&gt;ou&lt;span class="se"&gt;\p&lt;/span&gt;roject&lt;span class="se"&gt;\.&lt;/span&gt;venv&lt;span class="se"&gt;\S&lt;/span&gt;cripts&lt;span class="se"&gt;\p&lt;/span&gt;ython.exe &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows, the &lt;code&gt;py&lt;/code&gt; launcher can pick a version for you (&lt;code&gt;py -3.12 -m pip install requests&lt;/code&gt;), but be aware that &lt;code&gt;py&lt;/code&gt; doesn't know about your activated venv unless you call &lt;code&gt;python&lt;/code&gt; from inside it. Inside a project, prefer the venv's own &lt;code&gt;python&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;python -m pip&lt;/code&gt; itself fails with &lt;code&gt;No module named pip&lt;/code&gt;, that interpreter has no pip yet. &lt;code&gt;python -m ensurepip --upgrade&lt;/code&gt; usually restores it. I wrote up the other recovery paths, including broken venvs and PATH issues, in &lt;a href="https://www.blogs-world.in/blog/how-to-fix-modulenotfounderror-no-module-named-pip/" rel="noopener noreferrer"&gt;how to fix ModuleNotFoundError: No module named pip&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Check the five places the mismatch usually hides
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The venv wasn't active when you installed
&lt;/h3&gt;

&lt;p&gt;Virtual environment activation only lasts for that terminal session. Open a new terminal, forget to activate, run &lt;code&gt;pip install&lt;/code&gt;, and the package lands in your global Python. Then you activate the venv, run the script, and it isn't there.&lt;/p&gt;

&lt;p&gt;Check for the &lt;code&gt;(.venv)&lt;/code&gt; prefix in your prompt before installing anything. On Windows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.venv\Scripts\activate        # CMD
.venv\Scripts\Activate.ps1    # PowerShell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If PowerShell refuses with an execution policy error, &lt;code&gt;Set-ExecutionPolicy -Scope CurrentUser RemoteSigned&lt;/code&gt; is the usual fix. My &lt;a href="https://www.blogs-world.in/blog/python-virtual-environment-venv-tutorial-windows/" rel="noopener noreferrer"&gt;step-by-step venv guide for Windows&lt;/a&gt; covers CMD vs PowerShell activation, &lt;code&gt;.venv&lt;/code&gt; vs &lt;code&gt;venv&lt;/code&gt; naming, and why you shouldn't commit the folder to Git.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Your editor picked a different interpreter
&lt;/h3&gt;

&lt;p&gt;VS Code, PyCharm and other editors have their own interpreter setting, separate from your terminal. The terminal can be inside &lt;code&gt;.venv&lt;/code&gt; while the Run button uses the global Python.&lt;/p&gt;

&lt;p&gt;In VS Code, open the Command Palette, run &lt;strong&gt;Python: Select Interpreter&lt;/strong&gt;, and choose the one inside your project's &lt;code&gt;.venv&lt;/code&gt;. The selected interpreter is shown in the status bar. If the error only appears when you click Run but not when you type &lt;code&gt;python script.py&lt;/code&gt;, this is your cause.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Jupyter is running a different kernel
&lt;/h3&gt;

&lt;p&gt;Notebooks are the worst offender, because the kernel is a separate process. Running &lt;code&gt;!pip install pandas&lt;/code&gt; in a cell installs into whatever &lt;code&gt;pip&lt;/code&gt; is first on the shell PATH, which may not be the kernel's Python.&lt;/p&gt;

&lt;p&gt;Use the magic command instead, which targets the running kernel:&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="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart the kernel. &lt;code&gt;import sys; sys.executable&lt;/code&gt; inside the notebook tells you which environment the kernel is really using.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Multiple Python versions, one &lt;code&gt;pip&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Installing Python 3.12 doesn't remove 3.11. If &lt;code&gt;pip&lt;/code&gt; belongs to 3.11 and &lt;code&gt;python&lt;/code&gt; resolves to 3.12, every install goes to the wrong version. The giveaway is the version number inside the &lt;code&gt;Location:&lt;/code&gt; path from &lt;code&gt;pip show&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;python -m pip&lt;/code&gt; fixes this permanently because the pip always matches the python.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Your file is shadowing the package
&lt;/h3&gt;

&lt;p&gt;This one isn't an interpreter problem, but it produces confusing import errors. If your script is called &lt;code&gt;requests.py&lt;/code&gt;, &lt;code&gt;random.py&lt;/code&gt; or &lt;code&gt;pandas.py&lt;/code&gt;, or there's a folder with that name next to it, Python imports &lt;strong&gt;your file&lt;/strong&gt; instead of the installed package. You'll see &lt;code&gt;ModuleNotFoundError: No module named 'requests.adapters'&lt;/code&gt;, or an &lt;code&gt;AttributeError&lt;/code&gt; like &lt;code&gt;module 'requests' has no attribute 'get'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Rename the file, and delete any &lt;code&gt;__pycache__&lt;/code&gt; folder next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick note on package names vs import names
&lt;/h2&gt;

&lt;p&gt;Sometimes the install and the import really are different names. &lt;code&gt;pip install beautifulsoup4&lt;/code&gt; gives you &lt;code&gt;import bs4&lt;/code&gt;. &lt;code&gt;pip install opencv-python&lt;/code&gt; gives you &lt;code&gt;import cv2&lt;/code&gt;. &lt;code&gt;pip install Pillow&lt;/code&gt; gives you &lt;code&gt;import PIL&lt;/code&gt;. &lt;code&gt;pip install python-dotenv&lt;/code&gt; gives you &lt;code&gt;import dotenv&lt;/code&gt;. If &lt;code&gt;pip show&lt;/code&gt; finds the package in the right interpreter and the import still fails, check the import name in the package's docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 60-second checklist
&lt;/h2&gt;

&lt;p&gt;When pip says it installed and Python says it can't find it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Print &lt;code&gt;sys.executable&lt;/code&gt; from the place that fails (script, Run button, notebook).&lt;/li&gt;
&lt;li&gt;Compare it with the &lt;code&gt;Location:&lt;/code&gt; from &lt;code&gt;pip show &amp;lt;package&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reinstall with &lt;code&gt;&amp;lt;that python&amp;gt; -m pip install &amp;lt;package&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Make sure the venv is active and your editor or kernel points at it.&lt;/li&gt;
&lt;li&gt;Check that no local file or folder shadows the package name.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's all it usually takes. &lt;code&gt;ModuleNotFoundError&lt;/code&gt; is one of the most common errors beginners hit, and I cover it alongside fourteen others, with real tracebacks, in &lt;a href="https://www.blogs-world.in/blog/common-python-errors/" rel="noopener noreferrer"&gt;Common Python errors: 15 fixes with real examples&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write practical debugging guides for Python, Laravel, Next.js and Node.js at &lt;a href="https://www.blogs-world.in/" rel="noopener noreferrer"&gt;Blogs World&lt;/a&gt;, along with free browser-based developer tools like a &lt;a href="https://www.blogs-world.in/tools/json-formatter/" rel="noopener noreferrer"&gt;JSON formatter&lt;/a&gt; and a &lt;a href="https://www.blogs-world.in/tools/regex-tester/" rel="noopener noreferrer"&gt;regex tester&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>beginners</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Next.js App Router 405s: The Export-Name Trap (and Four Lookalikes)</title>
      <dc:creator>Blogs World</dc:creator>
      <pubDate>Mon, 12 Oct 2026 05:27:24 +0000</pubDate>
      <link>https://dev.to/blogs_world/nextjs-app-router-405s-the-export-name-trap-and-four-lookalikes-1ace</link>
      <guid>https://dev.to/blogs_world/nextjs-app-router-405s-the-export-name-trap-and-four-lookalikes-1ace</guid>
      <description>&lt;p&gt;Your frontend fires &lt;code&gt;POST /api/checkout&lt;/code&gt;. The Network panel shows &lt;strong&gt;405 Method Not Allowed&lt;/strong&gt;. The handler you swear you wrote never logs a line.&lt;/p&gt;

&lt;p&gt;In the Pages Router era this usually meant a missing &lt;code&gt;if (req.method === 'POST')&lt;/code&gt; branch. In the App Router it almost always means something quieter: &lt;strong&gt;Next.js never registered a POST handler for that path&lt;/strong&gt;, so it correctly refuses the method before your code runs.&lt;/p&gt;

&lt;p&gt;Here is the trap, four lookalikes that produce the same status code, and a short checklist that ends the guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: wrong export shape
&lt;/h2&gt;

&lt;p&gt;App Router Route Handlers live in &lt;code&gt;app/api/.../route.ts&lt;/code&gt; and register methods by &lt;strong&gt;uppercase named exports&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/checkout/route.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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="na"&gt;status&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These do &lt;strong&gt;not&lt;/strong&gt; register POST:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ default export — Pages Router muscle memory&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ lowercase — not a recognized method export&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ method switch inside a default export&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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;Next.js supports &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;HEAD&lt;/code&gt;, and &lt;code&gt;OPTIONS&lt;/code&gt;. If the matched &lt;code&gt;route.ts&lt;/code&gt; does not export the method the client sent, you get 405 — by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four lookalikes that feel like "my POST is broken"
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Wrong file path
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;POST /api/checkout&lt;/code&gt; only hits &lt;code&gt;app/api/checkout/route.ts&lt;/code&gt;. A file at &lt;code&gt;app/api/checkout/route.tsx&lt;/code&gt;, &lt;code&gt;pages/api/checkout.ts&lt;/code&gt;, or &lt;code&gt;app/api/checkouts/route.ts&lt;/code&gt; will not help. Dynamic segments must match too: &lt;code&gt;/api/tickets/12&lt;/code&gt; needs &lt;code&gt;app/api/tickets/[id]/route.ts&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;page.tsx&lt;/code&gt; and &lt;code&gt;route.ts&lt;/code&gt; at the same segment
&lt;/h3&gt;

&lt;p&gt;You cannot put both at the same route segment. If a &lt;code&gt;page.tsx&lt;/code&gt; already owns &lt;code&gt;/api/checkout&lt;/code&gt; (unusual but it happens during messy migrations), the Route Handler never mounts the way you expect. Split the paths.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Proxy / middleware short-circuit
&lt;/h3&gt;

&lt;p&gt;Next.js has been moving from &lt;code&gt;middleware.ts&lt;/code&gt; toward a &lt;code&gt;proxy&lt;/code&gt; convention. Whatever sits in front of the handler can return, redirect, or rewrite before your &lt;code&gt;POST&lt;/code&gt; export runs. If logs inside &lt;code&gt;POST&lt;/code&gt; never fire, inspect the proxy/middleware matcher and early returns next — not the handler body.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. CORS preflight confusion
&lt;/h3&gt;

&lt;p&gt;A browser cross-origin &lt;code&gt;POST&lt;/code&gt; often starts with &lt;code&gt;OPTIONS&lt;/code&gt;. If you only exported &lt;code&gt;POST&lt;/code&gt; and a gateway or edge layer answers OPTIONS poorly, DevTools can look like "POST failed" when the preflight did. For intentional cross-origin APIs, export &lt;code&gt;OPTIONS&lt;/code&gt; (or set headers in config) and verify with a real preflight. A local CORS header tester helps isolate whether the browser is blocked before your handler runs: &lt;a href="https://www.blogs-world.in/tools/cors-header-tester-generator/" rel="noopener noreferrer"&gt;CORS Header Tester &amp;amp; Generator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A five-minute triage
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the request URL and method in DevTools (no trailing-slash surprise, no rewrite).&lt;/li&gt;
&lt;li&gt;Open the exact &lt;code&gt;app/api/.../route.ts&lt;/code&gt; that should own that path.&lt;/li&gt;
&lt;li&gt;Confirm an &lt;strong&gt;uppercase&lt;/strong&gt; &lt;code&gt;export async function POST&lt;/code&gt; (or whichever method you need).&lt;/li&gt;
&lt;li&gt;Add one log on the first line of that export. Redeploy / refresh. If the log never appears, the export is not matched — fix path or export shape before debugging business logic.&lt;/li&gt;
&lt;li&gt;If only production fails, check the deployment adapter, basePath, and any CDN rules that strip or rewrite &lt;code&gt;/api&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Migration note (Pages → App)
&lt;/h2&gt;

&lt;p&gt;You can keep &lt;code&gt;pages/api&lt;/code&gt; endpoints alive while you migrate UI routes. When you do move an endpoint, rewrite it as named method exports on the Web &lt;code&gt;Request&lt;/code&gt;/&lt;code&gt;Response&lt;/code&gt; APIs — do not paste a Pages-style default handler into &lt;code&gt;route.ts&lt;/code&gt; and expect it to work. For the broader migration checklist (layouts, navigation hooks, async params), see the &lt;a href="https://www.blogs-world.in/blog/nextjs-app-router-vs-pages-router-migration-guide/" rel="noopener noreferrer"&gt;Next.js App Router vs Pages Router migration guide&lt;/a&gt; on Blogs World.&lt;/p&gt;

&lt;p&gt;If you are stuck on a stubborn 405 after the export shape is correct, the deeper walkthrough — Proxy matchers, deployment adapters, and App Router-specific pitfalls — is here: &lt;a href="https://www.blogs-world.in/blog/nextjs-api-route-405-method-not-allowed-fix-app-router/" rel="noopener noreferrer"&gt;Next.js API Route 405 Method Not Allowed Fix (App Router)&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;A 405 in App Router Route Handlers is usually not a mystery HTTP bug. It means &lt;strong&gt;no handler was registered for that method on that path&lt;/strong&gt;. Fix the export name and the file location first. Everything else is a lookalike.&lt;/p&gt;

&lt;p&gt;— Published for developers who would rather spend the afternoon on product work than on &lt;code&gt;route.ts&lt;/code&gt; archaeology. More practical guides and browser tools: &lt;a href="https://www.blogs-world.in/" rel="noopener noreferrer"&gt;Blogs World&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Laravel retry_after vs timeout vs backoff: The Three Numbers Behind Duplicate Queue Jobs</title>
      <dc:creator>Blogs World</dc:creator>
      <pubDate>Fri, 09 Oct 2026 05:25:57 +0000</pubDate>
      <link>https://dev.to/blogs_world/laravel-retryafter-vs-timeout-vs-backoff-the-three-numbers-behind-duplicate-queue-jobs-9mo</link>
      <guid>https://dev.to/blogs_world/laravel-retryafter-vs-timeout-vs-backoff-the-three-numbers-behind-duplicate-queue-jobs-9mo</guid>
      <description>&lt;p&gt;A customer gets charged twice. A report email goes out three times. The logs show &lt;code&gt;App\Jobs\SyncInvoice has been attempted too many times&lt;/code&gt; even though the job "only ran once" as far as anyone can tell.&lt;/p&gt;

&lt;p&gt;When I dig into these, the code inside &lt;code&gt;handle()&lt;/code&gt; is almost never the problem. The problem is three numbers that people treat as interchangeable: &lt;strong&gt;&lt;code&gt;retry_after&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/strong&gt;, and &lt;strong&gt;&lt;code&gt;backoff&lt;/code&gt;&lt;/strong&gt;. They live in different files, they're owned by different parts of Laravel, and they mean completely different things.&lt;/p&gt;

&lt;p&gt;Here's what each one actually controls, and the single rule that keeps them from fighting each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;retry_after&lt;/code&gt;: "I assume this worker died"
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;retry_after&lt;/code&gt; lives on the queue &lt;strong&gt;connection&lt;/strong&gt; in &lt;code&gt;config/queue.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="s1"&gt;'redis'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'connection'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'queue'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'REDIS_QUEUE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'retry_after'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'block_for'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; a retry delay. It's a lease. When a worker reserves a job, the queue gives it &lt;code&gt;retry_after&lt;/code&gt; seconds. If the job hasn't been deleted or released by then, the queue assumes the worker crashed and makes the job available again, so another worker can pick it up.&lt;/p&gt;

&lt;p&gt;Notice what that means: if your job legitimately takes 120 seconds and &lt;code&gt;retry_after&lt;/code&gt; is 90, a second worker will grab the same job at second 90 &lt;strong&gt;while the first one is still running&lt;/strong&gt;. That's your double charge. Each pickup also counts as an attempt, which is how you end up with "attempted too many times" on a job whose business logic never finished.&lt;/p&gt;

&lt;p&gt;(SQS doesn't use &lt;code&gt;retry_after&lt;/code&gt;; its visibility timeout plays the same role and is configured in AWS.)&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;timeout&lt;/code&gt;: "kill this job if it runs too long"
&lt;/h2&gt;

&lt;p&gt;Timeout is owned by the &lt;strong&gt;worker process&lt;/strong&gt;, not the connection. You set it on the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan queue:work redis &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or per job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SyncInvoice&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="nv"&gt;$timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&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;When the limit is hit, the worker process is killed (this needs the &lt;code&gt;pcntl&lt;/code&gt; PHP extension; without it, timeouts are silently ignored). The job then goes back through the normal attempt rules. If you'd rather a timed-out job fail immediately instead of being retried, set:&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="nv"&gt;$failOnTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. &lt;code&gt;backoff&lt;/code&gt;: "wait before the next attempt"
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;backoff&lt;/code&gt; is the one people &lt;em&gt;think&lt;/em&gt; &lt;code&gt;retry_after&lt;/code&gt; is. It's the delay before a job that &lt;strong&gt;threw an exception&lt;/strong&gt; is attempted again:&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;SyncInvoice&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="nv"&gt;$tries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 10s, then 60s, then 5 minutes&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&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;Without a backoff, a job that fails because an external API is down gets retried almost instantly, burns through all its tries in a few seconds, and lands in &lt;code&gt;failed_jobs&lt;/code&gt; before the API has even recovered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule: timeout &amp;lt; retry_after
&lt;/h2&gt;

&lt;p&gt;The Laravel docs state it plainly, and it's the one line I'd tattoo on every queue config:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;--timeout&lt;/code&gt; value should always be at least several seconds shorter than your &lt;code&gt;retry_after&lt;/code&gt; configuration value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That ordering guarantees the worker kills a stuck job &lt;em&gt;before&lt;/em&gt; the queue hands it to someone else. Get it backwards and you've built a duplicate-processing machine.&lt;/p&gt;

&lt;p&gt;A sane starting point for a job that normally finishes in under 30 seconds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Job &lt;code&gt;$timeout&lt;/code&gt; / &lt;code&gt;--timeout&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;job class or worker command&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;retry_after&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;config/queue.php&lt;/code&gt; connection&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$backoff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;job class&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[10, 60, 300]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$tries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;job class&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you have one genuinely long job (a big export, video processing), don't raise &lt;code&gt;retry_after&lt;/code&gt; for everything. Put it on its &lt;strong&gt;own connection&lt;/strong&gt; with a larger &lt;code&gt;retry_after&lt;/code&gt;, and run a dedicated worker with a matching larger &lt;code&gt;--timeout&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Horizon users
&lt;/h3&gt;

&lt;p&gt;Horizon sets the worker timeout per supervisor in &lt;code&gt;config/horizon.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="s1"&gt;'supervisor-1'&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;'connection'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'queue'&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;'default'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'tries'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&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;The same rule applies: that &lt;code&gt;timeout&lt;/code&gt; must stay below the &lt;code&gt;retry_after&lt;/code&gt; of the connection it uses. It's easy to raise one and forget the other during an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the job safe to run twice anyway
&lt;/h2&gt;

&lt;p&gt;Even with perfect numbers, a worker can die after charging the card but before deleting the job. Treat "runs at least once" as the contract and make the side effect idempotent:&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;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;PaymentGateway&lt;/span&gt; &lt;span class="nv"&gt;$gateway&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;$invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;findOrFail&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;invoiceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;paid_at&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&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="c1"&gt;// already done on a previous attempt&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"invoice-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$invoice&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="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'paid_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most payment providers accept an idempotency key, which makes the gateway itself refuse a second identical charge.&lt;/p&gt;

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

&lt;p&gt;Before you change anything else, check these in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;config/queue.php&lt;/code&gt; and write down &lt;code&gt;retry_after&lt;/code&gt; for the connection you actually use. Confirm &lt;code&gt;QUEUE_CONNECTION&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; points at that connection.&lt;/li&gt;
&lt;li&gt;Find the effective timeout: the job's &lt;code&gt;$timeout&lt;/code&gt; if set, otherwise the worker's &lt;code&gt;--timeout&lt;/code&gt; (default 60), or the Horizon supervisor's &lt;code&gt;timeout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirm timeout is at least a few seconds lower than &lt;code&gt;retry_after&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check that the job has a &lt;code&gt;$backoff&lt;/code&gt; if it calls anything over the network.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;php -m | grep pcntl&lt;/code&gt; on the worker server.&lt;/li&gt;
&lt;li&gt;After changing config, run &lt;code&gt;php artisan queue:restart&lt;/code&gt; (or &lt;code&gt;horizon:terminate&lt;/code&gt;). Long-running workers keep the old config and code in memory until they restart.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last one catches a lot of people: you fix the numbers, deploy, and nothing changes because the workers never reloaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  When retries still don't behave
&lt;/h2&gt;

&lt;p&gt;This post covers why jobs run &lt;strong&gt;too many&lt;/strong&gt; times. The opposite problem, a failed job that won't retry at all, has a different set of causes: tries and &lt;code&gt;retryUntil()&lt;/code&gt; interacting, exceptions being swallowed inside &lt;code&gt;handle()&lt;/code&gt;, &lt;code&gt;queue:retry&lt;/code&gt; pushing to a queue no worker is listening to, and Horizon quirks. I wrote that up in detail, with Laravel 11 to 13 differences, here: &lt;a href="https://www.blogs-world.in/blog/laravel-queue-failed-job-retry-not-working-causes-and-fixes/" rel="noopener noreferrer"&gt;Laravel Queue Failed Job Retry Not Working: Causes &amp;amp; Fixes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And if you need to send your &lt;code&gt;.env&lt;/code&gt; to a teammate while debugging queue settings, don't paste raw secrets into chat. The free &lt;a href="https://www.blogs-world.in/tools/laravel-env-validator/" rel="noopener noreferrer"&gt;Laravel .env security validator&lt;/a&gt; on Blogs World masks passwords and keys in the browser (nothing is uploaded) and flags production risks like &lt;code&gt;APP_DEBUG=true&lt;/code&gt; along the way.&lt;/p&gt;

&lt;p&gt;What's the worst duplicate-job bug you've hit? I'm curious whether it was the numbers or the missing idempotency check.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
    <item>
      <title>Laravel Sanctum 419 Errors: A 5-Request Network Panel Triage</title>
      <dc:creator>Blogs World</dc:creator>
      <pubDate>Mon, 05 Oct 2026 05:25:18 +0000</pubDate>
      <link>https://dev.to/blogs_world/laravel-sanctum-419-errors-a-5-request-network-panel-triage-5g</link>
      <guid>https://dev.to/blogs_world/laravel-sanctum-419-errors-a-5-request-network-panel-triage-5g</guid>
      <description>&lt;p&gt;You set up Sanctum for your SPA, hit "Log in", and get &lt;strong&gt;419 Page Expired&lt;/strong&gt; (or a CORS error, or a 401 right after a "successful" login). Then the guessing starts: flip &lt;code&gt;supports_credentials&lt;/code&gt;, add a domain somewhere, clear cache, try again.&lt;/p&gt;

&lt;p&gt;There's a faster way. Sanctum cookie auth is a chain of requests, and the browser's Network panel tells you exactly which link is broken. This is the triage order I use before touching any config.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the chain is supposed to work
&lt;/h2&gt;

&lt;p&gt;For a first-party SPA using Sanctum's cookie (stateful) mode:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The SPA calls &lt;code&gt;GET /sanctum/csrf-cookie&lt;/code&gt;. Laravel sets an &lt;code&gt;XSRF-TOKEN&lt;/code&gt; cookie and a session cookie.&lt;/li&gt;
&lt;li&gt;The SPA sends &lt;code&gt;POST /login&lt;/code&gt;. The HTTP client copies &lt;code&gt;XSRF-TOKEN&lt;/code&gt; into an &lt;code&gt;X-XSRF-TOKEN&lt;/code&gt; header and sends the cookies.&lt;/li&gt;
&lt;li&gt;Laravel validates the CSRF token, starts the session, and returns success.&lt;/li&gt;
&lt;li&gt;The SPA calls &lt;code&gt;GET /api/user&lt;/code&gt; with the same cookies and gets the authenticated user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A 419 means step 3 failed: Laravel didn't get a CSRF token that matches the session. Everything below is about finding out &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request 1: the preflight (OPTIONS)
&lt;/h2&gt;

&lt;p&gt;Open DevTools, tick &lt;strong&gt;Preserve log&lt;/strong&gt;, filter to Fetch/XHR, and log in once. If your SPA and API are on different origins, you'll see an &lt;code&gt;OPTIONS&lt;/code&gt; request before the real one.&lt;/p&gt;

&lt;p&gt;Check the response headers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; must be your &lt;strong&gt;exact&lt;/strong&gt; frontend origin (for example &lt;code&gt;http://localhost:3000&lt;/code&gt;), never &lt;code&gt;*&lt;/code&gt;. Browsers refuse credentials with a wildcard.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Access-Control-Allow-Credentials: true&lt;/code&gt; must be present.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If either is wrong, the fix is in &lt;code&gt;config/cors.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="s1"&gt;'paths'&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;'api/*'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sanctum/csrf-cookie'&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="s1"&gt;'logout'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="s1"&gt;'allowed_origins'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'FRONTEND_URL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'http://localhost:3000'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="s1"&gt;'supports_credentials'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A common miss: &lt;code&gt;login&lt;/code&gt; isn't in &lt;code&gt;paths&lt;/code&gt;, so &lt;code&gt;/sanctum/csrf-cookie&lt;/code&gt; works but &lt;code&gt;/login&lt;/code&gt; gets no CORS headers at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request 2: &lt;code&gt;/sanctum/csrf-cookie&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;You want a &lt;code&gt;204&lt;/code&gt; with two &lt;code&gt;Set-Cookie&lt;/code&gt; headers: &lt;code&gt;XSRF-TOKEN&lt;/code&gt; and your session cookie (&lt;code&gt;laravel_session&lt;/code&gt; or your app's name).&lt;/p&gt;

&lt;p&gt;If the cookies are set but show a yellow warning icon in the Cookies tab, the browser rejected them. Hover the icon; it usually tells you why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domain mismatch:&lt;/strong&gt; &lt;code&gt;SESSION_DOMAIN&lt;/code&gt; doesn't cover the host. Use &lt;code&gt;null&lt;/code&gt; for plain localhost, or &lt;code&gt;.example.com&lt;/code&gt; when the SPA and API share a parent domain (&lt;code&gt;app.example.com&lt;/code&gt; and &lt;code&gt;api.example.com&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure over HTTP:&lt;/strong&gt; &lt;code&gt;SESSION_SECURE_COOKIE=true&lt;/code&gt; on a non-HTTPS local setup means the cookie is silently dropped.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Request 3: &lt;code&gt;POST /login&lt;/code&gt; (where the 419 shows up)
&lt;/h2&gt;

&lt;p&gt;Open the request headers and check two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is &lt;code&gt;X-XSRF-TOKEN&lt;/code&gt; present?&lt;/strong&gt; If not, your client isn't reading the cookie. With Axios 1.6.2 and later, cross-origin requests need both flags:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;axios&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="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;withCredentials&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="na"&gt;withXSRFToken&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this one instance everywhere. A second bare &lt;code&gt;axios.post()&lt;/code&gt; call somewhere in the codebase is a classic source of "it works on one page but not another".&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is the &lt;code&gt;Cookie&lt;/code&gt; header sent?&lt;/strong&gt; If the header exists but cookies are missing, go back to Request 2. The browser never stored them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If both are present and you &lt;em&gt;still&lt;/em&gt; get 419, Laravel probably isn't treating the request as stateful. Check the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stateful-domain check
&lt;/h2&gt;

&lt;p&gt;Sanctum only applies session and CSRF middleware to requests from domains listed in &lt;code&gt;SANCTUM_STATEFUL_DOMAINS&lt;/code&gt;. The value is a host &lt;strong&gt;with port, without scheme&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SANCTUM_STATEFUL_DOMAINS=localhost:3000
SESSION_DOMAIN=null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in Laravel 11+ make sure stateful API middleware is enabled 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;statefulApi&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 traps I see all the time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The frontend runs on &lt;code&gt;localhost&lt;/code&gt;, but the API is called via &lt;code&gt;127.0.0.1&lt;/code&gt;. To the browser those are different sites, so cookies don't cross over. Pick one and use it everywhere.&lt;/li&gt;
&lt;li&gt;You edited &lt;code&gt;.env&lt;/code&gt; but config is cached. Run &lt;code&gt;php artisan config:clear&lt;/code&gt; and restart the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Request 4: &lt;code&gt;GET /api/user&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If login returns 200/204 but this returns &lt;strong&gt;401&lt;/strong&gt;, the session cookie from login isn't coming back. Usually that's the same &lt;code&gt;SESSION_DOMAIN&lt;/code&gt; or &lt;code&gt;localhost&lt;/code&gt; vs &lt;code&gt;127.0.0.1&lt;/code&gt; mismatch, or the route is protected with &lt;code&gt;auth:sanctum&lt;/code&gt; while the request isn't recognised as stateful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request 5: the one you shouldn't make
&lt;/h2&gt;

&lt;p&gt;Don't "fix" 419 by adding &lt;code&gt;/login&lt;/code&gt; to the CSRF exceptions. It hides the symptom and removes protection from the route that needs it most. If the chain above checks out, Sanctum works with CSRF on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; is the exact SPA origin, not &lt;code&gt;*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;supports_credentials&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, and &lt;code&gt;login&lt;/code&gt;/&lt;code&gt;logout&lt;/code&gt; are in CORS &lt;code&gt;paths&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;/sanctum/csrf-cookie&lt;/code&gt; sets both cookies with no browser warning&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;X-XSRF-TOKEN&lt;/code&gt; header and &lt;code&gt;Cookie&lt;/code&gt; header are both on &lt;code&gt;POST /login&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;SANCTUM_STATEFUL_DOMAINS&lt;/code&gt; includes host and port, no scheme&lt;/li&gt;
&lt;li&gt;[ ] Same hostname everywhere (&lt;code&gt;localhost&lt;/code&gt; &lt;em&gt;or&lt;/em&gt; &lt;code&gt;127.0.0.1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;php artisan config:clear&lt;/code&gt; after every &lt;code&gt;.env&lt;/code&gt; change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to sanity-check what your API actually returns for a given origin before digging into Laravel config, I built a free &lt;a href="https://www.blogs-world.in/tools/cors-header-tester-generator/" rel="noopener noreferrer"&gt;CORS header tester and generator&lt;/a&gt; for exactly that.&lt;/p&gt;

&lt;p&gt;For the full Laravel 13 walkthrough, including different-subdomain setups, Next.js server-context gotchas, and a long FAQ, see the complete guide: &lt;a href="https://www.blogs-world.in/blog/laravel-sanctum-cors-419-error-fix/" rel="noopener noreferrer"&gt;Laravel Sanctum CORS 419 Error Fix (Laravel 13)&lt;/a&gt; on Blogs World.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Laravel Sanctum 419 Error: A 5-Step Debugging Guide</title>
      <dc:creator>Blogs World</dc:creator>
      <pubDate>Tue, 08 Sep 2026 12:49:05 +0000</pubDate>
      <link>https://dev.to/blogs_world/laravel-sanctum-419-error-a-5-step-debugging-guide-1fj3</link>
      <guid>https://dev.to/blogs_world/laravel-sanctum-419-error-a-5-step-debugging-guide-1fj3</guid>
      <description>&lt;p&gt;Your Laravel API works.&lt;/p&gt;

&lt;p&gt;Your credentials are correct.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/sanctum/csrf-cookie&lt;/code&gt; might even return &lt;code&gt;204&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then you send the login request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /login
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and Laravel responds with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;419 Page Expired
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've been changing random CORS settings hoping the error disappears, stop there.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Laravel Sanctum 419 error is usually a CSRF/session authentication problem&lt;/strong&gt;, and the fastest way to fix it is to identify exactly where the Sanctum authentication flow breaks.&lt;/p&gt;

&lt;p&gt;Here's the debugging order I use.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;For Laravel Sanctum SPA authentication, verify these five things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Stateful middleware
2. SANCTUM_STATEFUL_DOMAINS
3. CORS credentials
4. Session cookies
5. Axios CSRF handling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And always request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /sanctum/csrf-cookie
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /login
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If one piece of that chain is wrong, Laravel can respond with &lt;strong&gt;419 CSRF token mismatch / Page Expired&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Understand What 419 Actually Means
&lt;/h2&gt;

&lt;p&gt;When using Laravel Sanctum's SPA authentication, Laravel relies on normal session cookies and CSRF protection.&lt;/p&gt;

&lt;p&gt;The browser flow looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend
   |
   | GET /sanctum/csrf-cookie
   v
Laravel
   |
   | Sets XSRF-TOKEN
   | Sets session cookie
   v
Browser
   |
   | POST /login
   | Cookie + X-XSRF-TOKEN
   v
Laravel validates request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Laravel cannot validate that CSRF/session relationship, you can end up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;419 Page Expired
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the question isn't simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is my CORS config correct?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"At which point does the authentication chain break?"&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. Start With the Browser Network Panel
&lt;/h2&gt;

&lt;p&gt;Before touching &lt;code&gt;.env&lt;/code&gt;, Axios, or Laravel configuration, open:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chrome DevTools → Network&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then perform the login again.&lt;/p&gt;

&lt;p&gt;Find the &lt;strong&gt;first request that doesn't behave as expected&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case A: OPTIONS fails
&lt;/h3&gt;

&lt;p&gt;If the browser blocks an &lt;code&gt;OPTIONS&lt;/code&gt; request, investigate CORS first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case B: &lt;code&gt;/sanctum/csrf-cookie&lt;/code&gt; fails
&lt;/h3&gt;

&lt;p&gt;Your browser isn't successfully initializing Sanctum's CSRF protection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case C: CSRF request succeeds but &lt;code&gt;/login&lt;/code&gt; returns 419
&lt;/h3&gt;

&lt;p&gt;This is where I'd inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;XSRF-TOKEN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Laravel session cookie&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-XSRF-TOKEN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;cookie domain&lt;/li&gt;
&lt;li&gt;Axios credentials&lt;/li&gt;
&lt;li&gt;Sanctum stateful domains&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Case D: Login succeeds but &lt;code&gt;/api/user&lt;/code&gt; returns 401
&lt;/h3&gt;

&lt;p&gt;Now the problem is more likely related to the authenticated session or Sanctum recognizing the request as stateful.&lt;/p&gt;

&lt;p&gt;This simple classification eliminates a lot of unnecessary debugging.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Make Sure Sanctum Treats the SPA as Stateful
&lt;/h2&gt;

&lt;p&gt;Modern Laravel applications can enable Sanctum's stateful API middleware in &lt;code&gt;bootstrap/app.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example:&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\Foundation\Configuration\Middleware&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;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="kt"&gt;void&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;statefulApi&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 matters because Sanctum needs to understand that requests coming from your first-party SPA should use session authentication.&lt;/p&gt;

&lt;p&gt;If you're following an older Laravel tutorial, you may see instructions involving:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/Http/Kernel.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be careful.&lt;/p&gt;

&lt;p&gt;Laravel's application structure has evolved, so old Sanctum tutorials can send you to configuration locations that aren't appropriate for newer Laravel applications.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Check &lt;code&gt;SANCTUM_STATEFUL_DOMAINS&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Assume your setup looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend:
https://app.example.com

API:
https://api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your SPA host needs to be recognized as stateful.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SANCTUM_STATEFUL_DOMAINS=app.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One common mistake is adding the protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SANCTUM_STATEFUL_DOMAINS=https://app.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not what you normally want here.&lt;/p&gt;

&lt;p&gt;Think &lt;strong&gt;host&lt;/strong&gt;, not frontend URL.&lt;/p&gt;




&lt;h2&gt;
  
  
  localhost can be even trickier
&lt;/h2&gt;

&lt;p&gt;Suppose Vite runs on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:5173
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you may need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SANCTUM_STATEFUL_DOMAINS=localhost:5173
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't casually mix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;localhost
127.0.0.1
localhost:5173
127.0.0.1:5173
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use one consistent development setup.&lt;/p&gt;

&lt;p&gt;A surprising number of Sanctum bugs come from tiny hostname differences.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. CORS Must Allow Credentials
&lt;/h2&gt;

&lt;p&gt;If your SPA authentication uses cookies, your CORS configuration must allow credentialed requests.&lt;/p&gt;

&lt;p&gt;A typical setup could include:&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;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;

    &lt;span class="s1"&gt;'paths'&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;'api/*'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'sanctum/csrf-cookie'&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="s1"&gt;'logout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'allowed_methods'&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;'*'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'allowed_origins'&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;'https://app.example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'allowed_headers'&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;'*'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'supports_credentials'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important setting 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="s1"&gt;'supports_credentials'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And don't casually 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="s1"&gt;'allowed_origins'&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;'*'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;when you're relying on cookie credentials.&lt;/p&gt;

&lt;p&gt;Specify the frontend origin explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Origin and domain are not the same thing
&lt;/h2&gt;

&lt;p&gt;For CORS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is an &lt;strong&gt;origin&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The scheme matters.&lt;/p&gt;

&lt;p&gt;These are different origins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://app.example.com
https://app.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanwhile, Sanctum's stateful domain configuration is concerned with the appropriate host/domain.&lt;/p&gt;

&lt;p&gt;Mixing these concepts is one reason Sanctum configuration feels confusing at first.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Inspect Your Session Cookie Configuration
&lt;/h2&gt;

&lt;p&gt;Now suppose your architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.example.com
api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typical production session configuration might include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SESSION_DOMAIN=.example.com
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=lax
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parent-domain cookie configuration allows the session to work across the relevant subdomains.&lt;/p&gt;

&lt;p&gt;After changing environment settings, also remember that cached configuration can make you think your changes aren't working.&lt;/p&gt;

&lt;p&gt;Depending on your deployment workflow, refresh Laravel's configuration cache appropriately.&lt;/p&gt;

&lt;p&gt;For example:&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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and rebuild/cache configuration according to your production deployment process.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Configure Axios Correctly
&lt;/h2&gt;

&lt;p&gt;Your Laravel setup can be perfect and still fail if the frontend never sends the cookies back.&lt;/p&gt;

&lt;p&gt;For Axios:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;axios&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="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;withCredentials&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="na"&gt;withXSRFToken&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="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&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;Before login:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sanctum/csrf-cookie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;password&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;The two important Axios options are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;withCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;withXSRFToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now look in the browser.&lt;/p&gt;

&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /sanctum/csrf-cookie
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you should normally be able to see the relevant CSRF/session cookies.&lt;/p&gt;

&lt;p&gt;Then inspect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /login
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and confirm the authentication data is actually being sent back to Laravel.&lt;/p&gt;




&lt;h3&gt;
  
  
  A Better Way to Debug 419
&lt;/h3&gt;

&lt;p&gt;Instead of changing Laravel, CORS, Axios, cookies, and middleware simultaneously, use this sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 1 — CSRF endpoint
&lt;/h2&gt;

&lt;p&gt;Request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /sanctum/csrf-cookie
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Successful response
+
XSRF/session cookies created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no cookies appear, don't debug your login controller yet.&lt;/p&gt;




&lt;h3&gt;
  
  
  Test 2 — Login request
&lt;/h3&gt;

&lt;p&gt;Request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /login
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Are cookies attached?
Is X-XSRF-TOKEN present?
Is the request being blocked by CORS?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the answer is no, you've narrowed the problem substantially.&lt;/p&gt;




&lt;h3&gt;
  
  
  Test 3 — Authenticated endpoint
&lt;/h3&gt;

&lt;p&gt;After successful login:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/user
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If login succeeds but this gives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;401 Unauthenticated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;focus on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sanctum stateful configuration
Session cookie
Cookie domain
withCredentials
Middleware
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of CSRF.&lt;/p&gt;




&lt;h3&gt;
  
  
  419 vs 401 vs 422
&lt;/h3&gt;

&lt;p&gt;These errors frequently get mixed together.&lt;/p&gt;

&lt;h3&gt;
  
  
  419
&lt;/h3&gt;

&lt;p&gt;Usually investigate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CSRF
XSRF token
Cookies
Session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  401
&lt;/h3&gt;

&lt;p&gt;Usually investigate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication
Session recognition
Sanctum stateful requests
Missing credentials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  422
&lt;/h3&gt;

&lt;p&gt;Usually investigate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Validation errors
Missing fields
Invalid input
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Knowing the difference can save a surprising amount of time.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Does Sanctum Work in Postman but Fail in Chrome?
&lt;/h3&gt;

&lt;p&gt;This is an extremely common situation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Postman ✅
Browser ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it's an important clue.&lt;/p&gt;

&lt;p&gt;Browsers enforce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CORS&lt;/li&gt;
&lt;li&gt;cookie policies&lt;/li&gt;
&lt;li&gt;origins&lt;/li&gt;
&lt;li&gt;credentials&lt;/li&gt;
&lt;li&gt;SameSite behavior&lt;/li&gt;
&lt;li&gt;CSRF flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;API clients such as Postman don't behave exactly like a browser page.&lt;/p&gt;

&lt;p&gt;So if the API works from Postman but fails from React/Vue/Vite, start investigating the &lt;strong&gt;browser authentication flow&lt;/strong&gt;, not your database credentials.&lt;/p&gt;




&lt;h3&gt;
  
  
  Don't "Fix" 419 by Disabling CSRF
&lt;/h3&gt;

&lt;p&gt;You might find solutions online suggesting that you exclude your login endpoint from CSRF protection.&lt;/p&gt;

&lt;p&gt;That can make the error disappear.&lt;/p&gt;

&lt;p&gt;It doesn't necessarily mean you've fixed Sanctum.&lt;/p&gt;

&lt;p&gt;For first-party SPA session authentication, the better solution is to correctly configure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CSRF
Session cookies
Stateful domains
CORS
Frontend credentials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than removing the protection that exposed the configuration problem.&lt;/p&gt;




&lt;h3&gt;
  
  
  One Production Architecture Detail People Miss
&lt;/h3&gt;

&lt;p&gt;This setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.example.com
api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is very different from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-app.vercel.app
api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the first example, the frontend and backend share the same top-level site.&lt;/p&gt;

&lt;p&gt;In the second, they're on unrelated domains.&lt;/p&gt;

&lt;p&gt;CORS cannot magically turn unrelated domains into the same first-party cookie environment.&lt;/p&gt;

&lt;p&gt;So if you're struggling with Sanctum cookie authentication across completely different production domains, revisit the architecture itself.&lt;/p&gt;

&lt;p&gt;A custom frontend domain such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can make a first-party Sanctum setup considerably cleaner.&lt;/p&gt;




&lt;h3&gt;
  
  
  My 60-Second Sanctum 419 Checklist
&lt;/h3&gt;

&lt;p&gt;Before searching Stack Overflow again, check this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] statefulApi() enabled

[ ] SPA exists in SANCTUM_STATEFUL_DOMAINS

[ ] Exact frontend origin allowed by CORS

[ ] supports_credentials = true

[ ] Correct SESSION_DOMAIN

[ ] Secure cookie configuration matches HTTPS setup

[ ] Axios withCredentials = true

[ ] Axios withXSRFToken = true

[ ] /sanctum/csrf-cookie called before login

[ ] XSRF-TOKEN appears in browser

[ ] Session cookie appears in browser

[ ] Login request sends cookies

[ ] X-XSRF-TOKEN is sent correctly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all twelve are correct, you've eliminated most of the common causes of a Laravel Sanctum 419 error.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Debugging Order Matters More Than the Fix
&lt;/h3&gt;

&lt;p&gt;The biggest lesson isn't a specific &lt;code&gt;.env&lt;/code&gt; setting.&lt;/p&gt;

&lt;p&gt;It's this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debug the first broken request instead of randomly changing authentication configuration.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OPTIONS
   ↓
/sanctum/csrf-cookie
   ↓
Cookies
   ↓
POST /login
   ↓
/api/user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find where the expected behavior stops.&lt;/p&gt;

&lt;p&gt;That's usually where your real problem begins.&lt;/p&gt;




&lt;h2&gt;
  
  
  Need the Full Laravel Sanctum 419 Reference?
&lt;/h2&gt;

&lt;p&gt;I wrote a more detailed guide covering Laravel Sanctum CORS, CSRF cookies, Axios configuration, production domains, localhost issues, session configuration, 401 vs 419 vs 422 errors, and deployment troubleshooting.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://www.blogs-world.in/blog/laravel-sanctum-cors-419-error-fix/" rel="noopener noreferrer"&gt;Read the complete Laravel Sanctum CORS 419 Error Fix guide&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use the full guide when the quick checklist above doesn't expose the problem immediately.&lt;/p&gt;




&lt;p&gt;If you're debugging Sanctum today, remember:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;419 is the symptom. The browser's request flow tells you the cause.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>authentication</category>
    </item>
    <item>
      <title>AI Coding Tools Are Fast. That Doesn't Mean They Make You Faster</title>
      <dc:creator>Blogs World</dc:creator>
      <pubDate>Wed, 02 Sep 2026 12:20:43 +0000</pubDate>
      <link>https://dev.to/blogs_world/ai-coding-tools-are-fast-that-doesnt-mean-they-make-you-faster-2a0m</link>
      <guid>https://dev.to/blogs_world/ai-coding-tools-are-fast-that-doesnt-mean-they-make-you-faster-2a0m</guid>
      <description>&lt;p&gt;AI coding assistants can generate a surprising amount of code in a fo a terrible way to measure whether the tool is actually making a developer more productive.&lt;/p&gt;

&lt;p&gt;A 200-line implementation generated in thirty seconds sounds great until you spend the next hour understanding the diff, correcting assumptions, repairing tests, and discovering that the assistant quietly changed something outside the requested scope.&lt;/p&gt;

&lt;p&gt;The more useful question is not:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How much code did the AI generate?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How quickly did I get from a clearly defined problem to a change I would actually be comfortable merging?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That difference changes how you evaluate tools such as GitHub Copilot, Cursor, Claude Code, Codex, JetBrains AI Assistant, and other coding agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generated Code Is Not Finished Work
&lt;/h2&gt;

&lt;p&gt;Consider two coding assistants.&lt;/p&gt;

&lt;p&gt;Tool A generates a complete implementation in five minutes.&lt;/p&gt;

&lt;p&gt;Tool B takes twelve minutes.&lt;/p&gt;

&lt;p&gt;Tool A looks much faster.&lt;/p&gt;

&lt;p&gt;But then you review the results.&lt;/p&gt;

&lt;p&gt;With Tool A:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;two unrelated files were modified&lt;/li&gt;
&lt;li&gt;one existing test was changed to match the new implementation&lt;/li&gt;
&lt;li&gt;an edge case was missed&lt;/li&gt;
&lt;li&gt;you spend thirty minutes debugging the result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Tool B:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;only the relevant files were changed&lt;/li&gt;
&lt;li&gt;existing tests were preserved&lt;/li&gt;
&lt;li&gt;a new regression test was added&lt;/li&gt;
&lt;li&gt;the implementation passes the project's checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool B was technically slower at generating code.&lt;/p&gt;

&lt;p&gt;It was faster at producing &lt;strong&gt;usable code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the metric developers should care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure Time to a Verified Change
&lt;/h2&gt;

&lt;p&gt;A better productivity metric for AI-assisted development is what I think of as &lt;strong&gt;verified-change latency&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Start the timer when the task is clearly defined.&lt;/p&gt;

&lt;p&gt;Stop it when the change has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;passed the relevant tests&lt;/li&gt;
&lt;li&gt;passed linting and type checks&lt;/li&gt;
&lt;li&gt;survived code review&lt;/li&gt;
&lt;li&gt;avoided unnecessary modifications&lt;/li&gt;
&lt;li&gt;preserved existing behavior&lt;/li&gt;
&lt;li&gt;satisfied security checks where required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you a much more realistic picture of productivity.&lt;/p&gt;

&lt;p&gt;Lines generated per minute tells you almost nothing.&lt;/p&gt;

&lt;p&gt;Minutes required per trustworthy change tells you a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different AI Coding Tools Solve Different Problems
&lt;/h2&gt;

&lt;p&gt;Another mistake is expecting one AI tool to be the best at everything.&lt;/p&gt;

&lt;p&gt;Coding assistance now covers several very different workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Autocomplete
&lt;/h3&gt;

&lt;p&gt;Autocomplete should feel almost invisible.&lt;/p&gt;

&lt;p&gt;You type part of a function and the assistant predicts what comes next.&lt;/p&gt;

&lt;p&gt;The important factors here are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;suggestion latency&lt;/li&gt;
&lt;li&gt;acceptance rate&lt;/li&gt;
&lt;li&gt;understanding of nearby code&lt;/li&gt;
&lt;li&gt;consistency with existing naming patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this type of work, a fast editor-integrated assistant can be more useful than an autonomous agent.&lt;/p&gt;

&lt;p&gt;You probably don't need an agent planning an entire implementation when all you wanted was to finish a mapping function.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Repository Exploration
&lt;/h3&gt;

&lt;p&gt;The problem changes when you inherit an unfamiliar project.&lt;/p&gt;

&lt;p&gt;Now you might ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where is authentication actually enforced?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which services eventually call this database method?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Answering that requires understanding relationships across several files.&lt;/p&gt;

&lt;p&gt;This is where repository-aware assistants become much more useful than ordinary autocomplete.&lt;/p&gt;

&lt;p&gt;A good assistant should be able to trace the path through the codebase rather than simply explain the file currently open in the editor.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Multi-File Implementation
&lt;/h3&gt;

&lt;p&gt;Then there are tasks where the assistant needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;inspect the repository&lt;/li&gt;
&lt;li&gt;understand the requirement&lt;/li&gt;
&lt;li&gt;modify several files&lt;/li&gt;
&lt;li&gt;run commands&lt;/li&gt;
&lt;li&gt;observe failures&lt;/li&gt;
&lt;li&gt;adjust the implementation&lt;/li&gt;
&lt;li&gt;report what changed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is where agent-style tools become interesting.&lt;/p&gt;

&lt;p&gt;But autonomy creates another problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the bigger the task, the bigger the possible wrong diff.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Giving an agent a prompt such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Improve this project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is asking for trouble.&lt;/p&gt;

&lt;p&gt;A much better task looks like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fix the failed login attempt counter. Preserve the current API response structure, add a regression test, run the authentication test suite, and do not modify unrelated files.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The completion condition is observable.&lt;/p&gt;

&lt;p&gt;Either the relevant tests pass or they do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the Tool Evidence, Not Just a Problem Description
&lt;/h2&gt;

&lt;p&gt;When debugging with an AI assistant, avoid starting with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;My application isn't working. Fix it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Provide evidence.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Command:
npm test -- auth

Expected:
All authentication tests pass.

Actual:
2 tests fail.

Error:
Expected status 401 but received 500.

Relevant behavior:
This started after the refresh-token validation change.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the assistant has something concrete to investigate.&lt;/p&gt;

&lt;p&gt;For debugging work, I prefer asking the tool to identify the likely root cause &lt;strong&gt;before changing anything&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That small instruction makes a big difference.&lt;/p&gt;

&lt;p&gt;Otherwise, coding agents often jump straight into editing the first suspicious file they encounter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect Your Tests
&lt;/h2&gt;

&lt;p&gt;One of the easiest ways for generated code to appear correct is to change the test.&lt;/p&gt;

&lt;p&gt;Imagine the requirement says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A user without permission must receive HTTP 403.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation accidentally returns &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A bad AI-assisted workflow might change the test expectation from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations. Everything is green.&lt;/p&gt;

&lt;p&gt;The bug is still there.&lt;/p&gt;

&lt;p&gt;This is why existing tests should be treated as part of the specification unless the requirement itself has intentionally changed.&lt;/p&gt;

&lt;p&gt;When using an AI coding agent, explicitly state:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not weaken or remove existing assertions simply to make the test suite pass.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence can prevent a surprising amount of nonsense.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Review Should Focus on Consequences
&lt;/h2&gt;

&lt;p&gt;AI-assisted code review has another common failure mode: noise.&lt;/p&gt;

&lt;p&gt;A review containing twenty comments about variable names is not necessarily useful.&lt;/p&gt;

&lt;p&gt;A review identifying one broken authorization path can be extremely useful.&lt;/p&gt;

&lt;p&gt;For AI review, prioritize questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can this change expose data to the wrong user?&lt;/li&gt;
&lt;li&gt;Is error handling missing?&lt;/li&gt;
&lt;li&gt;Did the change introduce an extra database query?&lt;/li&gt;
&lt;li&gt;Is a null or empty state unhandled?&lt;/li&gt;
&lt;li&gt;Can two concurrent requests create inconsistent state?&lt;/li&gt;
&lt;li&gt;Did the implementation alter an existing API contract?&lt;/li&gt;
&lt;li&gt;Are tests missing for the failure path?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Formatters already know how to complain about spaces.&lt;/p&gt;

&lt;p&gt;Use AI review for things that require context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Still Needs Deterministic Tools
&lt;/h2&gt;

&lt;p&gt;An AI assistant can notice insecure code.&lt;/p&gt;

&lt;p&gt;That does not make it a security scanner.&lt;/p&gt;

&lt;p&gt;If you ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this application secure?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;you will probably receive a confident answer containing several reasonable observations.&lt;/p&gt;

&lt;p&gt;You will not receive proof that every relevant vulnerability has been found.&lt;/p&gt;

&lt;p&gt;Use dedicated static-analysis, dependency-scanning, and secret-detection tools for concrete findings.&lt;/p&gt;

&lt;p&gt;Then use AI to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explain the finding&lt;/li&gt;
&lt;li&gt;trace the affected path&lt;/li&gt;
&lt;li&gt;suggest remediation&lt;/li&gt;
&lt;li&gt;generate a regression test&lt;/li&gt;
&lt;li&gt;help interpret scanner output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The order matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection first. AI-assisted remediation second. Verification last.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't Give an Agent Unlimited Scope
&lt;/h2&gt;

&lt;p&gt;AI agents are particularly effective when the task has boundaries.&lt;/p&gt;

&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Update the user endpoint to validate email addresses.

Requirements:
- preserve the existing response schema
- return 422 for invalid emails
- add tests for empty and malformed addresses
- run the API test suite
- list the files changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not so good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Improve user management.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first prompt defines success.&lt;/p&gt;

&lt;p&gt;The second defines an adventure.&lt;/p&gt;

&lt;p&gt;And adventures create large pull requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repository Instructions Are Underrated
&lt;/h2&gt;

&lt;p&gt;Developers often spend a lot of time improving prompts when the real problem is missing project context.&lt;/p&gt;

&lt;p&gt;Your repository should tell an assistant things such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Package manager: pnpm

Run tests:
pnpm test

Run type checking:
pnpm typecheck

Architecture:
API handlers live in /src/api
Database access belongs in /src/repositories

Rules:
Do not edit generated files.
Do not modify migrations that have already shipped.
Do not add dependencies without explaining why.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those instructions are much more useful than trying to invent a magical universal prompt.&lt;/p&gt;

&lt;p&gt;AI coding tools are extremely sensitive to context.&lt;/p&gt;

&lt;p&gt;Better context usually beats clever wording.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare Tools Using the Same Task
&lt;/h2&gt;

&lt;p&gt;If you are deciding between coding assistants, don't compare them using random daily interactions.&lt;/p&gt;

&lt;p&gt;Create one small but representative task.&lt;/p&gt;

&lt;p&gt;For example, prepare a repository containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one failing unit test&lt;/li&gt;
&lt;li&gt;one bug spanning multiple files&lt;/li&gt;
&lt;li&gt;one simple security problem&lt;/li&gt;
&lt;li&gt;one documented completion command&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then give every tool the same task.&lt;/p&gt;

&lt;p&gt;Record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;first-pass test result&lt;/li&gt;
&lt;li&gt;time to completion&lt;/li&gt;
&lt;li&gt;number of correction prompts&lt;/li&gt;
&lt;li&gt;files modified&lt;/li&gt;
&lt;li&gt;unrelated modifications&lt;/li&gt;
&lt;li&gt;whether existing tests were changed&lt;/li&gt;
&lt;li&gt;accuracy of the final explanation&lt;/li&gt;
&lt;li&gt;time required for human review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the test more than once.&lt;/p&gt;

&lt;p&gt;Agent output is not perfectly deterministic, so a single successful attempt can be misleading.&lt;/p&gt;

&lt;p&gt;I put together a more detailed comparison of current assistants, including GitHub Copilot, Cursor, Codex, Claude Code, JetBrains AI Assistant and specialist review/security workflows, in this &lt;strong&gt;&lt;a href="https://www.blogs-world.in/blog/best-ai-tools-for-coding-and-developers-to-boost-productivity/" rel="noopener noreferrer"&gt;AI coding tools comparison for developers&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The useful part isn't picking a universal winner. It is matching the tool to the type of development work you actually do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Best AI Workflow Still Has a Human Owner
&lt;/h2&gt;

&lt;p&gt;The most important rule is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Someone must still own the final diff.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before merging an AI-assisted change, a developer should understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what changed&lt;/li&gt;
&lt;li&gt;why it changed&lt;/li&gt;
&lt;li&gt;what could break&lt;/li&gt;
&lt;li&gt;how the change was tested&lt;/li&gt;
&lt;li&gt;whether security or data handling is affected&lt;/li&gt;
&lt;li&gt;how the change would be rolled back&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI can generate the implementation.&lt;/p&gt;

&lt;p&gt;It can run commands.&lt;/p&gt;

&lt;p&gt;It can explain the diff.&lt;/p&gt;

&lt;p&gt;It can even review parts of its own work.&lt;/p&gt;

&lt;p&gt;But the person merging the change still owns the result when production behaves differently at 2 AM.&lt;/p&gt;

&lt;p&gt;That responsibility hasn't been automated.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Rule for Using Coding Assistants
&lt;/h2&gt;

&lt;p&gt;Before accepting an AI-generated change, try to explain it in one or two sentences without looking at the assistant's explanation.&lt;/p&gt;

&lt;p&gt;If you cannot explain what the code does, you probably should not merge it yet.&lt;/p&gt;

&lt;p&gt;AI coding tools are becoming incredibly capable.&lt;/p&gt;

&lt;p&gt;The winning workflow, however, isn't the one that generates the most code.&lt;/p&gt;

&lt;p&gt;It's the one that gets you to a &lt;strong&gt;small, understandable, tested and reviewable change with the least unnecessary work&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's a much less exciting benchmark than "10x developer."&lt;/p&gt;

&lt;p&gt;It is also much closer to how good software actually gets shipped.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>React App Works Locally but Not on Vercel? A Production Checklist</title>
      <dc:creator>Blogs World</dc:creator>
      <pubDate>Fri, 21 Aug 2026 12:50:23 +0000</pubDate>
      <link>https://dev.to/blogs_world/react-app-works-locally-but-not-on-vercel-a-production-checklist-41lf</link>
      <guid>https://dev.to/blogs_world/react-app-works-locally-but-not-on-vercel-a-production-checklist-41lf</guid>
      <description>&lt;p&gt;&lt;code&gt;npm run dev&lt;/code&gt; works. The production build finishes. Vercel shows a reassuring green &lt;strong&gt;Ready&lt;/strong&gt; label.&lt;/p&gt;

&lt;p&gt;Then somebody refreshes &lt;code&gt;/dashboard&lt;/code&gt; and gets a 404.&lt;/p&gt;

&lt;p&gt;Or the page opens, but login does nothing. The API request is still going to &lt;code&gt;localhost&lt;/code&gt;. An image that looked fine on your laptop has disappeared. None of these failures feels related to deployment because, technically, the deployment succeeded.&lt;/p&gt;

&lt;p&gt;That green label proves Vercel built and published something. It does not prove that every route, environment variable, browser request, and authentication assumption survived the move to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;If a React app works locally but fails on Vercel, check these in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the production build locally.&lt;/li&gt;
&lt;li&gt;Confirm the project root, build command, and output directory.&lt;/li&gt;
&lt;li&gt;Test a direct visit and refresh on every client-side route.&lt;/li&gt;
&lt;li&gt;Check environment-variable names, values, and deployment scope.&lt;/li&gt;
&lt;li&gt;Inspect the real API URL, HTTPS, CORS, and cookie settings.&lt;/li&gt;
&lt;li&gt;Look for filename-case and asset-path differences.&lt;/li&gt;
&lt;li&gt;Read the browser's first Console or Network error before changing configuration.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The order matters. Changing the framework preset will not repair a CORS policy, and adding a rewrite will not install a missing package.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, separate “deployed” from “working”
&lt;/h2&gt;

&lt;p&gt;A deployment pipeline can answer a few useful questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Could it install the dependencies?&lt;/li&gt;
&lt;li&gt;Did the build command exit successfully?&lt;/li&gt;
&lt;li&gt;Was there an output directory to publish?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It cannot automatically confirm that a user can sign in, that the API accepts the production origin, or that React Router can recover after a hard refresh.&lt;/p&gt;

&lt;p&gt;Before debugging in the dashboard, I would reproduce the production build locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a Vite project, inspect that build with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run preview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an older Create React App project, you can serve the generated folder locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx serve &lt;span class="nt"&gt;-s&lt;/span&gt; build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a better test than &lt;code&gt;npm run dev&lt;/code&gt;. Development servers are forgiving. Production builds are much less interested in forgiving a missing dependency, a bad import, or a filename whose capitalization is wrong.&lt;/p&gt;

&lt;p&gt;If you are still at the initial setup stage, this &lt;a href="https://www.blogs-world.in/blog/how-to-deploy-a-react-app-on-vercel/" rel="noopener noreferrer"&gt;step-by-step guide to deploying a React app on Vercel&lt;/a&gt; covers the complete GitHub import flow, framework settings, environment variables, SPA routing, and common deployment errors. Here, I want to stay with the awkward part that begins &lt;em&gt;after&lt;/em&gt; Vercel says the app is ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three settings quietly control the whole build
&lt;/h2&gt;

&lt;p&gt;Most React deployment problems are not mysterious. Vercel is simply looking in one place while the app is building in another.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project setup&lt;/th&gt;
&lt;th&gt;Build command&lt;/th&gt;
&lt;th&gt;Normal output directory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vite + React&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dist&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create React App&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;build&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React app inside a monorepo&lt;/td&gt;
&lt;td&gt;Usually &lt;code&gt;npm run build&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Depends on the selected app and its configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The monorepo case is where this becomes easy to miss. If the repository looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;company-project/
├── api/
├── docs/
└── apps/
    └── web/
        ├── package.json
        ├── src/
        └── vite.config.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then &lt;code&gt;apps/web&lt;/code&gt; is the project root. Pointing Vercel at &lt;code&gt;company-project&lt;/code&gt; may leave it searching for the wrong &lt;code&gt;package.json&lt;/code&gt; or running the correct command from the wrong directory.&lt;/p&gt;

&lt;p&gt;There is one more wrinkle: &lt;code&gt;dist&lt;/code&gt; is only Vite's default. If &lt;code&gt;vite.config.js&lt;/code&gt; contains a custom &lt;code&gt;build.outDir&lt;/code&gt;, the Vercel output directory has to match it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitejs/plugin-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;outDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public-app&lt;/span&gt;&lt;span class="dl"&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;That app produces &lt;code&gt;public-app&lt;/code&gt;, not &lt;code&gt;dist&lt;/code&gt;. The setting is not wrong; the two sides just need to agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The home page works, but &lt;code&gt;/account&lt;/code&gt; does not
&lt;/h2&gt;

&lt;p&gt;This failure is almost a rite of passage with a client-side React app.&lt;/p&gt;

&lt;p&gt;Clicking from &lt;code&gt;/&lt;/code&gt; to &lt;code&gt;/account&lt;/code&gt; works because React Router is already running in the browser. Refreshing &lt;code&gt;/account&lt;/code&gt; is different. The browser asks the server for &lt;code&gt;/account&lt;/code&gt;, and a static host may look for a real file at that path. There is no such file, so the server returns 404 before React gets a chance to render anything.&lt;/p&gt;

&lt;p&gt;For a frontend-only Vite single-page application using browser history, add a &lt;code&gt;vercel.json&lt;/code&gt; file at the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://openapi.vercel.sh/vercel.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rewrites"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/(.*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/index.html"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit the file and deploy again. The browser keeps the requested URL, Vercel serves &lt;code&gt;index.html&lt;/code&gt;, and the client-side router decides which screen belongs there.&lt;/p&gt;

&lt;p&gt;Do not paste a catch-all rewrite into every repository without looking at its architecture. A project that also contains API routes, server-rendered pages, or other separately served paths may need a more specific routing rule. Vercel's &lt;a href="https://vercel.com/docs/frameworks/frontend/vite" rel="noopener noreferrer"&gt;Vite deployment documentation&lt;/a&gt; is the right reference for the current SPA behavior.&lt;/p&gt;

&lt;p&gt;My route test is deliberately boring: paste a nested production URL into a new private window, open it directly, and refresh it twice. Navigation from the home page is not the same test.&lt;/p&gt;

&lt;h2&gt;
  
  
  An environment variable can be present and still be wrong
&lt;/h2&gt;

&lt;p&gt;There are four separate ways an environment variable can fail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The name does not match the build tool.&lt;/li&gt;
&lt;li&gt;The value belongs to local development.&lt;/li&gt;
&lt;li&gt;It was added to Preview but not Production, or the reverse.&lt;/li&gt;
&lt;li&gt;It was changed after the current deployment was built.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vite exposes browser variables through &lt;code&gt;import.meta.env&lt;/code&gt;, normally with a &lt;code&gt;VITE_&lt;/code&gt; prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VITE_API_BASE_URL=https://api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiBaseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_API_BASE_URL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An existing Create React App project uses the older convention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REACT_APP_API_BASE_URL=https://api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiBaseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REACT_APP_API_BASE_URL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those conventions are not interchangeable.&lt;/p&gt;

&lt;p&gt;There is also a security trap hidden in the friendly phrase “environment variable.” A &lt;code&gt;VITE_*&lt;/code&gt; value used by client-side code is bundled into JavaScript and can be inspected by anyone using the site. Vite's &lt;a href="https://vite.dev/guide/env-and-mode" rel="noopener noreferrer"&gt;environment-variable guide&lt;/a&gt; is explicit about this. Public API base URLs and browser-safe identifiers are fine; database passwords, service-role keys, private payment credentials, and similar secrets are not.&lt;/p&gt;

&lt;p&gt;Keep privileged values on a backend or server-side function.&lt;/p&gt;

&lt;p&gt;Finally, changing a variable in the Vercel dashboard does not rewrite a bundle that has already been deployed. Build a new deployment after the change, and check whether you edited the Production, Preview, or Development value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sometimes the React app is innocent
&lt;/h2&gt;

&lt;p&gt;Open the deployed site, launch DevTools, and look at the failed request in the &lt;strong&gt;Network&lt;/strong&gt; panel. The actual URL usually tells a better story than the UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  The request still points to localhost
&lt;/h3&gt;

&lt;p&gt;This will never reach the backend on your laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:3000/api/products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For every visitor, &lt;code&gt;localhost&lt;/code&gt; means that visitor's own device. Replace it with the deployed HTTPS API URL through the correct environment variable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The page is HTTPS but the API is HTTP
&lt;/h3&gt;

&lt;p&gt;Browsers can block active mixed content. If the frontend is served over HTTPS, the production API should be available over HTTPS too.&lt;/p&gt;

&lt;h3&gt;
  
  
  The API rejects the Vercel origin
&lt;/h3&gt;

&lt;p&gt;That is a backend CORS decision, not a React build failure. Add the exact production frontend origin to the API's allowed origins. If preview deployments also need API access, decide how those changing preview origins will be handled rather than opening the API to every origin by reflex.&lt;/p&gt;

&lt;p&gt;Using cookies? Check &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;SameSite&lt;/code&gt;, the cookie domain, credentialed CORS headers, and whether the browser actually stored the cookie. A response can return &lt;code&gt;200&lt;/code&gt; while the next authenticated request still fails because the browser rejected the session cookie.&lt;/p&gt;

&lt;p&gt;This is why “the API works in Postman” is useful but incomplete. Postman is not enforcing the browser's CORS and cookie rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The capitalization bug that waits for production
&lt;/h2&gt;

&lt;p&gt;Some local filesystems treat &lt;code&gt;Header.jsx&lt;/code&gt; and &lt;code&gt;header.jsx&lt;/code&gt; as though they are the same filename. A Linux build environment does not have to be so generous.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Header&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components/header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the real file is &lt;code&gt;Header.jsx&lt;/code&gt;, make the import match exactly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Header&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components/Header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same issue appears with public assets. &lt;code&gt;/Logo.png&lt;/code&gt; and &lt;code&gt;/logo.png&lt;/code&gt; are different paths on a case-sensitive system. When only one image or module fails, compare the requested path with the repository filename character by character.&lt;/p&gt;

&lt;p&gt;Also confirm that the missing file was committed. Git cannot deploy an image that only exists in your local working directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview deployments are more than disposable links
&lt;/h2&gt;

&lt;p&gt;Once Git is connected, a branch push can create a Preview deployment while the production branch continues serving the current release. That is useful only if the preview is tested as a real environment.&lt;/p&gt;

&lt;p&gt;A sensible flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Push the change to a feature branch.&lt;/li&gt;
&lt;li&gt;Let Vercel create the Preview deployment.&lt;/li&gt;
&lt;li&gt;Test direct routes, authentication, API calls, and browser errors there.&lt;/li&gt;
&lt;li&gt;Merge only after those checks pass.&lt;/li&gt;
&lt;li&gt;Verify the new Production deployment once more.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Vercel documents the branch behavior in its &lt;a href="https://vercel.com/docs/git" rel="noopener noreferrer"&gt;Git deployment guide&lt;/a&gt;. For applications that can charge cards, send messages, modify customer data, or perform admin actions, point Preview at a staging backend or test account. A hard-to-guess preview URL is not a security boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  A five-minute production check
&lt;/h2&gt;

&lt;p&gt;After every meaningful deployment, I would test these before sharing the link:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open the home page in a private window.&lt;/li&gt;
&lt;li&gt;Paste a nested route directly into the address bar and refresh it.&lt;/li&gt;
&lt;li&gt;Complete one real sign-in and sign-out cycle.&lt;/li&gt;
&lt;li&gt;Trigger one representative API read and, where safe, one write.&lt;/li&gt;
&lt;li&gt;Look for the first red Console error.&lt;/li&gt;
&lt;li&gt;Inspect failed Network requests and their full URLs.&lt;/li&gt;
&lt;li&gt;Check one narrow mobile viewport.&lt;/li&gt;
&lt;li&gt;Confirm that Production is using the intended environment values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is a small checklist, but it catches more than staring at a green deployment card ever will.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnose by symptom, not by guesswork
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;Likely area&lt;/th&gt;
&lt;th&gt;First place to look&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build fails&lt;/td&gt;
&lt;td&gt;Dependency, import, Node version, or build script&lt;/td&gt;
&lt;td&gt;Earliest relevant build-log error and local &lt;code&gt;npm run build&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment is ready but the page is blank&lt;/td&gt;
&lt;td&gt;Runtime JavaScript or missing configuration&lt;/td&gt;
&lt;td&gt;Browser Console&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/&lt;/code&gt; works but refreshing &lt;code&gt;/dashboard&lt;/code&gt; returns 404&lt;/td&gt;
&lt;td&gt;SPA fallback routing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;vercel.json&lt;/code&gt; and direct-route test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests go to &lt;code&gt;localhost&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Production API variable&lt;/td&gt;
&lt;td&gt;Network request URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser reports a CORS error&lt;/td&gt;
&lt;td&gt;Backend origin policy&lt;/td&gt;
&lt;td&gt;API CORS configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Images fail only in production&lt;/td&gt;
&lt;td&gt;Case mismatch or asset path&lt;/td&gt;
&lt;td&gt;Requested URL and repository filename&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview works but Production fails&lt;/td&gt;
&lt;td&gt;Environment scope or branch configuration&lt;/td&gt;
&lt;td&gt;Production variables and production branch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first error is normally more valuable than the tenth. Later errors are often just consequences of the first failed import, request, or initialization step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need to upload the &lt;code&gt;dist&lt;/code&gt; folder to Vercel?
&lt;/h3&gt;

&lt;p&gt;Not for a normal Git-based deployment. Commit the source code, &lt;code&gt;package.json&lt;/code&gt;, and the correct lock file. Vercel installs dependencies, runs the build command, and publishes the generated output. Generated folders such as &lt;code&gt;dist&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt;, and &lt;code&gt;node_modules&lt;/code&gt; are normally ignored.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did changing a Vercel environment variable do nothing?
&lt;/h3&gt;

&lt;p&gt;Client-side Vite variables are inserted when the app is built. Redeploy after changing the value, and make sure the variable was added to the environment—Preview or Production—used by that deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does React Router work when I click a link but fail after refresh?
&lt;/h3&gt;

&lt;p&gt;Client-side navigation happens after React has loaded. A refresh sends the nested path to the server first. A static SPA needs a fallback rewrite so the server returns &lt;code&gt;index.html&lt;/code&gt; and React Router can handle the URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can the React frontend and API use different hosting providers?
&lt;/h3&gt;

&lt;p&gt;Yes. Use the API's deployed HTTPS address and configure the backend to accept the frontend's production origin. Cross-site authentication may also require deliberate cookie and CORS settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Vercel a good host for every React project?
&lt;/h3&gt;

&lt;p&gt;It is a strong fit for many Vite SPAs, frontend dashboards, prototypes, and applications built for supported serverless or framework patterns. A traditional long-running Express server, background worker, persistent WebSocket process, or content-heavy site needing server rendering has different requirements and should be evaluated separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  “Ready” is the start of verification
&lt;/h2&gt;

&lt;p&gt;A reliable React deployment is a small contract: the repository supplies a repeatable build, Vercel publishes the correct output, the router can recover on direct requests, the frontend receives the right public configuration, and the backend accepts requests from the deployed origin.&lt;/p&gt;

&lt;p&gt;When one part of that contract breaks, start with evidence. Build locally. Open the first browser error. Read the failed request URL. Change one verified cause at a time.&lt;/p&gt;

&lt;p&gt;That approach is slower than random dashboard edits for about thirty seconds. After that, it is much faster.&lt;/p&gt;

&lt;p&gt;_Disclosure: This article was prepared with AI assistance. Its technical examples were checked against the linked Vercel and Vite documentation.&lt;/p&gt;

</description>
      <category>react</category>
      <category>vercel</category>
      <category>vite</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Java’s `this` keyword helps distinguish fields from parameters, chain constructors, and reference the current object. Practical examples: https://www.blogs-world.in/blog/understanding-the-this-keyword-in-java-a-comprehensive-guide/</title>
      <dc:creator>Blogs World</dc:creator>
      <pubDate>Tue, 18 Aug 2026 08:08:08 +0000</pubDate>
      <link>https://dev.to/blogs_world/javas-this-keyword-helps-distinguish-fields-from-parameters-chain-constructors-and-reference-3eg7</link>
      <guid>https://dev.to/blogs_world/javas-this-keyword-helps-distinguish-fields-from-parameters-chain-constructors-and-reference-3eg7</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://www.blogs-world.in/blog/understanding-the-this-keyword-in-java-a-comprehensive-guide/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimagedelivery.net%2FlLmNeOP7HXG0OqaG97wimw%2F8564e8bc-4127-49de-8063-26cdeb16bd55%2F52d385e9-e687-464e-817b-fa4995200422.png%2Fpublic" height="768" class="m-0" width="1152"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://www.blogs-world.in/blog/understanding-the-this-keyword-in-java-a-comprehensive-guide/" rel="noopener noreferrer" class="c-link"&gt;
            Mastering this in Modern Java — 2026 Developer Playbook
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Master the this keyword in Java with clear examples, best practices, and advanced tips for cleaner, bug-free code. Boost your Java skills today!
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.blogs-world.in%2Ffavicon.ico" width="256" height="256"&gt;
          blogs-world.in
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
    </item>
    <item>
      <title>I Kept Reaching for Redux by Default. One Question Fixed That</title>
      <dc:creator>Blogs World</dc:creator>
      <pubDate>Tue, 18 Aug 2026 07:50:31 +0000</pubDate>
      <link>https://dev.to/blogs_world/i-kept-reaching-for-redux-by-default-one-question-fixed-that-18f0</link>
      <guid>https://dev.to/blogs_world/i-kept-reaching-for-redux-by-default-one-question-fixed-that-18f0</guid>
      <description>&lt;p&gt;There's a specific kind of shame that comes from opening a pull request review and seeing &lt;code&gt;npm install @reduxjs/toolkit react-redux&lt;/code&gt; as the first line of the diff, for a feature that turned out to be a dropdown and a boolean.&lt;/p&gt;

&lt;p&gt;I did that more than once. Not because I didn't understand Redux. Because I didn't have a real test for when &lt;em&gt;not&lt;/em&gt; to reach for it, so I defaulted to the tool that felt "serious."&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit nobody tells you to unlearn
&lt;/h2&gt;

&lt;p&gt;Early in my React years, the advice I absorbed from tutorials was basically: small app, use &lt;code&gt;useState&lt;/code&gt;. Big app, use Redux. That's a size-based rule, and size is a terrible proxy for what actually matters, which is &lt;em&gt;how the state behaves&lt;/em&gt;, not how many files your project has.&lt;/p&gt;

&lt;p&gt;A five-screen internal tool with a workflow editor, undo history, and three roles fighting over permissions? That probably wants Redux. A forty-page marketing site with a theme toggle and a mobile nav state? That's Context, maybe not even that.&lt;/p&gt;

&lt;p&gt;I learned this the slow way, on a dashboard project where I'd wired notification counts, a selected-workspace value, and a live job feed all into one &lt;code&gt;AppContext&lt;/code&gt;. It worked fine in the demo. Then someone added a fourth thing to that context, a component three levels down started re-rendering on every WebSocket tick, and I spent an afternoon staring at React DevTools trying to figure out why a settings icon was blinking every two seconds like it had a heartbeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually separates the two tools
&lt;/h2&gt;

&lt;p&gt;Here's the reframe that fixed it for me, and it's less "which library" and more "what job is this state doing."&lt;/p&gt;

&lt;p&gt;Context is a &lt;strong&gt;plumbing&lt;/strong&gt; solution. It moves a value down the tree without prop drilling. That's it. It doesn't care where the value came from or how it changes — you could feed it a &lt;code&gt;useState&lt;/code&gt;, a &lt;code&gt;useReducer&lt;/code&gt;, or a hardcoded object, and Context wouldn't know the difference.&lt;/p&gt;

&lt;p&gt;Redux is an &lt;strong&gt;architecture&lt;/strong&gt;. Actions, reducers, a single store, a predictable flow of "this happened → here's the new state." You get that structure whether you need it or not, and for small, cohesive values, that structure is overhead, not help.&lt;/p&gt;

&lt;p&gt;So the actual decision isn't Context vs Redux. It's closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;useState / useReducer + Context
        vs.
Redux Toolkit + selectors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once I started asking "does this state need an &lt;em&gt;architecture&lt;/em&gt;, or does it just need to reach three components without fifteen props," most of my choices got obvious fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick gut-check I actually use now
&lt;/h2&gt;

&lt;p&gt;Three questions, roughly in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Who owns this?&lt;/strong&gt; One feature tree, or does it get read/written across totally unrelated routes?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How does it change?&lt;/strong&gt; A handful of clean transitions, or a tangle of business rules that touch five other pieces of state?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who's subscribing?&lt;/strong&gt; Do consumers basically all need "the same thing," or does one tiny badge need to react to a firehose of updates without the rest of the screen re-rendering?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the answers land on "one feature, simple transitions, everyone wants the same value" — Context, and honestly probably just &lt;code&gt;useReducer&lt;/code&gt; + Context, no extra library required.&lt;/p&gt;

&lt;p&gt;If it's "read everywhere, complicated rules, and only slivers of it change independently" — that's where Redux Toolkit's selector model starts paying for its setup cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake I don't make anymore
&lt;/h2&gt;

&lt;p&gt;I used to think memoizing everything in a context provider was the fix for re-render pain. &lt;code&gt;useMemo&lt;/code&gt; on the value, &lt;code&gt;useCallback&lt;/code&gt; on every handler, wrap, wrap, wrap. It helps with &lt;em&gt;accidental&lt;/em&gt; identity changes — a new object reference on every render for no reason — but it can't give you the thing Redux's &lt;code&gt;useSelector&lt;/code&gt; gives you for free: a component subscribing to &lt;em&gt;one slice&lt;/em&gt; of a much bigger state tree.&lt;/p&gt;

&lt;p&gt;Memoization fixes noise. It doesn't fix architecture. If you're three layers of &lt;code&gt;useMemo&lt;/code&gt; deep just to stop unrelated components from updating, that's usually a sign the state itself is trying to tell you something — probably that it's outgrown Context, or that it was never one cohesive value in the first place and needs splitting into separate providers.&lt;/p&gt;

&lt;p&gt;That second one, by the way, is underrated as a fix. Before reaching for Redux, try just... splitting the context. A &lt;code&gt;ThemeContext&lt;/code&gt; and a &lt;code&gt;NotificationsContext&lt;/code&gt; and a &lt;code&gt;WorkspaceContext&lt;/code&gt; instead of one &lt;code&gt;AppContext&lt;/code&gt; solves a surprising number of "why is everything re-rendering" problems on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I landed
&lt;/h2&gt;

&lt;p&gt;These days my default order looks something like: local state first, always — don't be the person adding global anything for two props. Context next, for stuff that's genuinely shared but genuinely simple. Redux Toolkit only once I can point at real coordination complexity — multiple features reacting to the same event, selectors doing real work, or a debugging need that "just add a console.log" won't satisfy anymore.&lt;/p&gt;

&lt;p&gt;And API data doesn't go in either bucket, honestly. That's a separate problem — caching, refetching, staleness — that neither Context nor a hand-rolled Redux slice handles well without a lot of extra plumbing you're better off not building yourself.&lt;/p&gt;

&lt;p&gt;I went looking for a writeup that laid out the decision framework more rigorously than my "vibes and a bad afternoon" version, and found a genuinely thorough one — includes a full decision framework, side-by-side code for both approaches, and a section specifically on the failure modes (the universal-context trap, memoizing a design problem instead of fixing it, stuffing every form input into Redux) that I definitely lived through firsthand. Worth the read if you're mid-decision on a real feature right now: &lt;a href="https://www.blogs-world.in/blog/react-context-api-vs-redux-when-to-use-each/" rel="noopener noreferrer"&gt;React Context API vs Redux: When to Use Each&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you've got your own "I reached for the wrong tool and paid for it" story, I'd genuinely like to hear it in the comments. Misery loves company, and also I collect these for the next time I'm tempted to &lt;code&gt;npm install&lt;/code&gt; my way out of a design decision.&lt;/p&gt;

</description>
      <category>react</category>
      <category>redux</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
