<?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: Ganesh Joshi</title>
    <description>The latest articles on DEV Community by Ganesh Joshi (@ganeshjoshi).</description>
    <link>https://dev.to/ganeshjoshi</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%2F3812945%2F5c98e077-4bc4-4521-8bf1-006bca4726f1.png</url>
      <title>DEV Community: Ganesh Joshi</title>
      <link>https://dev.to/ganeshjoshi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ganeshjoshi"/>
    <language>en</language>
    <item>
      <title>Core Web Vitals When Your Hero Section Is an AI Chat</title>
      <dc:creator>Ganesh Joshi</dc:creator>
      <pubDate>Sun, 08 Nov 2026 15:45:29 +0000</pubDate>
      <link>https://dev.to/ganeshjoshi/core-web-vitals-when-your-hero-section-is-an-ai-chat-4lo</link>
      <guid>https://dev.to/ganeshjoshi/core-web-vitals-when-your-hero-section-is-an-ai-chat-4lo</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was created with AI assistance and reviewed for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Putting a live AI chat at the top of a landing page is a good demo and a bad hero. Every property that makes it compelling, that it is interactive, that it streams, that it responds to what you type, is also a property that &lt;a href="https://web.dev/articles/vitals" rel="noopener noreferrer"&gt;Core Web Vitals&lt;/a&gt; penalises. The chat is usually the largest element on screen, so it becomes the LCP element. It grows as tokens arrive, so it moves everything under it. And it is the first thing a visitor touches, so it owns your INP.&lt;/p&gt;

&lt;p&gt;None of that is unavoidable. It just means the hero has to be designed backwards from the metric.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chat should not be your LCP element
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://web.dev/articles/lcp" rel="noopener noreferrer"&gt;LCP&lt;/a&gt; measures when the largest block of content in the viewport finishes rendering. If that block is a chat transcript that appears after hydration, your LCP cannot happen until JavaScript has downloaded, parsed, and run. On a mid-range phone over slow 4G that is several seconds you do not control.&lt;/p&gt;

&lt;p&gt;The fix is to make something else the largest element, and to make that thing static.&lt;/p&gt;

&lt;p&gt;In practice this means the hero is a headline, a subhead, and a chat panel that is present but visually secondary. Render the headline server-side as real text. Text has no decode step, no network request beyond the HTML itself, and it is eligible to be the LCP element the moment the document paints.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The h1 is the LCP candidate. It ships in the HTML and needs no JS.&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hero"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Ask anything about your codebase&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Answers grounded in your own repository, not the public internet.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChatPanel&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the design genuinely requires the chat to dominate the fold, then pre-render its first state on the server: a greeting message, a couple of example prompts, the input box. That markup is real content, it counts for LCP, and it arrives with the document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserve the space before the tokens arrive
&lt;/h2&gt;

&lt;p&gt;Streaming output is the most reliable way to generate layout shift. Each chunk makes the transcript taller, and everything below it moves down. That is textbook &lt;a href="https://web.dev/articles/cls" rel="noopener noreferrer"&gt;CLS&lt;/a&gt;, and unlike LCP it keeps accumulating for as long as the response runs.&lt;/p&gt;

&lt;p&gt;Give the transcript a fixed height, or a min-height large enough for a typical answer, and let it scroll internally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.chat-transcript&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c"&gt;/* space is reserved before the first token */&lt;/span&gt;
  &lt;span class="nl"&gt;overflow-y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;overscroll-behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;contain&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 related mistakes are worth naming because they look harmless.&lt;/p&gt;

&lt;p&gt;The first is animating the container open as the response starts. Transitioning &lt;code&gt;height&lt;/code&gt; or &lt;code&gt;width&lt;/code&gt; recalculates layout on every frame and shifts whatever sits below. If you want the panel to feel like it is opening, animate &lt;code&gt;transform&lt;/code&gt; instead. &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt; are the only two properties the compositor can handle without touching layout.&lt;/p&gt;

&lt;p&gt;The second is the typing indicator. A three-dot bubble that is inserted, then removed, then replaced by the answer causes two shifts on its own. Render it inside the space you already reserved, and swap its contents rather than its box.&lt;/p&gt;

&lt;h2&gt;
  
  
  INP belongs to the input box, not the model
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://web.dev/articles/inp" rel="noopener noreferrer"&gt;INP&lt;/a&gt; measures the delay between an interaction and the next frame the browser paints. It does not measure how long your model takes to answer. This distinction is worth internalising, because it means a slow model does not have to mean a bad INP score.&lt;/p&gt;

&lt;p&gt;What ruins INP is doing work on the main thread inside the event handler. Tokenising the prompt, building embeddings client-side, or serialising a large conversation history before the request goes out all block the frame.&lt;/p&gt;

&lt;p&gt;Keep the handler to the minimum that produces visible feedback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Paint first: echo the user's message and clear the input.&lt;/span&gt;
  &lt;span class="nf"&gt;appendMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;setInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Everything expensive happens after, off the interaction's critical path.&lt;/span&gt;
  &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendToModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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 user sees their message appear immediately. The request, the streaming, and the rendering of the reply all happen afterwards, and none of it is attributed to that interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch TTFB if the route is server-rendered
&lt;/h2&gt;

&lt;p&gt;If the page server-renders anything that depends on a model or a vector database, that work sits in front of your &lt;a href="https://web.dev/articles/ttfb" rel="noopener noreferrer"&gt;TTFB&lt;/a&gt;, and TTFB is inside LCP. A route that waits 400ms on a similarity search before it can send a byte has spent 400ms of an LCP budget that is roughly 2.5 seconds on mobile.&lt;/p&gt;

&lt;p&gt;The hero almost never needs that data. Render the shell immediately and let the dynamic part stream in or load client-side. In frameworks with streaming SSR, put the model-dependent region inside a boundary so the rest of the document flushes without waiting for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure in the field, not on your laptop
&lt;/h2&gt;

&lt;p&gt;Lab tools are useful for finding causes and useless for judging severity. Your development machine has a fast CPU, a warm cache, and a local server, which is the combination least likely to reproduce what a phone on a train experiences.&lt;/p&gt;

&lt;p&gt;Two habits are worth building:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Habit&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read field data, not lab scores&lt;/td&gt;
&lt;td&gt;LCP and CLS are scored at the 75th percentile of real visits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confirm which element is actually LCP&lt;/td&gt;
&lt;td&gt;It is often not the one you assumed, and optimising the wrong element wastes the work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second one catches more mistakes than any other check. You can log it directly:&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;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LCP element:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ms&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="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;largest-contentful-paint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buffered&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that on the deployed site, on a throttled connection, and see what it names. If it points at your chat transcript, the rest of this post is the work list. If it points at the headline, the hero is already shaped correctly and your budget is going somewhere else.&lt;/p&gt;

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

&lt;p&gt;An AI chat hero is worth building. It just cannot be the thing the browser is waiting on. Make static text the largest element, reserve the transcript's space before the first token lands, keep the submit handler down to visible feedback, and stream anything the model touches instead of blocking the document on it. The demo stays impressive and the metrics stop paying for it.&lt;/p&gt;

</description>
      <category>webvitals</category>
      <category>ai</category>
      <category>performance</category>
      <category>frontend</category>
    </item>
    <item>
      <title>React error #418: five hydration bugs I shipped</title>
      <dc:creator>Ganesh Joshi</dc:creator>
      <pubDate>Sun, 01 Nov 2026 15:43:35 +0000</pubDate>
      <link>https://dev.to/ganeshjoshi/react-error-418-five-hydration-bugs-i-shipped-ae3</link>
      <guid>https://dev.to/ganeshjoshi/react-error-418-five-hydration-bugs-i-shipped-ae3</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was created with AI assistance and reviewed for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If your production console shows &lt;code&gt;Minified React error #418&lt;/code&gt;, React is telling you that the HTML the server sent and the first render in the browser did not match. The development build spells it out ("Hydration failed because the server rendered HTML didn't match the client") and shows a diff. The production build gives you a number and a link to &lt;code&gt;react.dev/errors/418&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is easy to treat it as console noise, because the page still works. It is not free. When hydration fails, React throws away the server-rendered tree and renders the whole root again in the browser. On my site that one error took mobile Total Blocking Time from 0 to 420 ms and the Lighthouse performance score from 96 to 83, and pushed Largest Contentful Paint 1.2 seconds past First Contentful Paint, which had always been equal.&lt;/p&gt;

&lt;p&gt;I have shipped this error five different ways on one Next.js site. Here is each one, how it was found, and the fix that stuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Randomness or time in a &lt;code&gt;useState&lt;/code&gt; initializer
&lt;/h2&gt;

&lt;p&gt;The games on the site generate a puzzle, a word or a board. The obvious code seeds state when the component is created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong: the initializer runs on the server and again in the browser.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;puzzle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPuzzle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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;generatePuzzle&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The initializer runs twice, once during prerendering and once during hydration, and &lt;code&gt;Math.random()&lt;/code&gt; gives a different answer each time. The same thing happened in a lorem ipsum generator, a mock data generator and a password generator. Anything that reads &lt;code&gt;Date.now()&lt;/code&gt; or &lt;code&gt;new Date()&lt;/code&gt; while rendering has the same problem.&lt;/p&gt;

&lt;p&gt;The fix is a deterministic first value, with the random one set after mount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;puzzle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPuzzle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;INITIAL_PUZZLE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fixed, the same everywhere&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&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;setPuzzle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;generatePuzzle&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// runs only in the browser, after hydration&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 first frame shows the fixed puzzle for an instant. For a game that is fine. For anything where the flash matters, render a placeholder of the same size until the effect runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Formatting with the machine's locale
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;downloads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;time&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLocaleDateString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;time&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With no locale argument, &lt;code&gt;toLocaleString()&lt;/code&gt; uses whatever the current machine prefers. The server that prerenders the page (on Vercel, en-US in UTC) and a visitor in Germany or India produce different strings, so the text differs and hydration fails. It never reproduces on your own machine if your machine matches the server.&lt;/p&gt;

&lt;p&gt;Pass the locale you rendered with, or move the formatting after mount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;downloads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To catch this class of bug, load the deployed page in a browser with a different locale and time zone. In Playwright that is two options on the context:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&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;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;de-DE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timezoneId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;America/Los_Angeles&lt;/span&gt;&lt;span class="dl"&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. Browser-only APIs while rendering
&lt;/h2&gt;

&lt;p&gt;Five tools on the site called something during render that only exists, or only behaves the same, in a browser: &lt;code&gt;DOMParser&lt;/code&gt; in an XPath evaluator, &lt;code&gt;document&lt;/code&gt; in an SVG animation tool and an HTML-to-Pug converter, and &lt;code&gt;URL.createObjectURL&lt;/code&gt; in an SVG viewer. That last one is a good example of why this can hide: on the server it produced nothing useful, in the browser it produced a &lt;code&gt;blob:&lt;/code&gt; URL, and the visible symptom was a broken image, not an error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong: runs during render on the server too.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/svg+xml&lt;/span&gt;&lt;span class="dl"&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 fix is to compute these only after the component has mounted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useHasMounted&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMounted&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&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;setMounted&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;mounted&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;mounted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useHasMounted&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;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mounted&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/svg+xml&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="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="nx"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;svg&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;Remember to revoke the URL when nothing can reach it any more, but that is a separate bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;usePathname()&lt;/code&gt; in a prerendered component
&lt;/h2&gt;

&lt;p&gt;This is the one that cost the most, because I fixed it and then shipped it again.&lt;/p&gt;

&lt;p&gt;The site's navigation highlights the current page. The highlight compared the path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePathname&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;isActive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the deployed homepage this logged #418 on every visit. It never happened locally, with &lt;code&gt;next dev&lt;/code&gt; or with a local production build. The comparison for the &lt;code&gt;/&lt;/code&gt; link came out differently in Vercel's prerender of the index route than in the browser, so the active class differed between the server HTML and the first client render.&lt;/p&gt;

&lt;p&gt;The first time, I fixed it in one header. Later a different navigation design carried the same comparison into a new component, and it went live again. That second time is the one that took Lighthouse from 96 to 83.&lt;/p&gt;

&lt;p&gt;The fix that stuck is a shared hook that gates only the &lt;code&gt;/&lt;/code&gt; comparison on mount and leaves every other link server-rendered, so the other routes keep their active state from the first paint. Simplified, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useIsRouteActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePathname&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;mounted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useHasMounted&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;href&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&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;mounted&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&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;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&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="nx"&gt;href&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second part of the fix was making sure it could not come back. The repository has a small script that runs before every build and fails when a client component compares &lt;code&gt;usePathname()&lt;/code&gt; directly. A lint rule would do the same job. Writing the lesson down had not been enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A script that edits &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; before React gets there
&lt;/h2&gt;

&lt;p&gt;The site sets a colour scheme and, on festival days, a few &lt;code&gt;data-&lt;/code&gt; attributes on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; from an inline script, so the right look paints before any JavaScript bundle loads. React then hydrates &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;, finds attributes it did not render, and complains.&lt;/p&gt;

&lt;p&gt;This is the one case where &lt;code&gt;suppressHydrationWarning&lt;/code&gt; is the right tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="na"&gt;suppressHydrationWarning&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It only silences attribute and text differences on that one element, not its children, which is exactly the scope you want. Two rules keep it honest: only the element the script edits gets it (here &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;, not &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;), and the script only ever sets attributes, never content.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;suppressHydrationWarning&lt;/code&gt; is not a fix for the first four causes. Those are real differences in what renders, and hiding the warning leaves the re-render in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to find yours
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read the console on the deployed site,&lt;/strong&gt; not just locally. Three of these five never reproduced on my machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the development build once&lt;/strong&gt; to get the readable message and the diff React prints, which names the element and the two values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load the page with a foreign locale and time zone.&lt;/strong&gt; It finds cause 2 and anything else that reads the environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search the codebase&lt;/strong&gt; for &lt;code&gt;Math.random&lt;/code&gt;, &lt;code&gt;Date.now&lt;/code&gt;, &lt;code&gt;new Date(&lt;/code&gt;, &lt;code&gt;toLocale&lt;/code&gt;, &lt;code&gt;window.&lt;/code&gt;, &lt;code&gt;document.&lt;/code&gt;, &lt;code&gt;DOMParser&lt;/code&gt; and &lt;code&gt;usePathname&lt;/code&gt; inside components that render on the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common thread is simple: everything a component renders the first time must be computable on the server, from the same inputs, with the same answer. Anything that depends on the browser, the clock, chance or the visitor waits until after mount.&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Node Streams for Large RAG Uploads and Embedding Pipelines</title>
      <dc:creator>Ganesh Joshi</dc:creator>
      <pubDate>Sun, 01 Nov 2026 15:43:33 +0000</pubDate>
      <link>https://dev.to/ganeshjoshi/node-streams-for-large-rag-uploads-and-embedding-pipelines-248n</link>
      <guid>https://dev.to/ganeshjoshi/node-streams-for-large-rag-uploads-and-embedding-pipelines-248n</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was created with AI assistance and reviewed for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most retrieval pipelines start the same way. Read the uploaded file, extract its text, split it into chunks, embed the chunks, write them to the database. Written with &lt;code&gt;await readFile()&lt;/code&gt; at the top it is about fifteen lines, and it works perfectly until a customer uploads a document larger than the memory you have.&lt;/p&gt;

&lt;p&gt;The problem is not just peak memory. It is that a single large upload can take down the process handling everyone else's, and that failures arrive after you have already spent money embedding half the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the memory actually goes
&lt;/h2&gt;

&lt;p&gt;There are three places a naive pipeline buffers, and they compound:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// 1. whole file in memory&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;extractText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// 2. whole extracted text&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;splitIntoChunks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// 3. every chunk, at once&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;    &lt;span class="c1"&gt;// and every request in flight&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 400MB PDF can hold well over a gigabyte across those stages before a single embedding call returns. The final line is the worst of them: &lt;code&gt;Promise.all&lt;/code&gt; over ten thousand chunks issues ten thousand concurrent HTTP requests, which will exhaust sockets, trip provider rate limits, and produce a rejection that discards all the successful work alongside the failed call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streams turn peak memory into steady memory
&lt;/h2&gt;

&lt;p&gt;The stream version processes a window at a time, so memory tracks chunk size rather than file size:&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;pipeline&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;node:stream/promises&lt;/span&gt;&lt;span class="dl"&gt;'&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;createReadStream&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;node:fs&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="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;createReadStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;extractTextStream&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;      &lt;span class="c1"&gt;// Transform: bytes to text&lt;/span&gt;
  &lt;span class="nf"&gt;chunkStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;overlap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="nf"&gt;embedStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;concurrency&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;writeToDatabase&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 &lt;code&gt;pipeline&lt;/code&gt; from &lt;code&gt;node:stream/promises&lt;/code&gt; rather than chaining &lt;code&gt;.pipe()&lt;/code&gt;. It propagates errors through every stage and destroys the whole chain when one fails, which is the difference between a clean failure and a leaked file descriptor plus a half-written batch. Chained &lt;code&gt;.pipe()&lt;/code&gt; calls do neither.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backpressure is the part that matters
&lt;/h2&gt;

&lt;p&gt;Backpressure is the mechanism that makes this work, and it is easy to accidentally disable.&lt;/p&gt;

&lt;p&gt;When a writable stage cannot keep up, it stops accepting data, and that signal travels back up the chain until the file read pauses. Embedding is always the slowest stage in a RAG pipeline, by orders of magnitude: parsing is CPU-bound and fast, embedding is a network round trip. Backpressure is what stops the parser from racing ahead and queuing the entire document in front of the embedder.&lt;/p&gt;

&lt;p&gt;You lose it the moment a stage buffers internally:&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="c1"&gt;// Breaks backpressure: collects everything, then acts.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Preserves it: one item at a time, with await between.&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&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 &lt;code&gt;for await&lt;/code&gt; loop is the simplest correct consumer. It will not request the next chunk until the body has finished, so the upstream source stays paused while you work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Batch the embedding calls
&lt;/h2&gt;

&lt;p&gt;Embedding one chunk per request wastes most of the round trip. Every provider accepts arrays, and a batch of 50 to 100 costs roughly the same wall-clock time as a batch of one.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;batched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// the tail matters&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;batch&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;batched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&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;vectors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;embedMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vectors&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;Forgetting the final partial batch is a common bug, and a quiet one: every document silently loses its last few dozen chunks, which usually includes the conclusion.&lt;/p&gt;

&lt;p&gt;Insert as you go rather than accumulating vectors for a single write at the end. Embeddings are large, holding them all defeats the purpose of streaming, and incremental writes mean a failure at 80% keeps the first 80%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it resumable, because it will fail
&lt;/h2&gt;

&lt;p&gt;A long ingest will be interrupted. The process restarts, the network blips, the provider rate limits you. If a failure means starting over, you pay for every embedding twice.&lt;/p&gt;

&lt;p&gt;Record progress alongside the data:&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vectors&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lastChunkIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&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="nx"&gt;index&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On restart, skip to &lt;code&gt;lastChunkIndex&lt;/code&gt; and continue. This ordering matters: write the chunks first, then the progress marker. If it crashes between the two you re-embed one batch, which is cheap. Reverse the order and a crash loses that batch entirely, with the job convinced it succeeded.&lt;/p&gt;

&lt;p&gt;For rate limits specifically, retry the batch with exponential backoff rather than failing the job. A 429 is a request to wait, not an error condition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify with the number that matters
&lt;/h2&gt;

&lt;p&gt;The test for all of this is not whether it completes. It is whether memory stays flat.&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;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;mb&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="nf"&gt;memoryUsage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;heapUsed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`heap: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mb&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;MB`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your largest realistic document through and watch. A correct streaming pipeline holds roughly steady regardless of file size. If the line climbs with the document, something in the chain is still buffering, and the profile will tell you which stage by where the climb begins.&lt;/p&gt;

&lt;p&gt;That single graph is worth more than any amount of reasoning about the code, because every one of the mistakes above looks fine when you read it.&lt;/p&gt;

</description>
      <category>node</category>
      <category>rag</category>
      <category>ai</category>
      <category>streams</category>
    </item>
    <item>
      <title>“Mindblowing” AI Results? Read the Paper or Model Card First</title>
      <dc:creator>Ganesh Joshi</dc:creator>
      <pubDate>Sun, 01 Nov 2026 15:43:02 +0000</pubDate>
      <link>https://dev.to/ganeshjoshi/mindblowing-ai-results-read-the-paper-or-model-card-first-52nk</link>
      <guid>https://dev.to/ganeshjoshi/mindblowing-ai-results-read-the-paper-or-model-card-first-52nk</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was created with AI assistance and reviewed for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A striking benchmark number crosses your feed. The claim is usually accurate and the framing is usually wrong, because the number arrived without the conditions that produced it. Those conditions are in the paper, and reading for them takes about fifteen minutes.&lt;/p&gt;

&lt;p&gt;This is a practical skill rather than an academic one. You do not need to evaluate the methodology like a reviewer. You need to work out whether the result applies to what you are building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read it out of order
&lt;/h2&gt;

&lt;p&gt;Nobody has time to read papers linearly, and you do not need to. Four sections answer most questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Abstract&lt;/strong&gt;, for the claim as the authors state it. Compare this to the claim as it reached you. The difference is often the entire story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;, which is where the caveats live and is the section least likely to survive summarisation. Many venues now require it, which is a gift to anyone reading quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experimental setup&lt;/strong&gt;, for what was actually measured. This is where you find out that the evaluation used a specific subset, or allowed multiple attempts, or gave the model tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Results tables&lt;/strong&gt;, including the baselines. A large improvement over a weak baseline is a different result from a small improvement over a strong one.&lt;/p&gt;

&lt;p&gt;Introductions and related work are the least useful parts for this purpose. Skip them without guilt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The questions that change what a number means
&lt;/h2&gt;

&lt;p&gt;A few specifics account for most of the distance between a headline and reality:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many attempts?&lt;/strong&gt; A score where the model gets several tries and any success counts is not comparable to one where the first answer is the answer. Both are legitimate measurements of different things, and only one resembles a user typing a question once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What was the model given?&lt;/strong&gt; Tool access, retrieval, a scaffold that retries and self-corrects, or a carefully engineered prompt. All are reasonable in research and none are free in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which subset?&lt;/strong&gt; Evaluations often report on a filtered slice, sometimes for good reasons. The filtering criteria decide how much the number generalises.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contamination?&lt;/strong&gt; For benchmarks that predate the model's training data, some of the test set may have been in it. Good papers address this directly. Its absence is worth noticing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who is comparing?&lt;/strong&gt; A lab reporting its own model against competitors chose the evaluation conditions. That is not dishonest, and it is not neutral either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model cards deserve more attention than papers
&lt;/h2&gt;

&lt;p&gt;For anything you are considering using, the model card or system card is usually the higher-value read. It is written by the people who built the thing, it states intended use and known weaknesses, and it tends to be blunter about limitations than a launch post.&lt;/p&gt;

&lt;p&gt;It is also where you find the operational facts that determine whether you can build on it: context limits, supported modalities, safety behaviour, and the conditions under which performance degrades.&lt;/p&gt;

&lt;p&gt;The pattern worth internalising is that the marketing post and the model card describe the same system with very different emphases, and the second one is the one written for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wait for someone else to try it
&lt;/h2&gt;

&lt;p&gt;The strongest signal on a surprising result arrives weeks later, when independent groups attempt to reproduce it. Reproductions get a fraction of the attention of the original and carry considerably more information.&lt;/p&gt;

&lt;p&gt;There is usually no cost to waiting. If a technique genuinely works, it will still work in a month, and by then you will know whether it survives contact with someone who did not have an interest in the outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;You do not need to do this for everything. Match the effort to the decision.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Curiosity&lt;/td&gt;
&lt;td&gt;Read the abstract, move on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Might affect a roadmap decision&lt;/td&gt;
&lt;td&gt;Abstract, limitations, setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planning to build on it&lt;/td&gt;
&lt;td&gt;The above, plus the model card and any reproductions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Citing it to convince someone&lt;/td&gt;
&lt;td&gt;Read it properly, including baselines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the one worth being strict about. Repeating a number you have not verified attaches your credibility to someone else's summary, and when it turns out the evaluation allowed ten attempts, it is your recommendation that was wrong.&lt;/p&gt;

&lt;p&gt;None of this requires cynicism. Real progress happens constantly and is worth being excited about. It just means the excitement should come after the fifteen minutes rather than before, and that you can say where a claim came from when somebody asks.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>research</category>
      <category>science</category>
      <category>learning</category>
    </item>
    <item>
      <title>CSP and Third-Party AI Widgets: Copilot, Chat, and Scripts</title>
      <dc:creator>Ganesh Joshi</dc:creator>
      <pubDate>Sun, 01 Nov 2026 15:43:01 +0000</pubDate>
      <link>https://dev.to/ganeshjoshi/csp-and-third-party-ai-widgets-copilot-chat-and-scripts-46h6</link>
      <guid>https://dev.to/ganeshjoshi/csp-and-third-party-ai-widgets-copilot-chat-and-scripts-46h6</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was created with AI assistance and reviewed for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Adding a vendor's AI chat widget is a two-line change to your HTML and a much larger change to your security posture. The script runs with the same privileges as your own code: it can read the DOM, read &lt;code&gt;localStorage&lt;/code&gt;, and make requests as the logged-in user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP" rel="noopener noreferrer"&gt;Content Security Policy&lt;/a&gt; is how you bound that. The catch is that most widget install guides ask you to weaken exactly the directives that were doing the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The install instructions are the problem
&lt;/h2&gt;

&lt;p&gt;Vendor docs commonly suggest one of these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;script-src 'self' 'unsafe-inline' 'unsafe-eval' https://widget.vendor.com;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each addition removes a specific protection. &lt;code&gt;'unsafe-inline'&lt;/code&gt; allows any injected &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag to execute, which is the primary thing CSP exists to prevent. &lt;code&gt;'unsafe-eval'&lt;/code&gt; allows strings to be compiled into code, re-opening a class of injection that is otherwise dead. A wildcard host allows anything the vendor ever hosts, including a compromised subdomain.&lt;/p&gt;

&lt;p&gt;You can usually avoid all three. Treat the vendor's snippet as a starting point to be narrowed, not a requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nonces instead of unsafe-inline
&lt;/h2&gt;

&lt;p&gt;If the widget needs an inline bootstrap, give that specific script a nonce rather than allowing every inline script on the page:&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;// middleware.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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;csp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s2"&gt;`script-src 'self' 'nonce-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' https://widget.vendor.com`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`connect-src 'self' https://api.vendor.com`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`frame-src https://widget.vendor.com`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`img-src 'self' data: https://cdn.vendor.com`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`style-src 'self' 'nonce-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nonce&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="s2"&gt;`object-src 'none'`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`base-uri 'self'`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A nonce is generated per response and must therefore come from a dynamically rendered layer. That is a real constraint: a fully static page cannot carry a per-request nonce, so on prerendered routes you are choosing between hashes and moving the widget behind an iframe.&lt;/p&gt;

&lt;p&gt;Hashes work when the inline content is fixed. Compute the SHA-256 of the script body and allowlist that digest. It breaks the moment the vendor changes a byte, which is either a maintenance burden or an early warning, depending on your temperament.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know which directive each behaviour needs
&lt;/h2&gt;

&lt;p&gt;Widgets fail in confusing ways when one directive is missing, because the browser blocks silently from the page's perspective:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directive&lt;/th&gt;
&lt;th&gt;Governs&lt;/th&gt;
&lt;th&gt;Missing it looks like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;script-src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Loading the widget bundle&lt;/td&gt;
&lt;td&gt;Nothing renders at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connect-src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Its API calls and WebSockets&lt;/td&gt;
&lt;td&gt;Widget appears, never responds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frame-src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Widgets that render in an iframe&lt;/td&gt;
&lt;td&gt;Empty box where the panel should be&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;img-src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Avatars, logos, uploaded images&lt;/td&gt;
&lt;td&gt;Broken images inside the widget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;style-src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Injected stylesheets&lt;/td&gt;
&lt;td&gt;Unstyled widget, correct behaviour&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second row causes the most support tickets. The panel opens, the user types, and nothing comes back, because &lt;code&gt;connect-src&lt;/code&gt; is blocking the API host while &lt;code&gt;script-src&lt;/code&gt; allows the bundle. The two are frequently different domains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll it out in report-only first
&lt;/h2&gt;

&lt;p&gt;Never enforce a new policy blind. &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; applies the rules, reports what would have been blocked, and blocks nothing:&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;Content-Security-Policy-Report-Only: script-src 'self' https://widget.vendor.com; report-uri /api/csp-report
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it for long enough to cover real traffic, including the paths where the widget behaves differently. Then read the reports, add the origins you genuinely need, and only afterwards switch the header name to enforce.&lt;/p&gt;

&lt;p&gt;Expect noise. Browser extensions inject scripts into your pages and generate violations you cannot fix and should not act on. Filter reports by whether the blocked URI is something you recognise before treating any of it as a signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  A widget you cannot trust belongs in an iframe
&lt;/h2&gt;

&lt;p&gt;CSP limits where code comes from. It does not limit what that code does once it is running in your page.&lt;/p&gt;

&lt;p&gt;If the widget handles anything sensitive, or if you simply do not want a third party reading your DOM, put it in an iframe on a different origin. The browser's same-origin policy then does the isolation that CSP cannot: the widget cannot read your cookies, your &lt;code&gt;localStorage&lt;/code&gt;, or the contents of the page around it. Communication goes through &lt;code&gt;postMessage&lt;/code&gt;, where you control exactly what crosses the boundary.&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&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;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://widget.vendor.com&lt;/span&gt;&lt;span class="dl"&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;// check first&lt;/span&gt;
  &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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 origin check is not optional. Without it any page that can reach your frame can send it messages, and the handler will process them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it on the built site, not in development
&lt;/h2&gt;

&lt;p&gt;This is the part most teams get wrong. Development servers routinely relax CSP because bundler tooling needs &lt;code&gt;'unsafe-eval'&lt;/code&gt; for hot reloading. Which means development cannot tell you whether production will work.&lt;/p&gt;

&lt;p&gt;Verify against a production build, and listen for violations directly rather than reading the console by eye:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;securitypolicyviolation&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;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violatedDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blockedURI&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 event fires even when the failure is swallowed by a &lt;code&gt;try/catch&lt;/code&gt; inside the widget, which is common. A vendor bundle that catches its own network errors and renders an empty state gives you no console error at all, and that event is the only thing that tells you the policy was the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of a good outcome
&lt;/h2&gt;

&lt;p&gt;You should end with a policy that names every vendor origin explicitly, has no wildcards, no &lt;code&gt;'unsafe-eval'&lt;/code&gt;, and no blanket &lt;code&gt;'unsafe-inline'&lt;/code&gt;. It should be verified against a production build with a violation listener attached, and anything you genuinely cannot constrain should be sitting in a cross-origin iframe where it can only do damage to itself.&lt;/p&gt;

</description>
      <category>csp</category>
      <category>security</category>
      <category>ai</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Anthropic Model Names and Deprecations: Build a Changelog Habit</title>
      <dc:creator>Ganesh Joshi</dc:creator>
      <pubDate>Sun, 11 Oct 2026 15:18:59 +0000</pubDate>
      <link>https://dev.to/ganeshjoshi/anthropic-model-names-and-deprecations-build-a-changelog-habit-1jn9</link>
      <guid>https://dev.to/ganeshjoshi/anthropic-model-names-and-deprecations-build-a-changelog-habit-1jn9</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was created with AI assistance and reviewed for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every model provider retires models. The identifier you pass in the &lt;code&gt;model&lt;/code&gt; field is a version string with a lifecycle, and unlike your npm dependencies nothing in your toolchain tells you when it is about to end.&lt;/p&gt;

&lt;p&gt;The failure is undramatic and annoying. A string that has worked for a year starts returning errors, and the person on call has to find where it is configured, which turns out to be five places, three of them not in the application repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find every place the string lives
&lt;/h2&gt;

&lt;p&gt;Before anything else, find out how many there are. In most codebases the answer is more than expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"claude-"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.ts"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.env*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.yml"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.tf"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; node_modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ones that hurt are outside the application code: a Terraform variable, a CI secret, a dashboard config, a seed script, a colleague's local &lt;code&gt;.env&lt;/code&gt;. Each is a separate thing to update under time pressure, and the ones you forget fail later than the ones you find.&lt;/p&gt;

&lt;h2&gt;
  
  
  One module owns the mapping
&lt;/h2&gt;

&lt;p&gt;Collapse them into a single place that maps a role to an identifier, and have the application refer to roles:&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;// models.ts is the only file that names a model.&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;MODELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;chat&lt;/span&gt;&lt;span class="p"&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;MODEL_CHAT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;summarize&lt;/span&gt;&lt;span class="p"&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;MODEL_SUMMARIZE&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-haiku-4-5&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="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Everywhere else:&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&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;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&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;The environment variable allows an override without a deploy, which is what you want during an incident. The default in code means a missing variable does not produce an empty model field and a confusing error.&lt;/p&gt;

&lt;p&gt;Naming by role rather than by model is the part that pays off. &lt;code&gt;MODELS.summarize&lt;/code&gt; stays meaningful across three model generations; &lt;code&gt;CLAUDE_HAIKU_MODEL&lt;/code&gt; becomes a lie the first time you point it at something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version the choice deliberately
&lt;/h2&gt;

&lt;p&gt;Providers usually offer both a dated identifier and a moving alias. They fail in opposite directions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dated, e.g. &lt;code&gt;claude-haiku-4-5-20251001&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Frozen&lt;/td&gt;
&lt;td&gt;Sunsets on a schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alias, e.g. a latest-style pointer&lt;/td&gt;
&lt;td&gt;Moves&lt;/td&gt;
&lt;td&gt;Behaviour changes without a deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For production, the dated form is usually right, for the same reason you commit a lockfile: you want the thing you tested to be the thing that runs. The cost is that you own the upgrade, which is the point.&lt;/p&gt;

&lt;p&gt;Aliases are fine in development and evaluation environments, where discovering a change early is useful rather than alarming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subscribe, then check on a schedule
&lt;/h2&gt;

&lt;p&gt;Providers publish deprecation notices in documentation and changelogs, and send console or email notices to account owners. Two habits close the gap.&lt;/p&gt;

&lt;p&gt;Route provider notification emails to a shared inbox rather than an individual. Deprecation notices have a way of arriving while the person who set up the account is on holiday, or after they have left.&lt;/p&gt;

&lt;p&gt;Then check the deprecations page as a recurring task, roughly monthly. That sounds primitive next to automation, and it is more reliable than most automation, because a page layout change silently breaks a scraper while a calendar reminder does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the upgrade routine boring
&lt;/h2&gt;

&lt;p&gt;When a new identifier is available, the path should already exist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Point staging at the new model via the environment variable.&lt;/li&gt;
&lt;li&gt;Run your evaluation set against it and compare outputs to the current model.&lt;/li&gt;
&lt;li&gt;Check cost and latency, since both frequently change.&lt;/li&gt;
&lt;li&gt;Promote by changing the production variable.&lt;/li&gt;
&lt;li&gt;Update the default in code so the variable is no longer load-bearing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step two is the one that requires prior investment. If you do not have a stored set of representative inputs with known-good outputs, every model change is a leap, and the only feedback is user complaints a week later. A few dozen cases is enough to catch a regression in format, tone, or refusal behaviour.&lt;/p&gt;

&lt;p&gt;Cost and latency deserve their own check because they move independently of quality. A newer model that is better and three times the price is a product decision, not an automatic upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail loudly on an unknown model
&lt;/h2&gt;

&lt;p&gt;Add a startup assertion that the configured identifier is one you recognise:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KNOWN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;KNOWN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;configured&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unknown model: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;configured&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typo in an environment variable otherwise surfaces as a runtime API error on the first user request, in whichever code path happens to run first. Failing at boot puts the error in the deploy log, where someone is already watching.&lt;/p&gt;

&lt;p&gt;None of this is sophisticated. It is the same discipline you already apply to dependency versions, extended to a string that behaves exactly like one but arrived through a different door.&lt;/p&gt;

</description>
      <category>anthropic</category>
      <category>claude</category>
      <category>api</category>
      <category>devops</category>
    </item>
    <item>
      <title>Turborepo for AI Product Monorepos and Shared Pipelines</title>
      <dc:creator>Ganesh Joshi</dc:creator>
      <pubDate>Sun, 04 Oct 2026 14:35:58 +0000</pubDate>
      <link>https://dev.to/ganeshjoshi/turborepo-for-ai-product-monorepos-and-shared-pipelines-52nc</link>
      <guid>https://dev.to/ganeshjoshi/turborepo-for-ai-product-monorepos-and-shared-pipelines-52nc</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was created with AI assistance and reviewed for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AI products fragment across runtimes quickly. There is a web app, one or more background workers doing embedding or evaluation, sometimes an edge function, and underneath them shared packages holding prompts, model adapters, and schema definitions. Every one of those consumers needs the shared code, and every change to it should rebuild exactly the things affected and nothing else.&lt;/p&gt;

&lt;p&gt;That is what &lt;a href="https://turbo.build/repo/docs" rel="noopener noreferrer"&gt;Turborepo&lt;/a&gt; is for. Most of the value comes from getting two things right: the task graph and the cache inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Declare the graph, not the order
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;^&lt;/code&gt; prefix means "this task depends on the same task in my dependencies", which is what makes a package build before the app importing it:&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;"tasks"&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;"build"&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;"dependsOn"&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="s2"&gt;"^build"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&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="s2"&gt;".next/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"!.next/cache/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/**"&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;"test"&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;"dependsOn"&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="s2"&gt;"^build"&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;"lint"&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;"dependsOn"&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;"dev"&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;"cache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"persistent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;Two details are easy to get wrong. &lt;code&gt;lint&lt;/code&gt; genuinely has no dependencies, so declaring one serialises work that could run in parallel across every package. And &lt;code&gt;dev&lt;/code&gt; must be marked &lt;code&gt;persistent&lt;/code&gt; and uncached, otherwise Turborepo waits for a long-running process to exit before starting anything downstream.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;outputs&lt;/code&gt; list is what gets restored on a cache hit. Omit a directory and a "cached" build leaves you without files you needed. Excluding &lt;code&gt;.next/cache&lt;/code&gt; is deliberate: it is large, it is a cache of a cache, and storing it makes every hit slower to download than the build it replaced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment variables are cache inputs
&lt;/h2&gt;

&lt;p&gt;This is the mistake specific to AI projects, and it produces genuinely confusing failures.&lt;/p&gt;

&lt;p&gt;Turborepo hashes inputs to decide whether a task can be restored from cache. Environment variables are not included unless you declare them. So if a build embeds a model name or an API base URL, and you change that variable, Turborepo sees identical inputs and hands back the old artifact.&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;"tasks"&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;"build"&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;"env"&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="s2"&gt;"MODEL_NAME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EMBEDDING_MODEL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NEXT_PUBLIC_*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&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="s2"&gt;"^build"&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="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;The symptom is a deploy that changes nothing. You switch the model, the build succeeds, and production keeps using the old one because the artifact was restored rather than rebuilt.&lt;/p&gt;

&lt;p&gt;The reverse mistake matters too. Declaring a variable that changes on every run, such as a build timestamp or a CI job id, means the hash never matches and you never get a cache hit at all. If your hit rate is zero, this is almost always why.&lt;/p&gt;

&lt;p&gt;Secrets should not be build inputs in the first place. An API key belongs in the runtime environment, not baked into an artifact, and adding it to &lt;code&gt;env&lt;/code&gt; implies it can change the build output, which is a design you do not want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote caching is the point in CI
&lt;/h2&gt;

&lt;p&gt;Local caching helps one developer. Remote caching means a build produced on someone's laptop is reused by CI, and a build from CI is reused by everyone else.&lt;/p&gt;

&lt;p&gt;For an AI monorepo where the web app and three workers all depend on the same prompt package, this is the difference between rebuilding four times and once.&lt;/p&gt;

&lt;p&gt;Treat the cache token as a credential with write access to your build outputs. A leaked token that can write to the cache lets an attacker poison an artifact that your deploy then trusts. Give CI a token scoped to what it needs, and prefer read-only tokens anywhere that does not need to publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope commands so you are not building everything
&lt;/h2&gt;

&lt;p&gt;The filter flag is what keeps day-to-day work fast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;turbo build &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;web                &lt;span class="c"&gt;# just the web app and its deps&lt;/span&gt;
turbo build &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...[origin/main]   &lt;span class="c"&gt;# only what changed since main&lt;/span&gt;
turbo &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@repo/prompts...    &lt;span class="c"&gt;# the package and everything depending on it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last form is the one worth internalising. A trailing &lt;code&gt;...&lt;/code&gt; means "and everything downstream", so after changing a shared prompt package you can test exactly the consumers that could be affected. That is the correct blast radius for a change to shared AI logic, where a prompt tweak can alter behaviour in three services at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep model adapters in their own package
&lt;/h2&gt;

&lt;p&gt;A structural recommendation that pays off repeatedly: put the code that talks to model providers in a single package with a stable interface, and let every app depend on that rather than calling providers directly.&lt;/p&gt;

&lt;p&gt;It gives you one place to add a new provider, one place to change timeout handling, and one place that fails type-checking when a response shape changes. In a monorepo without this, provider details leak into four codebases and a provider migration becomes four migrations.&lt;/p&gt;

&lt;p&gt;It also makes the cache work in your favour. Changing a worker does not invalidate the web app, and changing the adapter invalidates exactly the things that use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the summary when something feels wrong
&lt;/h2&gt;

&lt;p&gt;When a build is slower than expected or a change did not take effect, ask Turborepo what it decided:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;turbo build &lt;span class="nt"&gt;--summarize&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That writes a file listing every task, whether it hit the cache, and the hash inputs used. Comparing two summaries shows precisely which input changed, which is far faster than guessing at why a task rebuilt or, more often, why it did not.&lt;/p&gt;

</description>
      <category>turborepo</category>
      <category>monorepo</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>Feature Flags for A/B Testing AI Models and Prompts</title>
      <dc:creator>Ganesh Joshi</dc:creator>
      <pubDate>Sun, 27 Sep 2026 14:32:13 +0000</pubDate>
      <link>https://dev.to/ganeshjoshi/feature-flags-for-ab-testing-ai-models-and-prompts-2k8g</link>
      <guid>https://dev.to/ganeshjoshi/feature-flags-for-ab-testing-ai-models-and-prompts-2k8g</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was created with AI assistance and reviewed for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Changing the model behind a feature is a deploy-sized decision dressed up as a config change. The output shape shifts, the latency profile shifts, the cost per request shifts, and none of it shows up in your test suite because the test suite does not call the model.&lt;/p&gt;

&lt;p&gt;Feature flags are the right tool, but AI rollouts stress them in ways ordinary flags do not. The main differences are that the thing you are testing is non-deterministic, the quality signal is delayed, and a bad variant can be expensive rather than just broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluate on the server, without exception
&lt;/h2&gt;

&lt;p&gt;Model selection must never be decided by client code. A flag evaluated in the browser can be read and changed by anyone, which means the caller chooses which model you pay for.&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/chat/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;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSession&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variant&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;flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chat-model&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="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&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;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;control&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// fall back on unknown&lt;/span&gt;
  &lt;span class="c1"&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 fallback on an unknown variant matters. Flag services fail, configs get edited, and a variant name can disappear while sessions still reference it. Defaulting to the control means the worst case is no experiment, rather than an exception in your main request path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep experiments and entitlements apart
&lt;/h2&gt;

&lt;p&gt;These get conflated because both are "flags", and the consequences differ enormously.&lt;/p&gt;

&lt;p&gt;An experiment decides which of two equivalent implementations a user gets. It can be reassigned, ramped, and rolled back freely. An entitlement decides what someone has paid for. It is authorisation, and it belongs with your billing logic, checked server-side on every request.&lt;/p&gt;

&lt;p&gt;The failure is putting a paid capability behind an experiment flag. Ramp percentages then control who gets a feature they bought, and a rollback removes it from paying customers. Different systems, or at minimum different code paths with different review requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assign stably, or the data is noise
&lt;/h2&gt;

&lt;p&gt;A user reassigned between variants mid-session gets a conversation where the model changes underneath them. That produces incoherent behaviour and contaminates your results.&lt;/p&gt;

&lt;p&gt;Hash a stable identifier so assignment is deterministic:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;variantFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&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="nx"&gt;salt&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="nx"&gt;userId&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;digest&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="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readUInt32BE&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="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;split&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;treatment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;control&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;Include the experiment name in the salt. Without it, the same users land in the treatment group of every experiment you run, and effects compound invisibly.&lt;/p&gt;

&lt;p&gt;For conversational products, consider pinning to the conversation rather than the user, so a single thread always uses one model even if the user's assignment later changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide what "better" means before you start
&lt;/h2&gt;

&lt;p&gt;This is where AI experiments differ most from ordinary ones. There is no click-through rate for "gave a good answer", and the honest signals are indirect.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Reads as&lt;/th&gt;
&lt;th&gt;Caveat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Explicit thumbs up or down&lt;/td&gt;
&lt;td&gt;Quality&lt;/td&gt;
&lt;td&gt;Very low response rate, skewed to extremes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regeneration rate&lt;/td&gt;
&lt;td&gt;Dissatisfaction&lt;/td&gt;
&lt;td&gt;Also rises when responses are slow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversation length&lt;/td&gt;
&lt;td&gt;Engagement or struggle&lt;/td&gt;
&lt;td&gt;Ambiguous on its own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task completion&lt;/td&gt;
&lt;td&gt;Real success&lt;/td&gt;
&lt;td&gt;Only measurable if the task has an endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost and latency per request&lt;/td&gt;
&lt;td&gt;Operational fit&lt;/td&gt;
&lt;td&gt;Unambiguous, measure always&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pick the primary metric before launching. Choosing afterwards from whatever moved is how a worse model gets promoted on the strength of a metric that happened to rise.&lt;/p&gt;

&lt;p&gt;Cost and latency are worth tracking on every variant regardless, because they are the two that are never ambiguous and they frequently decide the question on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log the assignment with the output
&lt;/h2&gt;

&lt;p&gt;Every request should record which variant served it, alongside the tokens used, the latency, and any feedback that arrives later.&lt;/p&gt;

&lt;p&gt;Without that join, you cannot attribute anything. A week into the experiment someone asks whether the treatment group's costs went up, and if the variant is not on the usage record, the answer is unavailable and the experiment was wasted.&lt;/p&gt;

&lt;p&gt;Treat prompt text in these logs carefully. It is user content, and an experiment log is an easy place for it to end up with looser retention than the rest of your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the kill switch first
&lt;/h2&gt;

&lt;p&gt;Ramp deliberately: internal users, then a small percentage, then wider. But the ramp matters less than the ability to stop.&lt;/p&gt;

&lt;p&gt;A kill switch has to be a config change that takes effect immediately, with no deploy and no cache to wait out. Test that it works before you need it, and make sure whoever is on call can operate it without a code review.&lt;/p&gt;

&lt;p&gt;The scenarios it exists for are specific and worth naming: a variant is producing unsafe output, a provider incident makes one path unusable, or the cost per request turns out to be several times the control. All three are discovered in production, and in all three the time between noticing and stopping is the entire cost of the mistake.&lt;/p&gt;

</description>
      <category>featureflags</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>experimentation</category>
    </item>
    <item>
      <title>What to Check in an AI Coding Tool's Privacy Policy</title>
      <dc:creator>Ganesh Joshi</dc:creator>
      <pubDate>Sun, 20 Sep 2026 13:25:47 +0000</pubDate>
      <link>https://dev.to/ganeshjoshi/what-to-check-in-an-ai-coding-tools-privacy-policy-26dl</link>
      <guid>https://dev.to/ganeshjoshi/what-to-check-in-an-ai-coding-tools-privacy-policy-26dl</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was created with AI assistance and reviewed for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Does this tool upload my code?" is the wrong question, because the answer for every cloud-backed assistant is yes, at least sometimes. Completions require context, and context is your code.&lt;/p&gt;

&lt;p&gt;The useful questions are narrower: which code, how long is it kept, who else sees it, is it used for training, and which of those answers change depending on a setting somebody can toggle.&lt;/p&gt;

&lt;p&gt;This applies to any assistant. Vendor policies change, so what follows is a checklist to run against the current documentation rather than a summary of any particular product's terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually leaves the machine
&lt;/h2&gt;

&lt;p&gt;Start here, because it is usually broader than developers assume. Beyond the file you are editing, an assistant may send surrounding files, imports it resolved, repository structure, and in agent modes the output of commands it ran.&lt;/p&gt;

&lt;p&gt;That last category is the one that catches people. If the tool can run tests or a build, it can capture whatever those commands print, and build output routinely contains environment variables, connection strings, and internal hostnames.&lt;/p&gt;

&lt;p&gt;Also worth establishing: does an indexing feature upload the whole repository, or only embeddings of it? Those are meaningfully different exposures, and both are usually described somewhere in the documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five things to find in the policy
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Is my code used to train models?&lt;/td&gt;
&lt;td&gt;Usually configurable, and frequently defaults differently on personal and business plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How long is it retained?&lt;/td&gt;
&lt;td&gt;"Not trained on" and "not stored" are different claims&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which subprocessors receive it?&lt;/td&gt;
&lt;td&gt;Your code reaches the model provider too, under their terms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where is it processed?&lt;/td&gt;
&lt;td&gt;Determines which jurisdiction's law applies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What is logged for abuse monitoring?&lt;/td&gt;
&lt;td&gt;Often a separate retention path with a longer window&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The subprocessor list is the one most often skipped and it is where the real answer lives. A tool that does not train on your code may still route it to a model provider that has its own retention policy. Read that provider's terms too, since it is the party actually holding the data.&lt;/p&gt;

&lt;p&gt;Abuse-monitoring retention deserves specific attention. Providers commonly keep request data for some period for safety review even when the product tier promises no training and short retention. That is a defensible practice and it is a different number from the one in the marketing table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the defaults, then check them again after an update
&lt;/h2&gt;

&lt;p&gt;Almost every tool has a privacy mode that reduces or eliminates retention. Three things are worth verifying rather than assuming.&lt;/p&gt;

&lt;p&gt;Whether it is on by default, and specifically whether that differs between free, pro, and enterprise tiers. Whether it applies to every feature, since a tool can honour it for completions and not for a newer agent mode. And whether it survives updates, because settings do occasionally reset or a new feature ships with its own separate toggle.&lt;/p&gt;

&lt;p&gt;The practical version of this: after any major version update, re-open the privacy settings and look. It takes a minute and it catches the case where a new capability arrived with a permissive default.&lt;/p&gt;

&lt;h2&gt;
  
  
  For team and enterprise use
&lt;/h2&gt;

&lt;p&gt;If the tool is being approved for a team rather than an individual, a few additional items belong on the list.&lt;/p&gt;

&lt;p&gt;Whether an administrator can enforce the privacy setting centrally, rather than trusting each developer to configure it. Whether the vendor offers a data processing agreement, which you will need if any regulated data can appear in your codebase. Whether attestations such as SOC 2 are available on request, if your own contracts require them. And what the deletion path is, both for an individual leaving and for terminating the contract.&lt;/p&gt;

&lt;p&gt;Ask specifically whether deletion covers backups and logs, and on what timeline. "Deleted from our systems" and "deleted from our backups within 90 days" are both reasonable answers, and only one of them is what most people assume they are getting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep a dated record
&lt;/h2&gt;

&lt;p&gt;When tooling is approved, save a dated copy of the policy as it stood, not just a link. Policies are updated without notice, and if a question arises later about what was agreed, a live URL cannot tell you what it said at the time.&lt;/p&gt;

&lt;p&gt;A PDF or an archive link with the date is enough. This is unglamorous and it is exactly what an auditor or a customer security questionnaire will ask for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule underneath all of it
&lt;/h2&gt;

&lt;p&gt;Do not infer policy from forum posts, changelogs, or someone's confident summary, including this one. Read the vendor's current pages, note the date you read them, and re-check when the tool updates significantly.&lt;/p&gt;

&lt;p&gt;The tools are genuinely useful and this is not an argument against using them. It is an argument for knowing what you agreed to, which takes half an hour once and is difficult to reconstruct later.&lt;/p&gt;

</description>
      <category>cursor</category>
      <category>privacy</category>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>Redis Rate Limits for LLM API Keys and Tenant Quotas</title>
      <dc:creator>Ganesh Joshi</dc:creator>
      <pubDate>Sun, 13 Sep 2026 13:37:35 +0000</pubDate>
      <link>https://dev.to/ganeshjoshi/redis-rate-limits-for-llm-api-keys-and-tenant-quotas-3o02</link>
      <guid>https://dev.to/ganeshjoshi/redis-rate-limits-for-llm-api-keys-and-tenant-quotas-3o02</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was created with AI assistance and reviewed for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Rate limiting a normal API protects your servers. Rate limiting an LLM API protects your bank account. The difference matters, because it changes what you are counting and where the limit has to live.&lt;/p&gt;

&lt;p&gt;A conventional web endpoint costs you CPU time measured in milliseconds. A single model call can cost real money, and an agent loop can issue hundreds of them without a human ever clicking anything. One customer leaving a misconfigured retry loop running overnight is a bill, not a blip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count tokens, not just requests
&lt;/h2&gt;

&lt;p&gt;The instinct is to limit requests per minute. That is necessary but not sufficient, because requests vary in cost by three orders of magnitude. A one-line prompt and a 200-page document summarisation are both "one request".&lt;/p&gt;

&lt;p&gt;Track both, and enforce whichever binds first:&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;// Two windows, different units, same tenant.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`rl:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:req`&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;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;        &lt;span class="c1"&gt;// 60 requests / minute&lt;/span&gt;
  &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`rl:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:tok`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// 200k tokens / hour&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request limit stops runaway loops quickly. The token limit is what actually caps spend. You need the second one before you need the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key design is the whole security model
&lt;/h2&gt;

&lt;p&gt;Every limit key must contain the tenant identifier, and that identifier must come from your authenticated session, never from the request body.&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;// Correct: identity comes from the verified session.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`rl:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&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="nx"&gt;operation&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="c1"&gt;// Wrong: caller controls their own bucket, so they control their own limit.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`rl:&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&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="nx"&gt;operation&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second version is not a rate limit. Anyone who reads their own network traffic can send a different tenant id and get a fresh quota, or send someone else's and exhaust theirs.&lt;/p&gt;

&lt;p&gt;Include the operation in the key too. Embedding calls and chat completions have wildly different cost profiles, and a shared bucket means a bulk embedding job starves interactive users of the quota they are paying for.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sliding window that does not drift
&lt;/h2&gt;

&lt;p&gt;Fixed windows have a well-known flaw: a caller can send a full quota at 11:59:59 and another full quota at 12:00:00, which is double the intended rate across a two-second span.&lt;/p&gt;

&lt;p&gt;A sorted set gives you a true sliding window. Each request is a member scored by timestamp; expired entries are trimmed on read.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- KEYS[1] window key, ARGV: now_ms, window_ms, limit, member&lt;/span&gt;
&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ZREMRANGEBYSCORE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&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;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARGV&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="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ARGV&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="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ZCARD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARGV&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="k"&gt;then&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ZADD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&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="n"&gt;ARGV&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="n"&gt;ARGV&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PEXPIRE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&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="n"&gt;ARGV&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;used&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this as a Lua script matters. The trim, the count, and the insert have to be one atomic unit, otherwise two concurrent requests can both read &lt;code&gt;used = limit - 1&lt;/code&gt; and both proceed. Redis executes a script without interleaving other commands, which closes that race without a distributed lock.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;PEXPIRE&lt;/code&gt; on every call is deliberate. It means idle tenants expire out of memory on their own, so your key count tracks active tenants rather than every tenant you have ever had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserve before the call, reconcile after
&lt;/h2&gt;

&lt;p&gt;Token counts create an ordering problem: you cannot know the true cost until the response comes back, but you have to decide before sending it.&lt;/p&gt;

&lt;p&gt;Reserve an estimate up front, then correct it:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;estimate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;countPromptTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;maxOutputTokens&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;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`rl:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:tok`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QuotaExceeded&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;res&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;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Give back what was reserved but not used.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputTokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputTokens&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`rl:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:tok`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;estimate&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reserving &lt;code&gt;maxOutputTokens&lt;/code&gt; is pessimistic on purpose. Most responses come in well under the ceiling, and the refund returns the difference. The alternative, charging only actual usage after the fact, lets a tenant exceed their quota by the size of whatever is in flight when they hit the limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail closed on spend, open on latency
&lt;/h2&gt;

&lt;p&gt;When Redis is unreachable you have to choose, and the right answer differs by limit type.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;th&gt;Redis down&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requests per minute&lt;/td&gt;
&lt;td&gt;Allow&lt;/td&gt;
&lt;td&gt;Protects servers, and your servers are fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token or spend quota&lt;/td&gt;
&lt;td&gt;Deny&lt;/td&gt;
&lt;td&gt;Protects money, and money does not recover&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An abuse-prevention limit failing open for thirty seconds is an acceptable risk. A spend cap failing open for thirty seconds during an agent loop is a genuinely expensive outage. Encode the difference rather than applying one blanket policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the limiter's own latency
&lt;/h2&gt;

&lt;p&gt;Every model call now waits on a Redis round trip first. That is cheap, but it is not free, and it sits directly in your user-facing path.&lt;/p&gt;

&lt;p&gt;Two things keep it cheap. Put Redis in the same region as the service calling it, because a cross-region check can cost more than the limit saves. And pipeline the request and token checks into a single round trip rather than issuing them serially, as in the &lt;code&gt;Promise.all&lt;/code&gt; above.&lt;/p&gt;

&lt;p&gt;Measure the p99 of the check itself, separately from the model call. If it drifts upward, the usual cause is a sorted set that has grown large because the window is long and the traffic is heavy. A one-hour window on a busy tenant holds every request in memory for the full hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to build first
&lt;/h2&gt;

&lt;p&gt;Start with a per-tenant token quota on an hourly window, failing closed. That single control stops the failure mode that actually hurts, which is unbounded spend from one customer. Add per-operation buckets when a bulk job first starves your interactive traffic, and add request-per-minute limits when you see abuse rather than before.&lt;/p&gt;

&lt;p&gt;Everything here depends on the key containing an identity the caller cannot choose. Get that right and the rest is tuning.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>ai</category>
      <category>api</category>
      <category>saas</category>
    </item>
    <item>
      <title>Where to Read Real AI Lab News: Anthropic, OpenAI, Google DeepMind</title>
      <dc:creator>Ganesh Joshi</dc:creator>
      <pubDate>Sun, 06 Sep 2026 12:34:59 +0000</pubDate>
      <link>https://dev.to/ganeshjoshi/where-to-read-real-ai-lab-news-anthropic-openai-google-deepmind-33d</link>
      <guid>https://dev.to/ganeshjoshi/where-to-read-real-ai-lab-news-anthropic-openai-google-deepmind-33d</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was created with AI assistance and reviewed for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The gap between what a lab announced and what circulates about it is usually not misinformation. It is compression. A blog post says a capability is rolling out to a subset of users in some regions during a preview, and by the third repost it is "now available".&lt;/p&gt;

&lt;p&gt;That distinction rarely matters when you are reading casually. It matters a great deal when you are deciding what to build, because you can spend a sprint on something your users cannot access yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two source types that answer different questions
&lt;/h2&gt;

&lt;p&gt;Announcements tell you what a lab wants to communicate. Changelogs tell you what actually changed in the API you are calling. Both are official, and they answer questions you should not confuse.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want to know&lt;/th&gt;
&lt;th&gt;Read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Is there a new capability, and what is it for&lt;/td&gt;
&lt;td&gt;The announcement or research post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did the endpoint I depend on change&lt;/td&gt;
&lt;td&gt;The API changelog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What are the documented limits and failure modes&lt;/td&gt;
&lt;td&gt;The model card or system card&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is a model I use going away&lt;/td&gt;
&lt;td&gt;The deprecations page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For day-to-day engineering, the changelog and the deprecations page are worth more than the announcement. They are also the two nobody subscribes to.&lt;/p&gt;

&lt;p&gt;A model card is the most underused document in the category. It typically states the evaluation setup, known weaknesses, and intended use, and it is written by the same people making the claim. Reading it takes ten minutes and answers most of the questions a launch post leaves open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three questions that resolve most of the confusion
&lt;/h2&gt;

&lt;p&gt;When a capability is announced, the practical details are almost always in the post and almost always omitted from summaries:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who can use it now?&lt;/strong&gt; General availability, limited preview, waitlist, or a specific usage tier. This single distinction accounts for most of the disappointment when a team tries to use something they read about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where does it work?&lt;/strong&gt; Region availability differs by product and by model, and an enterprise agreement can restrict it further.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the actual price?&lt;/strong&gt; Announcements frequently quote a headline rate that applies to one model in one configuration. The pricing page is the source, and cached or batch rates often differ substantially from the number in the post.&lt;/p&gt;

&lt;p&gt;If a summary does not answer all three, it has not told you enough to act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aggregators are for discovery, not for detail
&lt;/h2&gt;

&lt;p&gt;Social feeds and newsletters are good at telling you something happened. They are unreliable for the specifics, not because people are careless but because a headline cannot carry a limitations section.&lt;/p&gt;

&lt;p&gt;The habit worth building is a hard split: use feeds to find out that something exists, then follow the link before forming a view. If there is no link to a primary source, treat the claim as unconfirmed. That single rule filters a surprising amount.&lt;/p&gt;

&lt;p&gt;Benchmark numbers deserve particular suspicion when they arrive without context. A score is meaningless without the evaluation setup, and the difference between a model tested with tool access and without, or with several attempts and one, can be larger than the difference between models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it a routine, not a feed
&lt;/h2&gt;

&lt;p&gt;The version of this that survives contact with a busy week is small:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Subscribe the team inbox to the API changelogs of every provider you depend on. Not an individual's inbox.&lt;/li&gt;
&lt;li&gt;Check deprecations monthly. Put it in the calendar.&lt;/li&gt;
&lt;li&gt;When something looks relevant, read the model card before the announcement.&lt;/li&gt;
&lt;li&gt;Write a two-line note in a shared doc: what changed, and whether it affects anything you run.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That fourth step is what turns reading into knowledge the team keeps. It also creates a record you can search when someone asks in six months why a particular decision was made.&lt;/p&gt;

&lt;p&gt;Rotating who does it weekly spreads both the work and the context, and stops the whole team depending on one person's algorithmic feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Independent evaluation is worth waiting for
&lt;/h2&gt;

&lt;p&gt;For anything genuinely surprising, the useful signal often arrives a few weeks later, when other groups have tried to reproduce it. Replication attempts and critical write-ups are less visible than the original announcement and considerably more informative about whether a result holds up outside the conditions it was measured in.&lt;/p&gt;

&lt;p&gt;There is rarely a cost to waiting. The capability will still be there in three weeks, and by then you will know whether it does what the headline said.&lt;/p&gt;

&lt;p&gt;The engineering version of this discipline is simple: cite what you actually read, and be able to point at where a claim came from. It costs a few minutes per decision and it is the difference between a team that knows what it is building on and one repeating a summary of a summary.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>news</category>
      <category>anthropic</category>
      <category>research</category>
    </item>
    <item>
      <title>Vetted Access Programs: Frontier AI Security</title>
      <dc:creator>Ganesh Joshi</dc:creator>
      <pubDate>Sun, 16 Aug 2026 09:23:41 +0000</pubDate>
      <link>https://dev.to/ganeshjoshi/vetted-access-programs-frontier-ai-security-4360</link>
      <guid>https://dev.to/ganeshjoshi/vetted-access-programs-frontier-ai-security-4360</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This post was created with AI assistance and reviewed for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unrestricted models like Mythos 5 are not gone. They are just locked behind vetting walls. If you want to use them for research, you must pass a compliance audit. We looked into the requirements.&lt;/p&gt;

&lt;p&gt;To qualify for vetted access, your team must undergo a background check. You must document your security practices. You must prove you can store API keys in hardware security modules.&lt;/p&gt;

&lt;p&gt;The process is slow. It takes weeks of paperwork and meetings. You need to prove your organization has a clean compliance history.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Submit organization registration and tax status documents.&lt;/li&gt;
&lt;li&gt;Complete a cybersecurity compliance questionnaire.&lt;/li&gt;
&lt;li&gt;Sign an agreement with strict export-control clauses.&lt;/li&gt;
&lt;li&gt;Establish secure VPN tunnels to the provider's dedicated endpoints.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This vetting is the future of frontier AI development. We must adapt to these hurdles. Start hardening your API security practices today if you expect to use advanced models in the future.&lt;/p&gt;

</description>
      <category>security</category>
      <category>compliance</category>
      <category>ai</category>
      <category>audit</category>
    </item>
  </channel>
</rss>
