<?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: Carlos José Castro Galante</title>
    <description>The latest articles on DEV Community by Carlos José Castro Galante (@carlosjcastrog).</description>
    <link>https://dev.to/carlosjcastrog</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%2F3850137%2F7c4306aa-d4a6-4d9d-9327-a2b882c9d13d.jpeg</url>
      <title>DEV Community: Carlos José Castro Galante</title>
      <link>https://dev.to/carlosjcastrog</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/carlosjcastrog"/>
    <language>en</language>
    <item>
      <title>The Bug That Kept Coming Back in Framer Motion</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Sat, 22 Aug 2026 16:46:56 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/the-bug-that-kept-coming-back-in-framer-motion-2kmh</link>
      <guid>https://dev.to/carlosjcastrog/the-bug-that-kept-coming-back-in-framer-motion-2kmh</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Entiscore is an agent that audits a website's digital entity, checking schema markup, identity consistency, authority signals, and technical accessibility before returning a scored report. Somewhere along the way I decided the report needed some animation to it, scroll-triggered entrance animations built with Framer Motion's &lt;code&gt;whileInView&lt;/code&gt;, a blur-to-focus reveal for cards, and staggered timing for lists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptoms
&lt;/h2&gt;

&lt;p&gt;The first time it happened, the Hero title on the landing page got stuck mid-animation, permanently blurred, permanently offset, frozen in its &lt;code&gt;hidden&lt;/code&gt; state with no way to recover. A hard refresh fixed it but scrolling did nothing, and it just sat there broken until the page reloaded.&lt;/p&gt;

&lt;p&gt;I fixed it, or so I thought.&lt;/p&gt;

&lt;p&gt;A few days later a different section broke the exact same way, with four feature cards on the homepage stuck in a half-rendered blur that never resolved to their final state. Same symptom, different component. I fixed that one too and moved on.&lt;/p&gt;

&lt;p&gt;Then it happened a third time, in the actual product report, in the "Evaluation by axis" section, which is the part of the UI a judge would actually look at during a demo. Three different components, three apparent bugs, the exact same failure mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The investigation
&lt;/h2&gt;

&lt;p&gt;Each individual fix had worked in isolation, which was the trap. I kept treating the symptom as local, this specific card's animation is broken so I'd rewrite its transition and move on, and that approach papers over the actual defect instead of finding it.&lt;/p&gt;

&lt;p&gt;The question I should have asked the first time was what these three components had in common. The answer was a shared helper that looked like this:&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;getVariants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;motionSafe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Variants&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Variants&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;motionSafe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;opacity&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="na"&gt;visible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;opacity&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="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;base&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;Called inline, inside the component body, on every single render:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;motion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;
  &lt;span class="nx"&gt;variants&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;getVariants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;motionSafe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blurReveal&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
  &lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;whileInView&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;viewport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Root cause
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;getVariants&lt;/code&gt; returns a brand-new object reference every time it runs, even when the underlying values are identical. Framer Motion tracks the &lt;code&gt;variants&lt;/code&gt; prop by reference rather than by deep equality, so on every re-render it received what looked like a completely new set of animation variants.&lt;/p&gt;

&lt;p&gt;Combined with &lt;code&gt;viewport={{ once: true }}&lt;/code&gt;, that created a serious problem. The transition to &lt;code&gt;"visible"&lt;/code&gt; needs to be triggered by the intersection observer callback, but if a re-render swaps out the variants object mid-transition or right as the observer fires, the animation state and the variants object fall out of sync. The component ends up holding a &lt;code&gt;visible&lt;/code&gt; state that points to a variants object that no longer matches what's actually being interpolated, and because &lt;code&gt;once: true&lt;/code&gt; means the trigger only fires a single time, there's no second chance to self-correct.&lt;/p&gt;

&lt;p&gt;The Hero title, the feature cards, and the axis evaluation cards weren't three separate bugs but the same defect hit three separate times, because the anti-pattern lived in one function and got reused everywhere Framer Motion needed a reduced-motion fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Once I stopped treating the problem as three isolated incidents and searched the entire codebase for every call site of &lt;code&gt;getVariants&lt;/code&gt; and its sibling &lt;code&gt;getStaggerVariants&lt;/code&gt;, the fix itself was straightforward:&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;CARD_VARIANTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Variants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;opacity&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="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blur(4px)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;visible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;opacity&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="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blur(0px)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&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="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REDUCED_MOTION_VARIANTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Variants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;opacity&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="na"&gt;visible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;opacity&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="p"&gt;};&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;motion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;
  &lt;span class="nx"&gt;variants&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;motionSafe&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;CARD_VARIANTS&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REDUCED_MOTION_VARIANTS&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;whileInView&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;viewport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Constants defined once, outside the render path, with a simple ternary instead of a function call. The object reference is now stable across renders so Framer Motion's internal tracking never gets confused about which variants it's interpolating toward. Six files had the same pattern and all six got fixed in a single pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and after
&lt;/h2&gt;

&lt;p&gt;Before the fix, cards and titles occasionally rendered permanently blurred and never recovered without a full page reload, reproducible but not consistently, which made it easy to treat each occurrence as unrelated to the others.&lt;/p&gt;

&lt;p&gt;After the fix, every scroll-triggered reveal in the app resolves correctly, every time, across every entry point I could find.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Animation libraries that key transitions off object identity are unforgiving of helper functions that build config objects inline. If you're writing a function that returns a &lt;code&gt;Variants&lt;/code&gt; object, a &lt;code&gt;style&lt;/code&gt; object, or anything else React or a library will diff by reference, and you're calling that function during render, the reference changes on every render even when the values don't. Hoisting it to a constant outside the component lets the render function pick between two stable references instead of generating a new object every time.&lt;/p&gt;

&lt;p&gt;The other thing that came out of this was more about process than code. The third occurrence was the only one I actually investigated properly, and it was the only one that produced a fix that stuck, because it was the first time I looked for what the three broken components had in common instead of patching the one in front of me.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built as part of &lt;a href="https://entiscore.vercel.app" rel="noopener noreferrer"&gt;Entiscore&lt;/a&gt;, an entity-audit agent built for the Kiro powered by AWS hackathon by Código Facilito.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>kiro</category>
      <category>typescript</category>
    </item>
    <item>
      <title>overflow: clip saved my navbar</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Fri, 21 Aug 2026 23:34:30 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/overflow-clip-saved-my-navbar-2a99</link>
      <guid>https://dev.to/carlosjcastrog/overflow-clip-saved-my-navbar-2a99</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There was no error in the console, the CSS was syntactically correct, the logic was sound, and &lt;code&gt;overflow: hidden&lt;/code&gt; was just not working.&lt;/p&gt;

&lt;p&gt;I was building the navigation overlay for my personal portfolio, which runs on Next.js 15 with App Router, React 19 and Tailwind v4. The navbar opens a fullscreen overlay that descends from the top using GSAP. Inside that overlay, each navigation link has a split reveal effect: the link text sits in white with a dark layer underneath it, and on hover the dark layer rises to cover the white text while dark-colored text climbs up from below to replace it. The classic typographic flip that you see in agency sites and high-end portfolios.&lt;/p&gt;

&lt;p&gt;The overlay itself had &lt;code&gt;position: fixed&lt;/code&gt; to cover the entire viewport. That part worked fine.&lt;/p&gt;

&lt;p&gt;The text effect required wrapping each link in a container with &lt;code&gt;overflow: hidden&lt;/code&gt; so that the rising dark layer and the incoming dark text would be clipped until they entered the visible area. Without that clip, both layers show simultaneously and the menu looks like every link is doubled in two colors, which is exactly what I was seeing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six things that didn't fix it
&lt;/h2&gt;

&lt;p&gt;The first attempt was &lt;code&gt;overflow: hidden&lt;/code&gt; directly on the link wrapper container, but nothing changed and both text layers stayed visible.&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;.nav-link-wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* did nothing */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I switched to a fixed pixel height on the container calculated manually to match the line height. The thinking was that maybe the browser needed an explicit dimension to clip against and... still nothing.&lt;/p&gt;

&lt;p&gt;Then I tried &lt;code&gt;clip-path: inset(0 0 100% 0)&lt;/code&gt; animated in CSS, which clips the element's painted area directly without involving overflow behavior at all. The layers kept showing through.&lt;/p&gt;

&lt;p&gt;I moved &lt;code&gt;overflow: hidden&lt;/code&gt; up to the &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; element with no difference, then to the &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;, which made things worse because now it was affecting the whole list.&lt;/p&gt;

&lt;p&gt;At that point I started suspecting a specificity conflict or a CSS-in-JS issue, so I switched to inline styles in React to eliminate any possible cascade interference:&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;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.2em&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;gt;&lt;/span&gt;
  ...
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&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;Same result, both layers still visible.&lt;/p&gt;

&lt;p&gt;After six attempts there was no progress, no error messages and nothing in the console pointing anywhere useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually happening
&lt;/h2&gt;

&lt;p&gt;After enough time searching I found the answer in the CSS specification. &lt;code&gt;overflow: hidden&lt;/code&gt; does not just clip content, it also creates what the spec calls a Block Formatting Context, which establishes an independent layout environment for the element and its descendants.&lt;/p&gt;

&lt;p&gt;There's a critical exception to this though: a Block Formatting Context cannot contain elements that are descendants of an ancestor with &lt;code&gt;position: fixed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The overlay had &lt;code&gt;position: fixed&lt;/code&gt;. Every element inside it, including my link wrappers trying to use &lt;code&gt;overflow: hidden&lt;/code&gt;, existed as a descendant of a fixed-positioned ancestor. When a child element tries to establish a BFC through &lt;code&gt;overflow: hidden&lt;/code&gt; but its nearest ancestor with a stacking context is &lt;code&gt;position: fixed&lt;/code&gt;, the browser cannot create the expected formatting context correctly. The clipping behavior does not apply.&lt;/p&gt;

&lt;p&gt;This interaction is defined in the CSS specification. It's not a bug in any browser. Chrome, Firefox and Safari all behave the same way because they're all following the spec. The problem is that &lt;code&gt;overflow: hidden&lt;/code&gt; works in probably 99% of cases people use it, so nobody builds a mental model of when it doesn't.&lt;/p&gt;

&lt;p&gt;There's also nothing diagnostic about the failure. The element renders. The CSS applies. The browser just quietly doesn't clip the overflow because the BFC can't be established in that context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The answer was &lt;code&gt;overflow: clip&lt;/code&gt;.&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;.nav-link-wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* instead of overflow: hidden */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;overflow: clip&lt;/code&gt; was introduced in CSS as a more precise clipping mechanism and unlike &lt;code&gt;overflow: hidden&lt;/code&gt; it does not create a Block Formatting Context. It clips the painted content visually without establishing any formatting context at all, which means there's no conflict with the &lt;code&gt;position: fixed&lt;/code&gt; ancestor and the clipping works exactly as expected, cutting off content that goes outside the element's bounds without any stacking context complications.&lt;/p&gt;

&lt;p&gt;For the user, &lt;code&gt;overflow: clip&lt;/code&gt; and &lt;code&gt;overflow: hidden&lt;/code&gt; look identical when they both work. The difference is only in what the browser does internally, and that internal difference is what determines whether clipping actually happens inside a fixed overlay.&lt;/p&gt;

&lt;p&gt;The text effect works now and the dark layer rises on hover, the white text disappears beneath it, the dark text climbs into view from below, and both layers clip cleanly at the container boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this one is worth knowing
&lt;/h2&gt;

&lt;p&gt;Most CSS bugs announce themselves. A layout breaks visually in an obvious way, or the console flags something, or the behavior changes between browsers in a way that points to a compatibility issue. This one didn't. The code looked right because it was right, just using a property that had a documented edge case that almost no practical documentation mentions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;overflow: clip&lt;/code&gt; has been supported in all major browsers since 2022, so it's not new, but because &lt;code&gt;overflow: hidden&lt;/code&gt; handles the vast majority of clipping scenarios without issue, most developers have never needed to reach for it. If you're building UI where overflow clipping needs to work inside a &lt;code&gt;position: fixed&lt;/code&gt; container, whether that's a fullscreen overlay, a fixed sidebar, or a sticky header with animated content inside it, &lt;code&gt;overflow: clip&lt;/code&gt; is the right tool.&lt;/p&gt;

&lt;p&gt;The six failed attempts weren't wasted. Working through &lt;code&gt;clip-path&lt;/code&gt;, inline styles and every possible container element confirmed that the problem wasn't specificity, wasn't React, wasn't Tailwind, and wasn't any configuration issue. It was the CSS spec doing exactly what the CSS spec says, in a case where the spec and the intuition don't match.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Portfolio built with Next.js 16, React 19 and Tailwind v4. Available at &lt;a href="https://carlosjcastrog.com" rel="noopener noreferrer"&gt;carlosjcastrog.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>nextjs</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The lighthouse that leaked WebGL memory every time you changed the theme</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Thu, 20 Aug 2026 11:48:36 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/the-lighthouse-that-leaked-webgl-memory-every-time-you-changed-the-theme-4mib</link>
      <guid>https://dev.to/carlosjcastrog/the-lighthouse-that-leaked-webgl-memory-every-time-you-changed-the-theme-4mib</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;FaroIQ is a strategic intelligence platform for nonprofits that I built for the Microsoft Agents League Hackathon 2026. It runs a 9-agent pipeline on Azure AI Foundry, produces a full strategic report in under 90 seconds, and executes automatically into Microsoft 365.&lt;/p&gt;

&lt;p&gt;The hero section was one of the most important design decisions of the project. In a hackathon, the first screen a judge sees defines the impression of everything that follows. I wanted something that represented the name FaroIQ, which means lighthouse in Spanish, without falling into the generic hero with a gradient, a headline, and a CTA button that every project ends up with.&lt;/p&gt;

&lt;p&gt;So I built a fully custom 3D lighthouse with Three.js from scratch: cylindrical tower segments with alternating stone and stripe materials, a lantern room with a rotating beam using a &lt;code&gt;SpotLight&lt;/code&gt; and additive blending cones, animated wave geometry updated every frame, star particles in dark mode, a sun sphere in light mode, directional and point lights, fog with different density between modes, and a lens sphere with pulsing opacity. The lighthouse was not decorative. It was functional to the concept and it was what made the hero feel like something built with intention rather than assembled from a template.&lt;/p&gt;

&lt;p&gt;The app also had a dark and light mode toggle. The lighthouse had to look completely different in each mode. Different fog density, different beam intensity, different water color, stars appearing and disappearing, moon versus sun. That meant the component received an &lt;code&gt;isDark&lt;/code&gt; prop and rebuilt its entire material palette and scene configuration based on it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Environment
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;React 18 + Vite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3D library&lt;/td&gt;
&lt;td&gt;Three.js r168&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Affected environments&lt;/td&gt;
&lt;td&gt;Local development and production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser&lt;/td&gt;
&lt;td&gt;Chrome, Firefox, Safari (all affected)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What was happening
&lt;/h2&gt;

&lt;p&gt;The bug had multiple faces and none of them were consistent, which made it harder to pin down.&lt;/p&gt;

&lt;p&gt;On the first load, the lighthouse sometimes did not finish rendering at all. The browser would stall partway through the Three.js initialization and the canvas would stay black. In production, a judge opening the app for the first time might see nothing in the hero section.&lt;/p&gt;

&lt;p&gt;When the user clicked the theme toggle, one of several things happened depending on timing and how much the browser had already used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The lighthouse froze mid-transition with both the old and new scene partially rendered at the same time&lt;/li&gt;
&lt;li&gt;The scene switched correctly but the browser's memory footprint climbed with each toggle&lt;/li&gt;
&lt;li&gt;Nothing happened at all and the lighthouse stayed stuck in the previous mode regardless of the new &lt;code&gt;isDark&lt;/code&gt; value&lt;/li&gt;
&lt;li&gt;The browser tab became unresponsive and had to be killed
The performance tab in Chrome DevTools showed the GPU memory climbing with each theme switch instead of staying flat. The console showed:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARNING: Too many active WebGL contexts. Oldest context will be lost.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And intermittently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WebGL: INVALID_OPERATION: drawArrays: no buffer is bound to enabled attribute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bug was reproducible every time. Sometimes it took one toggle to surface it, sometimes two or three but it always happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites to understand the bug
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How React's useEffect cleanup works with dependencies
&lt;/h3&gt;

&lt;p&gt;When a &lt;code&gt;useEffect&lt;/code&gt; has dependencies, React runs the cleanup function and then re-runs the effect whenever any dependency changes. This happens within the same component instance. The component does not unmount. The refs stay the same. The DOM node stays the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  How WebGL contexts work in the browser
&lt;/h3&gt;

&lt;p&gt;Every &lt;code&gt;new THREE.WebGLRenderer()&lt;/code&gt; call creates a new WebGL context bound to a new canvas element. Browsers impose a hard limit on how many active WebGL contexts a page can have. Chrome's limit is around 16. When that limit is exceeded, the browser starts discarding the oldest contexts to make room for new ones.&lt;/p&gt;

&lt;p&gt;The critical detail is that &lt;code&gt;renderer.dispose()&lt;/code&gt; releases the Three.js resources on the JavaScript side, but the GPU does not necessarily free the underlying context memory synchronously. The browser's GPU process has its own lifecycle that does not block the JavaScript thread. Calling &lt;code&gt;dispose()&lt;/code&gt; and then immediately calling &lt;code&gt;new THREE.WebGLRenderer()&lt;/code&gt; in the same component lifetime can create a new context before the old one is fully released at the GPU level.&lt;/p&gt;

&lt;p&gt;This is the gap the bug lived in.&lt;/p&gt;




&lt;h2&gt;
  
  
  The component structure
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LighthouseBackground&lt;/code&gt; was always its own isolated component, which was the right decision. All the Three.js setup, animation loop, and cleanup lived inside a single &lt;code&gt;useEffect&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LighthouseBackground&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;isDark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&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;mountRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mountRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;mount&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renderer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebGLRenderer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;antialias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alpha&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="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;W&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="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setPixelRatio&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;devicePixelRatio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// ... full scene setup: materials, geometries, lights, animation loop&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;id&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animate&lt;/span&gt; &lt;span class="o"&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nf"&gt;animate&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;cancelAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onResize&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;mount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domElement&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nx"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isDark&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mountRef&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;absolute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inset&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="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&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 cleanup looked correct. Cancel the animation frame, remove the canvas, dispose the renderer. All the right things in the right order.&lt;/p&gt;

&lt;p&gt;The problem was that with &lt;code&gt;[isDark]&lt;/code&gt; as the dependency, React ran this cleanup and immediately re-ran the effect in the same component instance when the theme changed. The old WebGL context was not guaranteed to be fully released by the GPU before the new &lt;code&gt;WebGLRenderer&lt;/code&gt; constructor call created another one. With a scene this heavy, that gap was enough to trigger the memory accumulation and the browser warnings.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ThemeToggle&lt;/code&gt; component was wiring directly into the theme 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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ThemeToggle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toggle&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&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;theme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&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;FiSun&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt; : &amp;lt;FiMoon size=&lt;/span&gt;&lt;span class="se"&gt;{15}&lt;/span&gt;&lt;span class="sr"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;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;Each click updated &lt;code&gt;theme&lt;/code&gt; in the context, which re-rendered &lt;code&gt;HeroSection&lt;/code&gt;, which passed the new &lt;code&gt;isDark&lt;/code&gt; to &lt;code&gt;LighthouseBackground&lt;/code&gt;, which triggered the effect cycle described above.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reproduction steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Clone the repository and run &lt;code&gt;npm install &amp;amp;&amp;amp; npm run dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Open the app in Chrome with DevTools open on the Performance and Console tabs&lt;/li&gt;
&lt;li&gt;Observe the lighthouse on initial load. Note whether it finishes rendering&lt;/li&gt;
&lt;li&gt;Click the theme toggle button once&lt;/li&gt;
&lt;li&gt;Observe the console for WebGL warnings&lt;/li&gt;
&lt;li&gt;Click the toggle four or five more times in quick succession&lt;/li&gt;
&lt;li&gt;Watch the GPU memory in the Performance tab. It climbs instead of staying flat
In production the same steps apply on the Vercel deployment.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why simplifying the geometry was not the answer
&lt;/h2&gt;

&lt;p&gt;The obvious alternative was to reduce the complexity of the lighthouse. Fewer polygons, simpler materials, basic shapes.&lt;/p&gt;

&lt;p&gt;The problem with that is that a lighthouse made of cylinders and cones with flat shading and no texture is just a cartoon. It would not have represented FaroIQ in any meaningful way. The whole point of building a custom 3D scene instead of a generic hero was that the lighthouse was recognizable as a lighthouse and felt like it belonged to the project.&lt;/p&gt;

&lt;p&gt;Reducing to spheres or abstract geometry would have solved the performance problem by removing the thing that was worth keeping. That is not a fix.&lt;/p&gt;

&lt;p&gt;The actual optimizations that helped performance without compromising the visual were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Capping &lt;code&gt;pixelRatio&lt;/code&gt; at 1.5 to avoid running at 3x or 4x on high DPI displays&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;MeshPhongMaterial&lt;/code&gt; instead of &lt;code&gt;MeshStandardMaterial&lt;/code&gt; across the scene, which skips physically based lighting calculations&lt;/li&gt;
&lt;li&gt;Setting segment counts conservatively on each geometry, enough to read as round without subdividing unnecessarily&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;MeshBasicMaterial&lt;/code&gt; for the beam cones and lens sphere since those elements do not need lighting
Those changes made the initial load faster. They did not fix the WebGL context leak.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What didn't work
&lt;/h2&gt;

&lt;p&gt;Before arriving at the &lt;code&gt;key&lt;/code&gt; prop, there were two attempts that seemed reasonable but did not solve the problem.&lt;/p&gt;

&lt;p&gt;The first was moving the cleanup logic into a separate component that would act as a wrapper and handle teardown independently. The idea was that decoupling the renderer lifecycle from the scene setup might give the GPU more breathing room between context changes. In practice it made things worse. The cleanup still happened within the same React tree lifecycle and the WebGL context still accumulated. What it did add was complexity: now the lighthouse logic was split across two components with no clear ownership of the renderer, which made the code harder to reason about for something as specific and self-contained as a 3D scene.&lt;/p&gt;

&lt;p&gt;The second attempt was separating the material definitions from the renderer setup, building the material palette outside the main &lt;code&gt;useEffect&lt;/code&gt; so that only the renderer and scene would re-initialize on theme change while the materials would be shared. This direction had a similar problem. The materials in Three.js are tied to the WebGL context they were created in. Sharing materials across renderer instances does not work the way sharing JavaScript objects does. More importantly, pulling the material definitions out of the component meant scattering the lighthouse configuration across multiple files for a component that was always going to live in one place and serve one purpose. The cognitive cost was not worth it.&lt;/p&gt;

&lt;p&gt;Both attempts were solving the symptom, which was the re-initialization cost, rather than the actual problem, which was the component instance persisting across changes that required a full reset.&lt;/p&gt;




&lt;h2&gt;
  
  
  Memory behavior before and after the fix
&lt;/h2&gt;

&lt;p&gt;Before the fix, the GPU memory pattern in Chrome DevTools was visible without needing exact measurements. Each theme toggle added a layer of memory that did not come back down. The first toggle was usually smooth. By the third or fourth, the page started producing small freezes, the kind where the animation loop stalls for a fraction of a second and the lighthouse beam stops mid-rotation before catching up. By the sixth or seventh toggle in quick succession, the browser tab either threw the WebGL context warning and lost the scene entirely, or became unresponsive and had to be killed.&lt;/p&gt;

&lt;p&gt;The experience was not just a visual problem. Because the lighthouse has a continuous animation loop updating the wave geometry every frame and rotating the beam, a stalled renderer was immediately perceptible. The scene did not degrade gracefully. It either worked or it froze.&lt;/p&gt;

&lt;p&gt;After the fix, the memory line in the performance tab stayed flat across theme switches. The full lighthouse unmounts cleanly, the GPU releases the context, and a fresh instance starts from a clean state. The transition between day and night now feels instant. The beam keeps rotating, the waves keep animating, and the fog density shift from the dense dark mode to the lighter day mode happens without any visible frame drop.&lt;/p&gt;

&lt;p&gt;The difference between the two behaviors is the difference between a WebGL context that accumulates and one that has a defined lifetime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Browser differences
&lt;/h2&gt;

&lt;p&gt;The bug was tested in Chrome and Firefox. Both showed the same core behavior: memory accumulation with each theme toggle and eventual WebGL context loss. Chrome was more explicit about it with the console warning naming the context limit directly. Firefox manifested it more as progressive slowdown and frame drops rather than a hard error, but the underlying cause was identical.&lt;/p&gt;

&lt;p&gt;Safari has a lower WebGL context limit than Chrome, around eight compared to Chrome's sixteen. In a hackathon where judges can open the project on any machine and any browser, that matters. A bug that takes six toggles to crash Chrome might crash Safari on the second or third. The fix applies equally to all of them since it addresses the root cause rather than pushing the limit further away.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;One prop in &lt;code&gt;HeroSection&lt;/code&gt;:&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="nc"&gt;LighthouseBackground&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;isDark&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isDark&lt;/span&gt;&lt;span class="si"&gt;}&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;The &lt;code&gt;key&lt;/code&gt; prop changes what React does at the component level. Without it, a theme change updates the existing &lt;code&gt;LighthouseBackground&lt;/code&gt; instance. The effect cleanup runs and the effect re-runs in the same component lifetime. The same DOM node, the same refs, the same component.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;key={theme}&lt;/code&gt;, React treats the component as a different element when &lt;code&gt;theme&lt;/code&gt; changes. It fully unmounts the old &lt;code&gt;LighthouseBackground&lt;/code&gt;, running the cleanup and removing it from the tree entirely. The old canvas element is removed from the DOM. The old renderer is disposed. The old component instance is gone. Only then does React mount a fresh &lt;code&gt;LighthouseBackground&lt;/code&gt; with new refs, new state, and a new Three.js scene.&lt;/p&gt;

&lt;p&gt;This gives the browser a genuine boundary between the old renderer's lifetime and the new one's. The GPU context count stays at one. No memory accumulation. No stale context errors.&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;// HeroSection.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="err"&gt;...&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;LighthouseBackground&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;isDark&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isDark&lt;/span&gt;&lt;span class="si"&gt;}&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;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;key&lt;/code&gt; here is &lt;code&gt;theme&lt;/code&gt;, which is the string &lt;code&gt;"dark"&lt;/code&gt; or &lt;code&gt;"light"&lt;/code&gt;. When it changes, React sees a component with a different key and treats it as a complete replacement.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it ended up documented
&lt;/h2&gt;

&lt;p&gt;The fix was intentional enough that it ended up in the project README:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Theme changes force a full remount of the Three.js scene via a React key prop to ensure clean state."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That single sentence captures what took several debug sessions to arrive at. The rest of the theme system description in the README covers what the switch actually does visually: cream and red tower with a bright sky, sun, and ambient light in day mode; dark navy and blue tower with a starfield, moon, and volumetric beam in night mode. The visual difference between modes was significant enough that a partial or broken transition was immediately obvious to anyone looking at the page. That visibility was part of what made fixing it non-negotiable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I took from this
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useEffect&lt;/code&gt; cleanup is not a component teardown. It is a side effect reset within the same component lifetime. For most cases, that distinction is irrelevant. For APIs that hold GPU resources, the difference between updating an instance and replacing it is the difference between a working app and a browser that runs out of WebGL contexts.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;key&lt;/code&gt; prop as a remount trigger is not a workaround. It is the correct React pattern when a prop change requires a full reset of imperative resources rather than an in-place update. Using it intentionally is different from using it as a patch for an architecture problem.&lt;/p&gt;

&lt;p&gt;The performance optimizations around &lt;code&gt;pixelRatio&lt;/code&gt; and material choice were real improvements and should have been there from the start. But they were solving a different problem. A lighter scene still leaks WebGL contexts if the underlying issue is not addressed.&lt;/p&gt;

&lt;p&gt;The lighthouse works now. Theme switching is instant, memory stays flat, and the browser stays alive.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FaroIQ was built for the Microsoft Agents League Hackathon 2026. Demo available at &lt;a href="https://faroiq.vercel.app" rel="noopener noreferrer"&gt;faroiq.vercel.app&lt;/a&gt;. Source on &lt;a href="https://github.com/carlosjcastro/faroiq" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>react</category>
      <category>typescript</category>
    </item>
    <item>
      <title>The loading screen that took down every browser I opened</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Thu, 20 Aug 2026 00:03:35 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/the-loading-screen-that-took-down-every-browser-i-opened-3pe8</link>
      <guid>https://dev.to/carlosjcastrog/the-loading-screen-that-took-down-every-browser-i-opened-3pe8</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There is a specific kind of bug that makes you question whether you know how to code at all. Not the kind where something throws a clear error and you trace it back in two minutes. The kind where the entire browser window starts flashing black and the only option is to kill the tab before Chrome locks up completely.&lt;/p&gt;

&lt;p&gt;That was the loading screen bug in my portfolio.&lt;/p&gt;

&lt;h2&gt;
  
  
  The project
&lt;/h2&gt;

&lt;p&gt;I had just migrated my personal portfolio to Next.js 16 and was building a more complete version from scratch. The site had a custom loading screen mounted in the root layout. It needed to be there because it was the first thing users saw before any content loaded.&lt;/p&gt;

&lt;p&gt;The loader is not a simple spinner. It draws an SVG path that traces my initial letter G using a stroke animation, moves it to the left, reveals my full name alongside it, and then executes a color wipe exit animation in two stages before calling an &lt;code&gt;onComplete&lt;/code&gt; callback and unmounting. There are refs for direct DOM manipulation of the SVG elements, state variables for the wipe stages, timers, and transition logic layered across all of it.&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;wipe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setWipe&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;wipe2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setWipe2&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setHidden&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;wipeColor&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="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;COLORS&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;floor&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;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;COLORS&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="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gPathRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SVGPathElement&lt;/span&gt;&lt;span class="o"&gt;&amp;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gWrapperRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SVGGElement&lt;/span&gt;&lt;span class="o"&gt;&amp;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textGroupRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SVGGElement&lt;/span&gt;&lt;span class="o"&gt;&amp;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svgRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SVGSVGElement&lt;/span&gt;&lt;span class="o"&gt;&amp;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timerRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NodeJS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Timeout&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Standard stuff. Or so I thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was happening
&lt;/h2&gt;

&lt;p&gt;The moment I opened the dev server, the browser window started flashing. Not a subtle flicker. The entire viewport alternating between black and content at full speed, over and over, with no way to stop it without closing the tab. Within seconds, Chrome would throw errors and warnings in the console and start slowing down noticeably. If I left it running, the browser would eventually saturate completely.&lt;/p&gt;

&lt;p&gt;The same thing happened in production. The page stayed black. The loop was executing so fast that the content never had time to render visibly. Anyone visiting the site would see a black screen and nothing else.&lt;/p&gt;

&lt;p&gt;I had to kill the local server every time I opened it just to be able to work on anything else in the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it took time to find
&lt;/h2&gt;

&lt;p&gt;The symptom was so extreme that it pointed in the wrong direction. When a browser behaves like that, the instinct is to look for something major: a memory leak, a broken build configuration, a dependency conflict. I spent time checking all of those and found nothing.&lt;/p&gt;

&lt;p&gt;The component itself looked plausible when I read through it. There was state, there were refs, there was timer logic. No obvious loop anywhere in the code.&lt;/p&gt;

&lt;p&gt;What eventually led me to the actual cause was a combination of two things. I found posts on DEV describing similar symptoms, which pointed me toward infinite render loops as the likely category of problem. Then I opened React DevTools and looked at what was actually happening at runtime. The console was showing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Maximum update depth exceeded. This can happen when a component calls 
setState inside useEffect, but useEffect either doesn't have a dependency 
array, or one of the dependencies changes on every render.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That error, combined with watching the component re-mount continuously in the DevTools component tree, made the cause clear.&lt;/p&gt;

&lt;p&gt;The problem was that state updates controlling the loader behavior were running during render, outside of any effect. In a component with this much timer and transition logic, that is easy to do accidentally. Every render triggered a state change, which triggered another render, which triggered another state change. The browser was re-rendering the entire root layout hundreds of times per second, which is why the viewport was flashing and everything eventually locked up.&lt;/p&gt;

&lt;p&gt;The complexity of the component made it harder to catch on a read-through because the timer logic created the impression that the state updates were conditional and time-bounded. They were not. The condition was being evaluated on every render.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The solution was two things applied together.&lt;/p&gt;

&lt;p&gt;First, separating the SVG animation logic and the wipe exit logic into two distinct &lt;code&gt;useEffect&lt;/code&gt; hooks, each with explicit cleanup. The SVG animation runs once after mount. The wipe timers run in a separate effect that cleans up all three timeouts on unmount.&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;// SVG draw and reveal, only needs to run once&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gPathRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;gWrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gWrapperRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;textGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;textGroupRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;svg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;svgRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;gPath&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;gWrapper&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;textGroup&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;svg&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTotalLength&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;gPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strokeDasharray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;length&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;gPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strokeDashoffset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;length&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;// force reflow so the transition actually fires&lt;/span&gt;
  &lt;span class="nx"&gt;gPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;gPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stroke-dashoffset 1.2s cubic-bezier(0.76, 0, 0.24, 1)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;gPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strokeDashoffset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;gWrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transform 0.7s cubic-bezier(0.16, 1, 0.3, 1)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;gWrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;translateX(0px)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&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;textGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opacity 0.5s ease&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;textGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&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="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1300&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

&lt;span class="c1"&gt;// Exit wipe timers, cleanup on unmount&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;setWipe&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="mi"&gt;2800&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;t2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;setWipe2&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="mi"&gt;3200&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;t3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;setHidden&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="nf"&gt;onComplete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;3800&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;timerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;t1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t3&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;timerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clearTimeout&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="nx"&gt;onComplete&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, I moved the loader into its own isolated component instead of keeping it inline in the root layout. That separation made the lifecycle predictable and prevented any accidental coupling with the state of other components in the layout tree.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;wipeColor&lt;/code&gt; also uses a functional initializer in &lt;code&gt;useState&lt;/code&gt; rather than computing a random value during render:&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;wipeColor&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="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;COLORS&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;floor&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;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;COLORS&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters in Next.js because components in the root layout render on the server first. A random value computed directly in the render body would produce a different result on the server versus the client, causing a hydration mismatch on top of the loop problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from this
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Maximum update depth exceeded&lt;/code&gt; error is one of those React warnings that sounds abstract until you see what it actually produces at runtime. In this case: a completely unusable dev environment and a broken production deployment with a black screen.&lt;/p&gt;

&lt;p&gt;Reading through the code was not enough to catch it. The timer logic created a plausible narrative for why the state updates seemed controlled, and that made the actual problem invisible on a first read. What made the difference was switching from reading the code to watching the runtime behavior in DevTools, combined with finding documentation about similar symptoms.&lt;/p&gt;

&lt;p&gt;Infinite render loops in root layout components are particularly destructive because they take down the entire page, not just the component. A loop in a leaf component somewhere deep in the tree has an isolated impact. In the root layout, nothing works until the loop is gone.&lt;/p&gt;

&lt;p&gt;The loading screen now works exactly as intended. The G traces itself, slides into position, the name appears, the color wipe plays out in two stages, and the page loads cleanly underneath. No flashing, no black screen, no console warnings.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Portfolio built with Next.js 16. Available at &lt;a href="https://carlosjcastrog.com" rel="noopener noreferrer"&gt;carlosjcastrog.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>nextjs</category>
      <category>react</category>
    </item>
    <item>
      <title>Next.js 16 and 16.3, what changed and what to know before upgrading</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Tue, 18 Aug 2026 11:52:33 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/nextjs-16-and-163-what-changed-and-what-to-know-before-upgrading-41f7</link>
      <guid>https://dev.to/carlosjcastrog/nextjs-16-and-163-what-changed-and-what-to-know-before-upgrading-41f7</guid>
      <description>&lt;p&gt;If your project runs Next.js 14 or 15 and you haven't looked at what's changed lately, there's a fair amount to process. Next.js 16 shipped in October 2025 with core changes to the bundler, the React compiler, and the caching model. Version 16.3 reached stable on August 3, 2026 with a redesigned navigation system. Both versions have breaking changes and new capabilities worth understanding before upgrading in production.&lt;/p&gt;

&lt;p&gt;This post covers what changed concretely, what's available by default, what requires manual activation, and when it's worth waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turbopack is the default since Next.js 16
&lt;/h2&gt;

&lt;p&gt;The most significant thing about version 16 wasn't a new feature, it was Webpack's exit. Turbopack, the Rust-based bundler Vercel had been developing since 2022, became the stable default for both development and production.&lt;/p&gt;

&lt;p&gt;No configuration needed. You upgrade, run &lt;code&gt;next dev&lt;/code&gt;, and you're already using Turbopack. The numbers from production projects are meaningful: builds that took 24.5 seconds dropped to 5.7 seconds. Fast Refresh is up to 10 times faster. Some projects in long development sessions were hitting 21.5 GB of memory before 16.3; the 16.3 release reduced dev server memory usage by up to 90%.&lt;/p&gt;

&lt;p&gt;If you have a custom Webpack configuration, you can still use it with the &lt;code&gt;--webpack&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;next dev &lt;span class="nt"&gt;--webpack&lt;/span&gt;
next build &lt;span class="nt"&gt;--webpack&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you time to migrate without blocking the project. But it's worth doing: Webpack won't receive improvements in future Next.js versions and support is maintenance-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  React Compiler, what it does and when it matters
&lt;/h2&gt;

&lt;p&gt;Next.js 16 included stable support for the React Compiler. What the compiler does is analyze the component tree and add automatic memoization where it detects that a value or component doesn't need to be recalculated. In practice it eliminates most manual &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, and &lt;code&gt;memo&lt;/code&gt; calls.&lt;/p&gt;

&lt;p&gt;What it doesn't do is fix poorly written code. If a component has side effects that should be in a &lt;code&gt;useEffect&lt;/code&gt; but are loose in the render body, the compiler doesn't compensate for that. And if your app already has good manual memoization discipline, the perceptible difference can be minimal.&lt;/p&gt;

&lt;p&gt;For new projects it makes sense to enable it from the start. For existing ones, the conservative path is enabling it in staging, measuring the impact, and migrating from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking changes in Next.js 16
&lt;/h2&gt;

&lt;p&gt;There are three changes that can break existing code and are worth reviewing before upgrading.&lt;/p&gt;

&lt;p&gt;Params and searchParams in layouts, pages, and metadata are now Promises. Code that assumed synchronous access to those values will fail. The migration is adding &lt;code&gt;await&lt;/code&gt; before accessing them, or using the official codemod that handles it automatically.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;next/image&lt;/code&gt; changed its defaults: &lt;code&gt;decoding&lt;/code&gt; is now &lt;code&gt;async&lt;/code&gt; by default and &lt;code&gt;fetchPriority&lt;/code&gt; is &lt;code&gt;auto&lt;/code&gt;. Images that relied on the previous behaviors may need explicit attribute adjustments.&lt;/p&gt;

&lt;p&gt;Fetch requests in Server Components that didn't pass a cache policy now default to &lt;code&gt;cache: 'no-store'&lt;/code&gt;. Data that was previously cached silently now gets fetched on every request. If you notice a latency increase after upgrading, that change is likely the reason.&lt;/p&gt;

&lt;p&gt;For migration, Vercel published a codemod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @next/codemod@canary upgrade latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It covers most automatic changes but not all. The official upgrade guide has the detail on what the codemod doesn't handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instant Navigations and Partial Prefetching
&lt;/h2&gt;

&lt;p&gt;Next.js 16.3 reached stable on August 3, 2026. The most significant change is Instant Navigations, which addresses a long-standing gap between Next.js and SPAs: the perceived speed of client-side transitions.&lt;/p&gt;

&lt;p&gt;In Next.js 14 and 15, when a user clicked a link the browser sent a request to the server, waited for the response, and rendered. The wait time was always visible, especially on slower connections. Classic SPAs avoided that by showing immediate content because everything was on the client, but they paid for it in initial load time and SEO.&lt;/p&gt;

&lt;p&gt;Instant Navigations combines two things. Cache Components lets you cache parts of the layout on the client. Partial Prefetching generates a single reusable shell per route and caches it once. If you have 20 links pointing to &lt;code&gt;/products/[id]&lt;/code&gt;, the browser prefetches one generic shell, not 20 individual prefetch requests. When the user clicks, the shell appears immediately while dynamic content arrives from the server.&lt;/p&gt;

&lt;p&gt;The practical result: navigations that feel like those in a SPA without giving up Server Components or the server-first model.&lt;/p&gt;

&lt;p&gt;Both features are opt-in for now. To enable them:&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;// next.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&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;next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cacheComponents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;partialPrefetching&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They're planned as defaults in a future major version. Enabling them now isn't a dead end, it's early adoption of something that will become standard.&lt;/p&gt;

&lt;p&gt;Inside routes, you have three options:&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;// Stream: shell appears immediately, content arrives after&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="o"&gt;=&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;Shell&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Content&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Suspense&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Cache: content is cached on the client&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;ProductData&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;id&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Block: disable Instant Navigations for this specific route&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;instant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For fully static prerendered routes with SSG, Instant Navigations doesn't make a perceptible difference. The benefit is highest on dynamic routes with data that varies per request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turbopack and memory in 16.3
&lt;/h2&gt;

&lt;p&gt;Beyond Instant Navigations, 16.3 brought targeted improvements to Turbopack. Dev server memory usage dropped by up to 90% in large projects. Some setups that reached 21.5 GB in long sessions now run on a fraction of that.&lt;/p&gt;

&lt;p&gt;File system caching for builds was also added. Subsequent builds reuse previous work. In large projects that shows up in incremental build time.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP and agents in Next.js 16.3
&lt;/h2&gt;

&lt;p&gt;One addition that's gotten less coverage is the MCP endpoint. Next.js 16.3 exposes &lt;code&gt;/_next/mcp&lt;/code&gt; on the dev server, which allows coding agents to connect to the running server and check the compilation status of specific routes without running a full &lt;code&gt;next build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The route compiler has a &lt;code&gt;compile_route&lt;/code&gt; tool that responds whether a specific route compiles correctly. For AI-assisted development flows, that significantly reduces validation time.&lt;/p&gt;

&lt;p&gt;The Next.js team also shipped four first-party agent skills: one that adopts Cache Components, one that optimizes routes after adoption, one that adopts Partial Prefetching, and a dev-loop skill that connects the agent to the running server through the MCP endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to enable now and what to wait on
&lt;/h2&gt;

&lt;p&gt;For a new project, enabling Turbopack and React Compiler from the start makes sense. Both are stable and the benefits are immediate.&lt;/p&gt;

&lt;p&gt;For Instant Navigations and Partial Prefetching, the decision depends on the application profile. Dynamic routes with many links between pages are the ideal case. Fully static routes won't notice a difference. The sensible path is enabling them in staging, measuring with Lighthouse or Web Vitals, and moving to production with data.&lt;/p&gt;

&lt;p&gt;The breaking changes around async params and the defaults in &lt;code&gt;next/image&lt;/code&gt; and &lt;code&gt;fetch&lt;/code&gt; need review regardless of the rest. The codemod handles most of it but not everything.&lt;/p&gt;

&lt;p&gt;If you use Azure Static Web Apps to deploy your Next.js application, the official Microsoft documentation covers both static and hybrid modes with Server Components. A version migration doesn't require infrastructure configuration changes for most projects:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://learn.microsoft.com/azure/static-web-apps/nextjs?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/static-web-apps/nextjs?wt.mc_id=studentamb_510930&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Information based on official Next.js 16, 16.2, and 16.3 release notes as of August 16, 2026. Next.js may update behaviors between minor versions. Check the official changelog at nextjs.org/blog before upgrading production projects.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>GitHub Actions for developers who still deploy manually</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Mon, 03 Aug 2026 00:51:55 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/github-actions-for-developers-who-still-deploy-manually-22he</link>
      <guid>https://dev.to/carlosjcastrog/github-actions-for-developers-who-still-deploy-manually-22he</guid>
      <description>&lt;p&gt;Manual deployments follow a fairly predictable pattern. They work fine until someone on the team pushes a rushed change on a Friday, forgets to run the tests, and the bug hits production while everyone's offline. It's not a discipline problem. It's a process problem. A manual process fails exactly when there's the most pressure for it not to.&lt;/p&gt;

&lt;p&gt;GitHub Actions solves that by putting the pipeline inside the repository. There's no separate CI server to maintain, no external integrations to configure. Workflows live in &lt;code&gt;.github/workflows/&lt;/code&gt; and run in response to Git events.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CI/CD is and what GitHub Actions handles
&lt;/h2&gt;

&lt;p&gt;Continuous Integration (CI) is the practice of running tests automatically every time someone pushes code. The goal is catching problems early, when context is fresh and the fix is cheap. Continuous Delivery (CD) is taking that validated code to a staging or production environment without manual intervention.&lt;/p&gt;

&lt;p&gt;GitHub Actions can do both. It's an event-driven automation platform: when something happens in the repository, a push, a pull request opening, a tag creation, a cron schedule, the jobs you've defined run.&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;In early 2026, the platform processes over 71 million jobs per day and the GitHub Marketplace hosts more than 10,000 published actions across 32 categories.&lt;/cite&gt; Practically anything you need to do already has a published action.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anatomy of a workflow
&lt;/h2&gt;

&lt;p&gt;A workflow is a YAML file inside &lt;code&gt;.github/workflows/&lt;/code&gt;. A repository can have as many workflows as it needs. The filename is up to you, but it needs a &lt;code&gt;.yml&lt;/code&gt; or &lt;code&gt;.yaml&lt;/code&gt; extension.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;on&lt;/code&gt; field defines which events trigger the workflow. &lt;code&gt;jobs&lt;/code&gt; contains one or more units of work. Each job runs on a runner, which is by default an ephemeral virtual machine that GitHub provisions, uses, and destroys. Steps within a job run in sequence on the same machine.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;uses: actions/checkout@v4&lt;/code&gt; is a step that clones the repository onto the runner. Without it, the machine exists but has no code. It's one of the few steps that almost always appears first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runners, what they are and what they cost
&lt;/h2&gt;

&lt;p&gt;&lt;cite&gt;GitHub Actions is free for public repositories and includes 2,000 minutes per month on the free tier for private repositories.&lt;/cite&gt; For personal projects and open source, that's more than enough.&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;In January 2026, GitHub cut runner prices by up to 39%.&lt;/cite&gt; &lt;cite&gt;Current pricing is $0.008 per minute for standard Linux runners and $0.016 per minute for larger runners.&lt;/cite&gt; macOS is where you need to pay attention because it costs $0.08 per minute, ten times more than Linux.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ubuntu-latest&lt;/code&gt; runner currently points to Ubuntu 24.04. &lt;cite&gt;In 2026, GitHub added new images in public preview: Ubuntu 26.04 for x64 and arm64, and Windows 11 arm64 with Visual Studio 2026.&lt;/cite&gt;&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;Custom images for GitHub-hosted runners reached general availability in April 2026.&lt;/cite&gt; That lets you define exactly what software comes preinstalled on the runner instead of installing it on every run, which reduces execution times in pipelines with many system-level dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets and environments
&lt;/h2&gt;

&lt;p&gt;Credentials never go in the YAML. GitHub has a secrets system at the repository and organization level that encrypts them at rest and injects them into workflows as environment variables. You define them in Settings → Secrets and variables → Actions.&lt;/p&gt;

&lt;p&gt;Inside the workflow you reference them like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.API_KEY }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./deploy.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Environments add a layer of control over secrets and deployments. You can define a &lt;code&gt;production&lt;/code&gt; environment that requires manual approval before any job using that environment runs. That's useful for avoiding a direct push to main from automatically deploying to production without review.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "Deploying to production"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Secrets defined in the &lt;code&gt;production&lt;/code&gt; environment are only accessible to jobs that declare that environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  OIDC for connecting to Azure without secrets
&lt;/h2&gt;

&lt;p&gt;Storing an Azure secret in GitHub works, but it introduces a credential that needs rotating, can leak, and tends to have broader permissions than necessary. OpenID Connect (OIDC) solves that more cleanly.&lt;/p&gt;

&lt;p&gt;With OIDC, the workflow requests a token signed by GitHub directly during execution. Azure verifies that token against a federated credential configured on a service principal and issues temporary access credentials for that specific run. There's nothing to store as a secret. The credentials last exactly as long as the job does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.AZURE_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;tenant-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.AZURE_TENANT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;subscription-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.AZURE_SUBSCRIPTION_ID }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;az webapp deploy --name my-app --resource-group my-rg --src-path ./dist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three login values are variables (not secrets) because they aren't credentials themselves, they're public identifiers for the service principal. The OIDC runtime negotiates the temporary credentials in the background.&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;In April 2026, OIDC for GitHub Actions added support for repository custom properties as claims in the token, a feature that reached general availability. That allows writing more granular access policies in Azure based on repository attributes, not just the name or branch.&lt;/cite&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache and concurrency
&lt;/h2&gt;

&lt;p&gt;Installing dependencies on every run is the biggest time sink in a typical pipeline. The &lt;code&gt;actions/cache&lt;/code&gt; action saves and restores directories between runs based on a cache key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;setup-node&lt;/code&gt; with &lt;code&gt;cache: npm&lt;/code&gt; handles &lt;code&gt;node_modules&lt;/code&gt; caching automatically. For other languages and tools you need to configure it explicitly with &lt;code&gt;actions/cache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Concurrency control prevents multiple runs of the same workflow from overlapping. If you push three times quickly, without concurrency control three runs will queue up. With this, the in-progress run gets cancelled when a new one arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.workflow }}-${{ github.ref }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On feature branches, cancelling the previous run makes sense. On &lt;code&gt;main&lt;/code&gt; you might not want to cancel because each run produces a deploy artifact. You can condition this using &lt;code&gt;github.ref&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updates through July and August 2026
&lt;/h2&gt;

&lt;p&gt;GitHub Agentic Workflows entered public preview earlier in 2026 for automating tasks like issue triage, CI failure analysis, and documentation updates. In July, another integration landed: you can now run the GitHub Copilot CLI directly inside a workflow using the built-in GITHUB_TOKEN, with no personal access token to create or store. The same principle that already applied to Agentic Workflows extended to Copilot CLI.&lt;/p&gt;

&lt;p&gt;Two security changes also came into effect. GitHub Enterprise Cloud with Data Residency started enforcing minimum version requirements for self-hosted runners on July 31, 2026. Runners below the minimum required version can no longer register or execute workflow jobs. GitHub Enterprise Cloud without Data Residency follows the same enforcement starting September 25, 2026.&lt;/p&gt;

&lt;p&gt;The other change targets supply chain security: GitHub Actions now automatically holds workflow runs identified as potentially malicious in public repositories, requiring explicit approval from a collaborator with write access before the workflow executes. That prevents compromised credentials from triggering workflows without human intervention.&lt;/p&gt;

&lt;p&gt;None of these three changes affects a standard, well-configured pipeline. They do matter for teams running outdated self-hosted runners and for open source projects that accept external contributions without prior review.&lt;/p&gt;

&lt;h2&gt;
  
  
  One practical observation before wrapping up
&lt;/h2&gt;

&lt;p&gt;The learning curve for GitHub Actions isn't in understanding YAML. It's in understanding the execution model: what shares state between steps and what doesn't, when it makes sense to split into multiple jobs and when it doesn't, and how the &lt;code&gt;github.*&lt;/code&gt; context gives you information about the event that triggered the workflow.&lt;/p&gt;

&lt;p&gt;The official documentation is solid. The problem is that it covers a lot of ground and it's not always clear where the right entry point is for someone just starting. The most useful place to begin is Microsoft Learn's quickstart that combines GitHub Actions with Azure, which connects directly to most practical cloud deployment scenarios:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://learn.microsoft.com/azure/developer/github/github-actions?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/developer/github/github-actions?wt.mc_id=studentamb_510930&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The information in this article is based on official GitHub documentation and verified sources at the time of publication. GitHub may update pricing, features, and platform behavior at any time. Check the official documentation at docs.github.com before making decisions based on this content.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>cicd</category>
      <category>devops</category>
      <category>azure</category>
    </item>
    <item>
      <title>How to join the Microsoft Student Ambassadors program in 2026</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Mon, 13 Jul 2026 23:38:13 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/how-to-join-the-microsoft-student-ambassadors-program-in-2026-47mp</link>
      <guid>https://dev.to/carlosjcastrog/how-to-join-the-microsoft-student-ambassadors-program-in-2026-47mp</guid>
      <description>&lt;p&gt;The Microsoft Student Ambassadors program has existed for over two decades, but in 2026 it changed significantly. It's no longer limited to computer science or engineering students. There's no cover letter or interview required. Any student who meets the requirements can register and start working toward becoming an Ambassador at their own pace.&lt;/p&gt;

&lt;p&gt;I'm writing this having gone through the full process myself, including mistakes that cost me time. The information in this post comes from the official program handbook, the requirements documents, and responses from the support team.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get as an Ambassador
&lt;/h2&gt;

&lt;p&gt;Before getting into the process, it's worth knowing what's on the other side. Upon completing onboarding and being officially accepted into the program you receive a &lt;code&gt;@studentambassadors.com&lt;/code&gt; email address, access to Microsoft Teams with the program's global community, Microsoft 365 with Copilot, Visual Studio Enterprise, and monthly Azure credits.&lt;/p&gt;

&lt;p&gt;You also receive an official program certificate and a Credly badge you can add to LinkedIn and your portfolio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eligibility requirements
&lt;/h2&gt;

&lt;p&gt;There are five requirements and all of them are mandatory.&lt;/p&gt;

&lt;p&gt;You must be at least 18 years old at the time of registration. You must be enrolled full-time at an accredited academic institution. You must have an active Azure for Students account. You can't be a Microsoft employee or active contractor. And you can't have been previously removed from the program.&lt;/p&gt;

&lt;p&gt;The Azure for Students account requires verifying your student status with an institutional email address. If your institution doesn't provide email addresses with their own domain or that domain isn't in Microsoft's system, you can try to verify with alternative documentation such as an official enrollment certificate. In some cases the support team accepts that type of documentation, in others they don't. If you're unsure, write to &lt;code&gt;registration@studentambassadors.com&lt;/code&gt; before registering.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to register
&lt;/h2&gt;

&lt;p&gt;Registration happens at &lt;code&gt;studentambassadors.microsoft.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The most critical field in the form is the &lt;strong&gt;Learn Username&lt;/strong&gt; field. There you need to enter only your Microsoft Learn username, which is the final part of your profile URL: &lt;code&gt;learn.microsoft.com/users/YOUR-USERNAME&lt;/code&gt;. Just that. Don't paste the full URL, don't include the language locale (&lt;code&gt;/en-us/&lt;/code&gt;, &lt;code&gt;/es-es/&lt;/code&gt;), don't add any extra text.&lt;/p&gt;

&lt;p&gt;If you enter something incorrect in that field, the system won't be able to index your certification and your progress won't register correctly. The only way to fix it is to delete your registration and start over. The handbook says this explicitly and the support team confirms it.&lt;/p&gt;

&lt;p&gt;Once the form is submitted, the status will show as &lt;strong&gt;Under Review&lt;/strong&gt;. That's normal and expected. It's not an error and it doesn't mean something went wrong. That status changes when you reach the path requirements and receive the formal program invitation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two paths and what each requires
&lt;/h2&gt;

&lt;p&gt;When registering you need to choose one of two paths. They differ in activity type and onboarding requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Community Influencer
&lt;/h3&gt;

&lt;p&gt;This path is for those who want to grow their online presence by sharing Microsoft technical content. The activity is publishing Microsoft links with your Contributor ID and accumulating &lt;strong&gt;250 Preferred Visitors&lt;/strong&gt; before the cohort deadline.&lt;/p&gt;

&lt;p&gt;Preferred Visitors aren't just clicks. It's a proprietary Microsoft metric that values genuine engagement: the person clicking, reading the content, and that content being relevant to the audience you're sharing it with. The handbook is clear on this: sharing links in messaging groups without context doesn't generate valid Preferred Visitors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Community Skiller
&lt;/h3&gt;

&lt;p&gt;This path is for those who want to grow their leadership by organizing learning sessions with Microsoft Learn content. The activity is creating Learn Plans and accumulating &lt;strong&gt;1,000 net-new modules completed&lt;/strong&gt; by participants.&lt;/p&gt;

&lt;p&gt;Net-new means modules the participant completed after joining your plan, not before. If someone had already completed a module before joining, that module doesn't count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which one to choose?&lt;/strong&gt; If you already publish technical content or have presence on dev platforms or social media, the Influencer path is more predictable because the outcome depends on you. The Skiller path requires other people to join your plan and complete modules, which is harder to control at the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The certification or Applied Skills you need
&lt;/h2&gt;

&lt;p&gt;In addition to meeting your path requirement, you need a Microsoft Certification or Applied Skills credential obtained or renewed within the 12 months prior to onboarding.&lt;/p&gt;

&lt;p&gt;Microsoft Certifications such as AZ-900, AI-900, AI-102, and others qualify without issue. If you don't have one yet, the program suggests two Applied Skills options that are faster to obtain.&lt;/p&gt;

&lt;p&gt;The first is &lt;strong&gt;Microsoft Applied Skills: Streamline business workflows with AI chat&lt;/strong&gt;, which takes approximately 30 minutes and works with Microsoft 365 Copilot Chat. The second is &lt;strong&gt;Microsoft Applied Skills: Generate reports with AI research agents&lt;/strong&gt;, which takes approximately 45 minutes and also uses Microsoft 365 Copilot Chat.&lt;/p&gt;

&lt;p&gt;Both are done in an interactive lab on Microsoft Learn and don't cost anything. They're practical assessments, not multiple-choice exams.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Contributor ID
&lt;/h2&gt;

&lt;p&gt;After registering, within &lt;strong&gt;3 to 5 business days&lt;/strong&gt; you'll receive a Contributor ID by email. It has this format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;?wt.mc_id=studentamb_######
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That ID is unique and won't change while you're a registered member. It's different from the Learn Contributor ID you can see in your Microsoft Learn profile. Don't confuse them or use them interchangeably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important: don't publish anything before receiving your Contributor ID.&lt;/strong&gt; Clicks you accumulate before receiving it won't count toward your progress because there's no ID to track the activity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to use it:&lt;/strong&gt; paste it at the end of any eligible Microsoft URL. If the URL has no &lt;code&gt;?&lt;/code&gt;, paste the ID as-is. If the URL already has a &lt;code&gt;?&lt;/code&gt;, replace the &lt;code&gt;?&lt;/code&gt; in your ID with &lt;code&gt;&amp;amp;&lt;/code&gt; before pasting it. If the URL includes a language locale like &lt;code&gt;/en-us/&lt;/code&gt; or &lt;code&gt;/es-es/&lt;/code&gt;, remove it first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# URL without parameters:&lt;/span&gt;
https://learn.microsoft.com/azure?wt.mc_id=studentamb_######

&lt;span class="gh"&gt;# URL with existing parameters:&lt;/span&gt;
https://learn.microsoft.com/copilot?WT.mc_id=academic&amp;amp;wt.mc_id=studentamb_######
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where to publish and where not to
&lt;/h2&gt;

&lt;p&gt;This is one of the areas where most people waste time. Not all platforms generate valid Preferred Visitors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platforms that work:&lt;/strong&gt; LinkedIn, X (Twitter), Facebook Groups, Dev.to, Hashnode, GitHub, Stack Overflow, and other public platforms where content is accessible without needing to be a member of a closed group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platforms that don't work:&lt;/strong&gt; WhatsApp, Telegram, Discord, and any closed messaging platform. The handbook is explicit about this: these are closed platforms and the counts won't be validated.&lt;/p&gt;

&lt;p&gt;From personal experience, publishing on your own blog or portfolio doesn't always generate valid counts either, even if the domain is yours. Public developer platforms like Dev.to and Hashnode work consistently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can't do
&lt;/h2&gt;

&lt;p&gt;The program has clear rules on this and it's worth reading them before you start.&lt;/p&gt;

&lt;p&gt;You can't ask other people to click your links. You can't use scripts, bots, or any kind of automation to generate traffic. You can't spam groups or communities with links that have no relevant context.&lt;/p&gt;

&lt;p&gt;Microsoft doesn't actively review every account in real time, but it can request evidence if it detects patterns of inorganic activity. If that happens and you can't justify the origin of your Preferred Visitors, the risk is removal from the program.&lt;/p&gt;

&lt;p&gt;The right approach is sharing content that's genuinely useful to your audience, with context that explains why that link is relevant to them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cohorts and what happens if you don't make it in time
&lt;/h2&gt;

&lt;p&gt;Program invitations are sent four times a year: January, April, July, and October. Each cohort has a deadline for completing requirements.&lt;/p&gt;

&lt;p&gt;If you don't make that deadline, your activity and progress &lt;strong&gt;aren't lost&lt;/strong&gt;. They carry forward automatically to the next cohort. So if you started accumulating Preferred Visitors and didn't reach 250 before the cutoff, that count stays active for the next cohort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reports and the Progression Board
&lt;/h2&gt;

&lt;p&gt;Activity reports are sent to your email &lt;strong&gt;every Saturday&lt;/strong&gt; (Pacific Time). The Progression Board inside the program portal updates at the same time.&lt;/p&gt;

&lt;p&gt;If you haven't received your first weekly report yet, the Progression Board will appear empty. That's not an error. It only starts showing data after the first report.&lt;/p&gt;

&lt;p&gt;Activity that occurs within 48 hours before the report is sent might not appear until the following week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Onboarding when the invitation arrives
&lt;/h2&gt;

&lt;p&gt;When you meet the requirements for your path and the corresponding cohort arrives, you'll receive an invitation by email. The onboarding process includes accepting the program agreement and code of conduct, setting up your Microsoft 365 account, and accessing Microsoft Teams with your new &lt;code&gt;@studentambassadors.com&lt;/code&gt; account.&lt;/p&gt;

&lt;p&gt;The Teams account can take up to 24 hours to provision. If during onboarding you can't access Teams, select Yes and continue with the remaining steps without stopping. It's not an error, it's an expected system delay.&lt;/p&gt;

&lt;h2&gt;
  
  
  A recommendation before asking questions
&lt;/h2&gt;

&lt;p&gt;The program has a very complete official handbook available inside the Student Ambassadors platform once you register. Most questions that come up during the process already have answers there.&lt;/p&gt;

&lt;p&gt;Before writing on Discord or to support, check the handbook. Beyond saving you time, it gives you a more complete understanding of the process than any single answer would.&lt;/p&gt;

&lt;p&gt;For questions you can't find in the handbook, the official support team contact is &lt;code&gt;registration@studentambassadors.com&lt;/code&gt;. They respond in English and the usual response time is 1 to 2 business days.&lt;/p&gt;

&lt;p&gt;If you want to explore the program directly:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://studentambassadors.microsoft.com?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;https://studentambassadors.microsoft.com?wt.mc_id=studentamb_510930&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The information in this article is based on the official Microsoft Student Ambassadors program handbook and the requirements documents in effect at the time of publication. Microsoft may update program requirements, eligible platforms, cohort dates, and benefits at any time without prior notice. Before registering or making any decisions based on this content, review the official sources directly at &lt;code&gt;studentambassadors.microsoft.com&lt;/code&gt; and the program handbook. This article has no official affiliation with Microsoft beyond the author's participation in the program as a Student Ambassador.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>microsoft</category>
      <category>career</category>
      <category>azure</category>
    </item>
    <item>
      <title>Azure AI Search in 2026, how to build a RAG pipeline</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Fri, 10 Jul 2026 23:15:53 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/azure-ai-search-in-2026-how-to-build-a-rag-pipeline-43h8</link>
      <guid>https://dev.to/carlosjcastrog/azure-ai-search-in-2026-how-to-build-a-rag-pipeline-43h8</guid>
      <description>&lt;p&gt;Most RAG tutorials online show the same pattern: take a document, split it into chunks, generate embeddings, store them in a vector database, and when a query arrives search for the nearest vectors and send them to the LLM. It works. The problem is that in production, that pattern fails in ways that don't show up in the tutorial.&lt;/p&gt;

&lt;p&gt;A user types "SNAT error in network configuration". The vector for that query is semantically similar to dozens of documents about NAT, networking, and general network configurations. The vector search returns conceptually relevant results, but the specific document that contains "SNAT" many times over might not be in the top five. Keyword search would have found it immediately.&lt;/p&gt;

&lt;p&gt;Azure AI Search exists to solve exactly that kind of problem, and in 2026 the service has changed enough that much of what was written about it before is outdated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Azure AI Search actually is today
&lt;/h2&gt;

&lt;p&gt;The service was called Azure Search, then Azure Cognitive Search, and since November 2023 it is Azure AI Search. The most recent name that appeared at Build 2026 is Foundry IQ, which is how Microsoft exposes it inside the Azure AI Foundry portal. They are not two separate services: Foundry IQ is Azure AI Search seen from the agent platform.&lt;/p&gt;

&lt;p&gt;Internally, the service combines three search technologies. The full-text engine uses BM25, the same algorithm used by traditional search engines, which scores documents by term frequency and inverse document frequency across the corpus. The vector engine uses HNSW (Hierarchical Navigable Small World) for approximate nearest neighbor search over embeddings. And the Semantic Ranker is a transformer-based cross-encoder model adapted from Microsoft Bing that reorders the results from the other two based on actual semantic relevance.&lt;/p&gt;

&lt;p&gt;These three components can be used separately or in combination. Most production systems use them together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why vector search alone is not enough
&lt;/h2&gt;

&lt;p&gt;An embedding captures general meaning. It works well for conceptual questions like "how does OAuth authentication work", where the intent is semantic and there is no specific term to match. It fails when the query contains very specific terms: product codes, error numbers, technical acronyms, proper names.&lt;/p&gt;

&lt;p&gt;The reason is technical: embedding models compress a lot of semantic information into a fixed-dimension vector. Rare or very specific terms tend to get diluted in that compression process. BM25 does not have that problem because it works with exact term frequencies.&lt;/p&gt;

&lt;p&gt;Hybrid search runs both queries in parallel and merges results using Reciprocal Rank Fusion (RRF). RRF combines ranking lists without requiring the scores from both systems to be on the same scale, using a formula that penalizes results that appear very low in either list. The outcome consistently outperforms either search alone, especially for enterprise content that mixes descriptive text with specific identifiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Semantic Ranker and what it actually does
&lt;/h2&gt;

&lt;p&gt;After hybrid search returns its ranked results, the Semantic Ranker takes the top 50 and reorders them. It uses a cross-encoder model that processes the query and the document snippet together, unlike bi-encoders that generate independent embeddings for each. That difference matters because the cross-encoder can capture subtle relationships like negations and contextual dependencies that vector similarity misses.&lt;/p&gt;

&lt;p&gt;The Semantic Ranker also generates captions (representative snippets from the document) and optionally answers (passages that directly respond to the question if it was phrased as a question). Both captions and answers are verbatim text from the index, not text generated by the model.&lt;/p&gt;

&lt;p&gt;One relevant detail: the Semantic Ranker operates only over text, even in hybrid queries. And it only processes the top 50 results from the previous ranking, which means that if the right document is not in those 50, the ranker cannot recover it. The quality of the initial ranking matters.&lt;/p&gt;

&lt;p&gt;A practical consideration: semantic queries have additional cost. It is not necessary to enable it for all queries. For background search or batch processing where precision is not critical, it can be skipped. For user-facing search where result quality affects the LLM response, it is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two RAG paths in 2026
&lt;/h2&gt;

&lt;p&gt;Since 2026, Azure AI Search offers two distinct approaches for building a RAG pipeline. Microsoft calls them the classic RAG pattern and agentic retrieval, and the choice between them depends on the type of queries you need to handle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Classic RAG
&lt;/h3&gt;

&lt;p&gt;The classic RAG pattern is the proven, generally available approach. You control the full pipeline: generate the search query (or use the user's question directly), run hybrid search with semantic ranking, take the top N most relevant results, and send them as context to the LLM.&lt;/p&gt;

&lt;p&gt;This is the right path when questions are relatively straightforward, you have existing orchestration code you do not want to replace, or you need generally available features for production. The advantage is full control and predictable latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agentic retrieval
&lt;/h3&gt;

&lt;p&gt;Agentic retrieval adds an LLM planning layer before running the search. The system receives the user query, uses an LLM to decompose it into focused subqueries, runs all of them in parallel (each can be keyword, vector, or hybrid), applies semantic ranking to each result, and synthesizes a unified response with references.&lt;/p&gt;

&lt;p&gt;This solves a problem, questions like "find the time-off policies for remote employees hired after 2023" do not have a single search dimension. They are multiple questions combined. A simple vector query tends to capture only part of the meaning.&lt;/p&gt;

&lt;p&gt;The tradeoff is additional latency (the planning step adds time) and the fact that some agentic retrieval features are still in preview as of July 2026. The 2026-04-01 REST API has agentic retrieval generally available for programmatic access. The Azure portal and the Foundry portal still show agentic retrieval as preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Foundry IQ, the new knowledge base model
&lt;/h2&gt;

&lt;p&gt;Foundry IQ is the unified knowledge layer Microsoft built on top of Azure AI Search for the Foundry agent ecosystem. The idea is to solve a problem that appears when an organization scales from one or two agents to dozens: each team ends up rebuilding its own RAG pipeline, with its own vector store, its own chunking logic, and its own access control system.&lt;/p&gt;

&lt;p&gt;With Foundry IQ, you create a knowledge base once and expose it as a shared endpoint for multiple agents. The knowledge base can connect to SharePoint, OneLake, Azure Blob Storage, Fabric IQ, the web, and MCP servers. You do not need to separately configure the retrieval strategy for each source: the system does automatic routing.&lt;/p&gt;

&lt;p&gt;Each knowledge base in Azure AI Search is also a standalone MCP server. Any MCP-compatible client, including Foundry Agent Service, GitHub Copilot, Claude, and Cursor, can invoke the knowledge_base_retrieve tool to query the base. The endpoint follows this pattern:&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;https://&amp;lt;service&amp;gt;.search.windows.net/knowledgebases/&amp;lt;name&amp;gt;/mcp?api-version=&amp;lt;version&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Foundry IQ automatically handles the indexing pipeline for connected sources: ingestion, chunking, vectorization, and enrichment. When you enable Azure Content Understanding on supported sources, complex documents with tables, figures, and headers get layout-aware enrichment without extra engineering work.&lt;/p&gt;

&lt;p&gt;Access control and permissions are part of the design from the start. Foundry IQ uses Entra ID for document-level access control, which means that when an agent queries the knowledge base, it only receives documents the user who started the conversation has access to. That control travels automatically through the system without needing to implement separate security filters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chunking, because it matters more than it seems
&lt;/h2&gt;

&lt;p&gt;Retrieval quality depends heavily on how you split documents before indexing. Chunks that are too large compress too much information into a single embedding and make precise retrieval harder. Chunks that are too small lose context and fragment information that belongs together.&lt;/p&gt;

&lt;p&gt;The recommendation that appears consistently across benchmarks: 512-token chunks with 25% overlap, preserving sentence boundaries. Splitting in the middle of a sentence degrades both the embedding quality and the readability of the fragment when it reaches the LLM.&lt;/p&gt;

&lt;p&gt;Azure AI Search has native vectorization integration that eliminates the need for custom chunking code. You configure the data source, define the skillset (including which embedding model to use), and the indexer handles the complete cycle: extracts text from PDFs and Office documents, splits it into chunks, generates embeddings, and stores them in the index. The indexer can be scheduled to run periodically and keep the index updated when source documents change.&lt;/p&gt;

&lt;p&gt;For large volumes, uploads should be done in batches using the upload_documents API with between 500 and 1,000 documents per batch. Uploading one at a time at scale is not viable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose between the two approaches
&lt;/h2&gt;

&lt;p&gt;Classic RAG works well for: direct questions with a single search dimension, existing pipelines you do not want to change, need for generally available features, latency-critical scenarios where every second counts.&lt;/p&gt;

&lt;p&gt;Agentic retrieval makes sense for: complex conversational queries with multiple implicit questions, agents that need maximum possible relevance, new implementations where the additional latency is acceptable, and scenarios where prior conversation context is relevant for interpreting the current query.&lt;/p&gt;

&lt;p&gt;Foundry IQ makes sense when you are building multiple agents that need access to the same data sources, or when you want to delegate pipeline management to Microsoft and focus on agent logic.&lt;/p&gt;

&lt;p&gt;Microsoft's official documentation recommends starting with vector_semantic_hybrid on the Standard tier as the default starting point, and adding agentic retrieval when complex query patterns appear that classic RAG does not handle well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Azure AI Search is not a vector store with some extra features. It is a retrieval platform with three ranking layers that work together: keyword for precision, vector for semantics, semantic ranker for contextual relevance. Understanding what each layer does and when to enable it is what separates a prototype from a system that performs well in production.&lt;/p&gt;

&lt;p&gt;If you want to explore the official documentation and the agentic retrieval quickstarts:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://learn.microsoft.com/azure/search/?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/search/?wt.mc_id=studentamb_510930&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Official references
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Main documentation: &lt;a href="https://learn.microsoft.com/azure/search/" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/search/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RAG overview: &lt;a href="https://learn.microsoft.com/azure/search/retrieval-augmented-generation-overview" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/search/retrieval-augmented-generation-overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Agentic retrieval overview: &lt;a href="https://learn.microsoft.com/azure/search/agentic-retrieval-overview" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/search/agentic-retrieval-overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Semantic ranking overview: &lt;a href="https://learn.microsoft.com/azure/search/semantic-search-overview" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/search/semantic-search-overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;What's new: &lt;a href="https://learn.microsoft.com/azure/search/whats-new" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/search/whats-new&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Foundry IQ announcement: &lt;a href="https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/foundry-iq-unlocking-ubiquitous-knowledge-for-agents/4470812" rel="noopener noreferrer"&gt;https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/foundry-iq-unlocking-ubiquitous-knowledge-for-agents/4470812&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>azure</category>
      <category>ai</category>
      <category>rag</category>
      <category>microsoft</category>
    </item>
    <item>
      <title>FaroIQ: how I built a 9-agent pipeline for nonprofits with Azure AI Foundry</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Fri, 03 Jul 2026 22:58:55 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/faroiq-how-i-built-a-9-agent-pipeline-for-nonprofits-with-azure-ai-foundry-1gpg</link>
      <guid>https://dev.to/carlosjcastrog/faroiq-how-i-built-a-9-agent-pipeline-for-nonprofits-with-azure-ai-foundry-1gpg</guid>
      <description>&lt;p&gt;I'm Carlos, a Software Design student at the National University of Catamarca, Argentina. This post covers how I built FaroIQ during the Microsoft Agents League 2026: what decisions I made, why I made them, and what problems I ran into along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the idea came from
&lt;/h2&gt;

&lt;p&gt;Growing up in San Juan, Argentina, I watched organizations like Caritas collect donations through schools and universities, doing what they could with very little. The problem was never a lack of will. Those organizations know exactly what their communities need. What they lack is a way to turn that knowledge into something concrete: a plan with data, phases, and a budget.&lt;/p&gt;

&lt;p&gt;When the hackathon started, I wanted to build something that solved that. Not a technology demo. Something that, if someone actually used it, would be useful to them.&lt;/p&gt;

&lt;p&gt;The idea: the user describes their organization in plain language, and the system produces in under 90 seconds a needs analysis, a phased implementation plan, impact projections, a grant proposal ready to submit, and executes all of that in Microsoft 365 (Calendar, To Do, OneDrive, Email) automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  What FaroIQ is
&lt;/h2&gt;

&lt;p&gt;FaroIQ is a community intelligence platform built on the three Microsoft IQ layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Foundry IQ&lt;/strong&gt;: Azure AI Foundry with the &lt;code&gt;gpt-oss-120b&lt;/code&gt; model for agent reasoning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fabric IQ&lt;/strong&gt;: Azure Blob Storage for session persistence and aggregated analytics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Work IQ&lt;/strong&gt;: Microsoft 365 via Azure Logic Apps for execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The name comes from the Spanish word for lighthouse. Not much more to explain there.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why a sequential pipeline instead of parallel agents
&lt;/h3&gt;

&lt;p&gt;One of the first decisions was how to structure the nine agents. Two obvious options: run them in parallel to reduce total time, or run them in sequence where each one receives the output of all previous agents as context.&lt;/p&gt;

&lt;p&gt;I went with the sequence. A coherent action plan requires each stage to build on the previous one. If the Planning agent doesn't know what needs the Analysis agent identified, or what resource capacity the Classification agent estimated, it ends up generating something generic. With context chaining, each agent receives the structured output of all previous agents plus its own system prompt and a JSON schema that defines exactly what it needs to return.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Research&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Live Tavily web search for real statistics and comparable NGO programs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Intake&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Classifies sector, urgency, target population, and resource capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Analyzer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Identifies primary needs, root causes, and existing community strengths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Planner&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generates phased action plan with milestones, quick wins, and risk factors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Evaluator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Projects beneficiaries, feasibility score, and expected outcomes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Critique&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reviews full output and triggers automatic revision if quality falls below 0.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Builds Microsoft 365 payloads and triggers the Logic Apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Grant&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Writes a complete funding proposal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chat&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Conversational agent with full analysis as context, can re-run agents if constraints change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All agents use &lt;code&gt;AsyncAzureOpenAI&lt;/code&gt; against the Azure AI Foundry endpoint. Each event (start, progress, completion) streams to the frontend in real time via WebSocket, so the user watches the pipeline execute rather than staring at a loading screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  The autonomous revision loop
&lt;/h3&gt;

&lt;p&gt;This was the part that took me the longest to think through properly. After the Evaluator finishes, the Critique agent scores the entire pipeline output between 0.0 and 1.0. If the score falls below 0.70, the system doesn't ask the user what to do. It injects the Critique's feedback as additional context and re-runs the Planner and Evaluator with those observations incorporated. Up to two cycles.&lt;/p&gt;

&lt;p&gt;Setting the limit at two was pragmatic: a third cycle rarely improved the result meaningfully, and the total time was already close to 90 seconds. More than two revisions started to feel like a loop the user couldn't predict.&lt;/p&gt;

&lt;p&gt;In the UI, the user can see the quality score, number of cycles completed, critical gaps identified, and specific recommendations. There's an expandable panel that shows exactly what the Critique detected and why it decided to revise. The system doesn't just improve itself silently, it shows its work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fabric IQ: why persist everything
&lt;/h3&gt;

&lt;p&gt;From the start I wanted analyses to survive beyond the browser session. It makes no sense for a nonprofit to lose an analysis because they closed the tab or because the connection dropped on iOS (more on that later).&lt;/p&gt;

&lt;p&gt;Each completed analysis is saved as structured JSON in Azure Blob Storage under the &lt;code&gt;faroiq-lakehouse&lt;/code&gt; container. Each session has an 8-character code derived from the UUID, which lets anyone retrieve it without authentication. A second container called &lt;code&gt;reports&lt;/code&gt; stores the rendered HTML version of the report as a permanent public URL.&lt;/p&gt;

&lt;p&gt;The Intelligence Dashboard aggregates data across all stored sessions: sector distribution, average feasibility scores, urgency trends, cumulative beneficiary projections. Individual analyses become more useful over time as the dataset grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Work IQ: execution in Microsoft 365
&lt;/h3&gt;

&lt;p&gt;Four Azure Logic Apps connect the system to Microsoft 365:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Calendar&lt;/strong&gt;: creates Outlook events for each implementation phase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;To Do&lt;/strong&gt;: generates structured task lists with the organization name as prefix, for example &lt;code&gt;[Caritas San Juan] Phase 1: Diagnosis&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OneDrive&lt;/strong&gt;: saves the full HTML report to &lt;code&gt;/FaroIQ Reports&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email&lt;/strong&gt;: delivers the complete analysis via Outlook&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All Logic Apps calls run as async background tasks in the backend to avoid blocking the API response. If one fails, it gets logged but doesn't surface as an error to the user, because the M365 integrations are secondary to the analysis itself.&lt;/p&gt;

&lt;p&gt;There's also a Declarative Agent manifest in &lt;code&gt;/teams-plugin/appPackage/&lt;/code&gt; for deployment in Microsoft 365 Copilot in enterprise environments, with a nine-step reasoning instruction set and six conversation starters in English and Spanish.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hard parts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Design took longer than expected
&lt;/h3&gt;

&lt;p&gt;I underestimated how much time the interface would take. There's a lot of information to show: the pipeline visualizer with each agent's state, the autonomous revision panel, the full report with tabs, the critique panel, the chat, the M365 integrations. Finding the right visual hierarchy so all of that makes sense without overwhelming the user took quite a few iterations.&lt;/p&gt;

&lt;p&gt;The Three.js lighthouse background was its own challenge. Day mode and night mode render completely different scenes: cream tower with sunlight in day, dark navy with a starfield and volumetric beam at night. My first attempt tried to mutate the scene on theme change, but Three.js doesn't clean up that state cleanly. I ended up forcing a full remount via a &lt;code&gt;key&lt;/code&gt; prop in React every time the theme changes. It's the most direct solution and it works without side effects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping agents from making up data
&lt;/h3&gt;

&lt;p&gt;One requirement I set for myself from the beginning: the system couldn't return invented statistics. If a nonprofit is going to make decisions based on this analysis, the data needs to come from somewhere verifiable.&lt;/p&gt;

&lt;p&gt;That's why the first agent does live web search with Tavily before the rest of the pipeline starts. Results get truncated to 3,000 characters to avoid blowing the token budget of the following agents, but that preserves the relevant information. The Analyzer and Planner have access to real data from the first step.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebSocket and iOS
&lt;/h3&gt;

&lt;p&gt;Keeping a WebSocket connection open for 60 to 90 seconds has problems, especially on iOS. Both Safari and Chrome on iPhone use WebKit, which has stricter connection limits than desktop or Android. The connection closes before the pipeline finishes.&lt;/p&gt;

&lt;p&gt;I solved this in layers. First, a keepalive ping every 8 seconds to keep the connection alive. Second, the backend sends the &lt;code&gt;session_id&lt;/code&gt; to the frontend before the pipeline starts, so the user has the session code from the very first second. If the connection drops, the pipeline keeps running on the server and the user can recover the full analysis by entering that code on the Session Lookup page. Third, an ErrorBoundary component shows the session code in any error state so it's never lost.&lt;/p&gt;

&lt;p&gt;There's no perfect solution for iOS without refactoring all the streaming to Server-Sent Events or polling, which would have meant rewriting significant parts of the backend. The current solution covers the use case without breaking anything else.&lt;/p&gt;




&lt;h2&gt;
  
  
  What surprised me when I ran it the first time
&lt;/h2&gt;

&lt;p&gt;I expected the pipeline to produce something useful. I didn't expect how detailed and specific the output would be when the agents had good input to work with.&lt;/p&gt;

&lt;p&gt;The first time I ran a full analysis on a real nonprofit, the output had specific activities per phase, measurable milestones, risk factors with their mitigations, a funding proposal with theory of change and sustainability plan, and impact projections with confidence intervals. None of that was hardcoded. It came from the context chaining across the nine agents.&lt;/p&gt;

&lt;p&gt;That's when I understood why the added complexity of the sequential approach was worth it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full stack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt; Python 3.12 · FastAPI · uvicorn · AsyncAzureOpenAI · azure-storage-blob · Tavily&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; React 18 · TypeScript · Vite · Three.js · WebSocket&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure:&lt;/strong&gt; Railway (backend, Hobby plan) · Vercel (frontend) · Azure Blob Storage · Azure Logic Apps · Azure AI Foundry&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrations:&lt;/strong&gt; Microsoft 365 (Calendar, To Do, OneDrive, Email) · Microsoft 365 Copilot (Declarative Agent)&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;I finished the hackathon with 227 commits, a production deploy, real sessions from real organizations, a README with screenshots of every feature, a demo video, and the submission delivered.&lt;/p&gt;

&lt;p&gt;When I saw the pipeline visualizer running in real time, the expandable revision panel with the Critique agent's reasoning, the funding proposal generated in seconds, and the tasks showing up automatically in Microsoft To Do, I felt like I had built something that works end to end.&lt;/p&gt;

&lt;p&gt;I entered the Reasoning Agents track at Microsoft Agents League 2026. I didn't win. But FaroIQ is deployed, it's free, and any nonprofit that finds it can use it today. That's reason enough to have built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live demo: &lt;a href="https://faroiq.vercel.app" rel="noopener noreferrer"&gt;faroiq.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/carlosjcastro/faroiq" rel="noopener noreferrer"&gt;github.com/carlosjcastro/faroiq&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Microsoft Agents League 2026 · Reasoning Agents Track · Hack for Good&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>ai</category>
      <category>python</category>
      <category>react</category>
    </item>
    <item>
      <title>How to build production AI agents with Azure AI Foundry Agent Service</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Tue, 23 Jun 2026 23:55:48 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/how-to-build-production-ai-agents-with-azure-ai-foundry-agent-service-4964</link>
      <guid>https://dev.to/carlosjcastrog/how-to-build-production-ai-agents-with-azure-ai-foundry-agent-service-4964</guid>
      <description>&lt;p&gt;For months, the Assistants API in Azure OpenAI was the only real option for building agents on Azure. It worked, but it had clear limits: only OpenAI models, no native multi-agent support, no direct integration with Microsoft's data ecosystem, and an architecture Microsoft was gradually moving away from.&lt;/p&gt;

&lt;p&gt;That chapter closed in March 2026. Azure AI Foundry Agent Service reached general availability and became the official platform for building, deploying, and scaling AI agents on Azure. The classic Assistants API still works, but it has a retirement date: March 31, 2027. If you are starting a new project or thinking about migrating an existing one, Foundry Agent Service is where you should be building.&lt;/p&gt;

&lt;p&gt;This article explains what the service is, how it is organized, what you can build with it, and which technical decisions matter most when designing an agent for production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an agent is and what it is not
&lt;/h2&gt;

&lt;p&gt;Before getting into the service, it is worth being precise about the term because it gets used in very different ways depending on context.&lt;/p&gt;

&lt;p&gt;An agent in the Microsoft Foundry ecosystem is an AI application that uses a model from the Foundry model catalog to reason about user requests and take autonomous actions to fulfill them. Unlike a chatbot that only generates text, an agent can call tools, access external data, and make decisions across multiple steps to complete a task. In some cases, agents do not even have a chat interface at all: they work autonomously in the background, triggered by system events, completing tasks on a user's or organization's behalf.&lt;/p&gt;

&lt;p&gt;The three basic components of any agent are the model, which provides reasoning and language capabilities; the instructions, which define goals, constraints, and behavior; and the tools, which provide access to data or concrete actions like search, file operations, or API calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two agent types in Foundry
&lt;/h2&gt;

&lt;p&gt;Foundry Agent Service organizes agents into two main categories, and the choice between them determines how much code you write and how much control you have.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt agents
&lt;/h3&gt;

&lt;p&gt;Prompt agents are agents you define with instructions, tools, and configuration, and Foundry runs them without you having to write or maintain any application code. No compute to pay for, no containers to optimize, no infrastructure to scale. You define the agent from the Foundry portal or through the SDK, and the service handles the rest.&lt;/p&gt;

&lt;p&gt;These are the right choice when the use case is well-defined, the tools available in Foundry cover what you need, and you do not require complex orchestration logic written by hand. For most enterprise automation, customer support, or data analysis scenarios, prompt agents are sufficient and much faster to implement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosted agents
&lt;/h3&gt;

&lt;p&gt;Hosted agents let you bring your own agent code, packaged as a container, and have Foundry run it with a managed endpoint, automatic scaling, identity, and built-in observability. You can write that code with Agent Framework, LangGraph, the OpenAI Agents SDK, the Anthropic Agent SDK, the GitHub Copilot SDK, or your own code.&lt;/p&gt;

&lt;p&gt;This path makes sense when you need orchestration logic that cannot be expressed with instructions alone, very specific integrations with proprietary systems, or full control over agent behavior at each step. Hosted agents are expected to reach general availability in early July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The technical foundation: Responses API
&lt;/h2&gt;

&lt;p&gt;Foundry Agent Service is built on the OpenAI Responses API. This has an important practical implication: if you already have code running against the Responses API directly, migrating it to Foundry requires minimal changes. What you gain by doing so is enterprise security, private networking, Entra ID access control, full traceability, and evaluation, layered on top of your existing agent logic.&lt;/p&gt;

&lt;p&gt;SDKs are available for Python, JavaScript, TypeScript, Java, and .NET. The stable version is 2.0.0, released in March 2026. Starting with that version, the package bundles &lt;code&gt;openai&lt;/code&gt; and &lt;code&gt;azure-identity&lt;/code&gt; as direct dependencies, so you no longer need to install them separately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;azure.identity&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DefaultAzureCredential&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;azure.ai.projects&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AIProjectClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;azure.ai.projects.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PromptAgentDefinition&lt;/span&gt;

&lt;span class="n"&gt;credential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DefaultAzureCredential&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AIProjectClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AZURE_AI_FOUNDRY_ENDPOINT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;credential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;credential&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a technical support assistant. Answer questions using the available knowledge base.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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="n"&gt;thread_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How do I restart the authentication service?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_and_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thread_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&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;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&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;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Available tools
&lt;/h2&gt;

&lt;p&gt;The real utility of an agent depends on which tools it can invoke. Foundry Agent Service has a wide set of built-in tools and supports external tools via MCP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built-in tools
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;File Search&lt;/strong&gt; lets the agent search through files uploaded to the service, useful for internal knowledge bases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code Interpreter&lt;/strong&gt; runs Python code in an isolated sandbox environment, ideal for data analysis and chart generation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bing Search&lt;/strong&gt; lets the agent search the web to answer questions that require up-to-date information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SharePoint&lt;/strong&gt; is now a first-class knowledge tool, integrated directly into the service. Agents can access SharePoint documents without building a custom RAG pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microsoft Fabric&lt;/strong&gt; through Fabric IQ lets agents query structured data from Microsoft's data platform, including semantic models and ontologies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logic Apps&lt;/strong&gt; opens access to over 1,400 Azure Logic Apps workflows as tools for agents, covering a huge range of integrations with external systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deep Research&lt;/strong&gt; runs a multi-step research process using the Azure OpenAI &lt;code&gt;o3-deep-research&lt;/code&gt; model with Bing Search as the knowledge source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Computer Use&lt;/strong&gt; and &lt;strong&gt;Browser Automation&lt;/strong&gt; are in preview. The first lets the agent interact with desktop application user interfaces. The second allows executing real tasks in the browser through natural language, using isolated Microsoft Playwright sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP and Toolboxes
&lt;/h3&gt;

&lt;p&gt;Model Context Protocol is the open standard Foundry adopted as the primary mechanism for connecting agents with external tools. You can add remote MCP servers directly from the portal catalog, including the Azure DevOps MCP Server in public preview. You can also expose your own tools hosted on Azure Functions through the MCP webhook endpoint.&lt;/p&gt;

&lt;p&gt;Toolboxes, available in public preview since Build 2026, lets you define a curated set of tools once, manage them centrally in Foundry, and expose them through a single MCP-compatible endpoint. Any agent or MCP client can consume a Toolbox regardless of the framework it uses. Toolboxes include explicit versioning to control when changes take effect in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-agent: Connected Agents
&lt;/h2&gt;

&lt;p&gt;When a task is too complex for a single agent, Foundry Agent Service supports multi-agent workflows through Connected Agents, available in preview.&lt;/p&gt;

&lt;p&gt;Connected Agents enable point-to-point interactions where an agent can call other agents as tools to delegate specialized tasks. The primary agent coordinates and the secondary agent handles the subtask, without needing an external orchestrator.&lt;/p&gt;

&lt;p&gt;For more complex workflows, Foundry integrates with the converged runtime for Semantic Kernel and AutoGen, combining AutoGen's dynamic orchestration patterns with Semantic Kernel's modular, production-grade architecture. The result is a unified API for defining, chaining, and managing both single-agent and multi-agent workflows, with consistent behavior between local environments and the cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security for enterprise environments
&lt;/h2&gt;

&lt;p&gt;An agent that accesses internal data, executes actions on production systems, and makes autonomous decisions needs very different security controls than a public chatbot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Entra Agent ID&lt;/strong&gt; gives each agent its own Microsoft Entra identity. This means agents authenticate as entities with their own permissions, not as the user invoking them, which allows applying the principle of least privilege at the agent level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Private networking&lt;/strong&gt; is supported at GA. You can bring your own virtual network (BYO VNet) for a completely isolated environment, with no public egress, container and subnet injection into your network. Private networking extends to tool connectivity, including MCP servers, Azure AI Search, and Fabric data agents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrated content safety&lt;/strong&gt; includes guardrails to reduce unsafe outputs and mitigate prompt injection risks, including Cross-Prompt Injection Attacks (XPIA).&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability in production
&lt;/h2&gt;

&lt;p&gt;Running an agent in production without visibility into what it is doing is not viable. Foundry Agent Service has built-in observability that reached general availability in March 2026.&lt;/p&gt;

&lt;p&gt;Tracing captures the complete end-to-end execution path: requests, tool invocations, and responses, with OpenTelemetry semantics for AI workloads that include memory, state, and planning. Evaluation results link directly to the trace that produced them, so when a regression appears you can go from the score to the exact production trace that exposed it.&lt;/p&gt;

&lt;p&gt;Built-in evaluators cover coherence, relevance, groundedness, retrieval quality, and safety, for both direct generation and RAG scenarios. Custom evaluators, in preview, let you define LLM-as-a-judge evaluation logic aligned to specific business requirements or regulatory standards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distribution: where agents live
&lt;/h2&gt;

&lt;p&gt;An agent without a distribution channel reaches no users. Foundry supports multiple ways to publish agents.&lt;/p&gt;

&lt;p&gt;The most direct is a REST endpoint that any application can consume. For Microsoft environments, agents can be published directly to Microsoft Teams and Microsoft 365 Copilot, with identity, permissions, and policies flowing automatically through the platform's channels. This capability was planned for general availability in June 2026.&lt;/p&gt;

&lt;p&gt;The Entra Agent Registry centralizes the registration and discovery of agents across the organization, so teams can find and reuse existing agents rather than rebuilding them.&lt;/p&gt;

&lt;p&gt;The A2A (agent-to-agent) protocol, in preview, enables communication between agents from different systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to use today
&lt;/h2&gt;

&lt;p&gt;If you come from Azure OpenAI and use the classic Assistants API, it is deprecated and retires on March 31, 2027. The classic service is now documented under &lt;code&gt;/azure/foundry-classic/agents/&lt;/code&gt; on Microsoft Learn. The new service lives under &lt;code&gt;/azure/foundry/agents/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Migrating from the classic Assistants API to the new Agent Service is not trivial but it is not complex either: the official migration guide is available on Microsoft Learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I get started?
&lt;/h2&gt;

&lt;p&gt;Foundry Toolkit for VS Code reached general availability at Build 2026. It lets you create agents from templates or using GitHub Copilot directly in the IDE, debug runs locally with trace visualization, connect Toolboxes, and deploy to Foundry Agent Service without leaving VS Code.&lt;/p&gt;

&lt;p&gt;If you'd like to experiment without installing anything, the Azure AI Foundry portal at ai.azure.com lets you create, configure, debug, and test agents in no-code mode, view conversation threads, add tools, and interact with the agent directly from the interface.&lt;/p&gt;

&lt;p&gt;If you want to explore the official documentation and quickstarts:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://learn.microsoft.com/azure/ai-foundry/agents/?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/ai-foundry/agents/?wt.mc_id=studentamb_510930&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Building agents on Azure no longer requires assembling infrastructure from scratch or depending on APIs that will disappear. Foundry Agent Service covers the runtime, identity, networking, tools, memory, and observability as parts of the service. What remains on the developer's side is deciding what the agent does, with which tools, and how to distribute it. Which is, in the end, the interesting part.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>microsoft</category>
      <category>azure</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Azure AI Foundry, qué es y por qué reemplaza todo lo que conocías de Azure AI</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Sat, 13 Jun 2026 15:04:07 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/azure-ai-foundry-que-es-y-por-que-reemplaza-todo-lo-que-conocias-de-azure-ai-4n11</link>
      <guid>https://dev.to/carlosjcastrog/azure-ai-foundry-que-es-y-por-que-reemplaza-todo-lo-que-conocias-de-azure-ai-4n11</guid>
      <description>&lt;p&gt;Si en algún momento trabajaste con Azure OpenAI Service o Azure AI Studio y hoy entrás a la documentación de Microsoft esperando encontrar lo mismo, probablemente te perdiste. Esto es debido a que, los nombres cambiaron, los recursos se consolidaron, y lo que antes eran tres servicios separados ahora vive bajo un solo nombre: Azure AI Foundry.&lt;/p&gt;

&lt;p&gt;Este post explica qué es Azure AI Foundry en 2026, cómo se diferencia de lo que había antes, y qué necesitás entender si estás empezando o si venís de usar Azure OpenAI directamente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué es Azure AI Foundry
&lt;/h2&gt;

&lt;p&gt;Azure AI Foundry es la plataforma unificada de desarrollo de IA de Microsoft. Reemplazó a Azure AI Studio a fines de 2024 y expandió su alcance más allá de los modelos de OpenAI para incluir Phi, Mistral, Llama, Cohere y muchos más.&lt;/p&gt;

&lt;p&gt;En términos concretos, es el lugar donde Microsoft concentró todo lo que antes estaba disperso: el catálogo de modelos, las herramientas de evaluación y prompt engineering, el despliegue de endpoints, el fine-tuning, la observabilidad y la construcción de agentes. Todo bajo un solo portal en ai.azure.com y una sola capa de recursos en Azure.&lt;/p&gt;

&lt;p&gt;El catálogo tiene más de 1900 modelos que van desde Foundation Models y Reasoning Models hasta Small Language Models, modelos multimodales, modelos de dominio específico y modelos industriales.&lt;/p&gt;

&lt;h2&gt;
  
  
  La diferencia con Azure OpenAI Service
&lt;/h2&gt;

&lt;p&gt;Esta es la confusión más común y es importante aclararla&lt;/p&gt;

&lt;p&gt;Azure OpenAI Service es un servicio administrado enfocado exclusivamente en dar acceso a los modelos de OpenAI como GPT-5, GPT-4o, GPT-4.1, embeddings y modelos multimodales. Corre en la infraestructura de Azure y agrega características empresariales como seguridad con Microsoft Entra ID, filtros de contenido, escalabilidad e integración con herramientas de Azure.&lt;/p&gt;

&lt;p&gt;Azure AI Foundry es una plataforma más amplia orientada a construir, personalizar, desplegar y administrar aplicaciones de IA y agentes. Incluye Azure OpenAI como uno de sus componentes bajo Foundry Models.&lt;/p&gt;

&lt;p&gt;La manera más simple de entenderlo: Azure OpenAI Service sigue existiendo y podés usarlo directamente para llamadas a la API sin necesitar todo el ecosistema de Foundry. Pero si estás construyendo algo más complejo, con múltiples modelos, evaluación, agentes o fine-tuning, Azure AI Foundry es donde todo eso vive hoy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los modelos disponibles
&lt;/h2&gt;

&lt;p&gt;El catálogo incluye modelos de OpenAI, Anthropic Claude, Fireworks AI, DeepSeek, xAI, Hugging Face, Meta, Mistral AI, Cohere, Stability AI, NVIDIA y más.&lt;/p&gt;

&lt;p&gt;Los modelos del catálogo se dividen en dos categorías principales. Los modelos vendidos directamente por Azure son hosteados y vendidos por Microsoft bajo sus propios términos de producto. Microsoft los evaluó y están profundamente integrados en el ecosistema de Azure. Los modelos de partners y comunidad son soportados por sus respectivos proveedores.&lt;/p&gt;

&lt;p&gt;La distinción importa en la práctica porque los modelos directos de Azure vienen con SLA garantizado de Microsoft y soporte de primer nivel, mientras que los modelos de partners tienen niveles variables de soporte. Para producción en entornos empresariales, ese detalle puede ser determinante.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué desapareció y qué va a desaparecer
&lt;/h2&gt;

&lt;p&gt;Hay dos cosas concretas que tenés que tener en cuenta si venís de Azure OpenAI.&lt;/p&gt;

&lt;p&gt;La estructura de recursos colapsó. Antes, un despliegue de IA típico en Azure requería tres recursos separados: una cuenta de Azure OpenAI, una cuenta de Azure AI Services y un AI Hub. Eso se consolidó en un solo recurso de Foundry que hostea múltiples proyectos.&lt;/p&gt;

&lt;p&gt;La Assistants API tiene una fecha de retiro definitiva el 26 de agosto de 2026, reemplazada por la Foundry Agent Service Responses API. Si tenés agentes construidos sobre la Assistants API, el tiempo para migrar es ahora.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que podés hacer desde el portal
&lt;/h2&gt;

&lt;p&gt;El portal de Azure AI Foundry en ai.azure.com te da acceso a todo sin tener que tocar código en primera instancia. Podés explorar el catálogo de modelos, comparar modelos lado a lado usando tus propios datos y prompts, hacer fine-tuning de modelos como GPT-4o, GPT-4o-mini, Llama y Phi, desplegar endpoints con un par de clics, y monitorear el comportamiento de los modelos en producción.&lt;/p&gt;

&lt;p&gt;El Serverless API, también llamado Model as a Service o MaaS, es el tipo de despliegue más accesible para empezar. Te permite acceder a modelos hosteados en Azure sin necesidad de provisionar GPUs ni gestionar infraestructura de backend. Pagás por lo que usás.&lt;/p&gt;

&lt;h2&gt;
  
  
  Desde el código
&lt;/h2&gt;

&lt;p&gt;El SDK oficial para trabajar con Azure AI Foundry desde código es &lt;code&gt;azure-ai-projects&lt;/code&gt;, disponible para Python, JavaScript y TypeScript, y .NET. La versión actual es 2.2.0 y trae soporte para definiciones de agentes externos, skills, toolboxes, registro de pesos de modelos, rutinas y trabajos de optimización.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;azure.ai.projects&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AIProjectClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;azure.identity&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DefaultAzureCredential&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AIProjectClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://tu-endpoint.services.ai.azure.com/api/projects/tu-proyecto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;credential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;DefaultAzureCredential&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_chat_completions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hola, qué es Azure AI Foundry?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para llamadas directas a modelos sin toda la capa de proyectos, podés seguir usando el SDK de Azure OpenAI que ya conocés, ya que los endpoints son compatibles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Y si estudio para la AI-103, ¿dónde encaja esto?
&lt;/h2&gt;

&lt;p&gt;Si estás preparándote para la certificación AI-103, Azure AI Foundry es la plataforma central del examen. Microsoft Foundry reemplazó a Azure AI Studio como el entorno de referencia para construir aplicaciones de IA listas para producción. Entender cómo se organiza el portal, cómo se crean proyectos y cómo se despliegan modelos es conocimiento directamente evaluado.&lt;/p&gt;

&lt;p&gt;Lo mismo aplica si venís del AI-102: la arquitectura que antes estudiabas con Azure Cognitive Services y Azure Applied AI Services ahora está reorganizada dentro de Foundry. Los servicios siguen existiendo, pero el punto de acceso y la forma de gestionarlos cambió.&lt;/p&gt;

&lt;h2&gt;
  
  
  Entonces, ¿cómo empiezo?
&lt;/h2&gt;

&lt;p&gt;Azure AI Foundry tiene una capa gratuita en el portal que te permite explorar el catálogo y probar modelos sin necesitar una suscripción de pago. Para despliegues en producción, el modelo de Serverless API cobra por token consumido dependiendo del modelo que elijas.&lt;/p&gt;

&lt;p&gt;Si querés explorar la documentación oficial y empezar con el portal:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://learn.microsoft.com/azure/ai-foundry/?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/ai-foundry/?wt.mc_id=studentamb_510930&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;La confusión de nombres entre Azure OpenAI, Azure AI Studio y Azure AI Foundry frenó a mucha gente en los últimos meses. La realidad es que la plataforma evolucionó hacia algo más completo y el punto de entrada hoy es uno solo: Foundry. Todo lo demás se accede desde ahí.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Azure Functions para developers que nunca usaron serverless</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Sat, 16 May 2026 03:14:21 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/azure-functions-para-developers-que-nunca-usaron-serverless-4j0k</link>
      <guid>https://dev.to/carlosjcastrog/azure-functions-para-developers-que-nunca-usaron-serverless-4j0k</guid>
      <description>&lt;p&gt;Si alguna vez escuchaste "serverless" y pensaste que era solo marketing, entendible. El nombre confunde porque los servidores siguen existiendo, simplemente dejás de encargarte de ellos. Azure Functions es el servicio serverless de Microsoft y en 2026 es uno de los más usados para construir backends event-driven, APIs livianas, pipelines de datos y, cada vez más, herramientas para agentes de IA.&lt;/p&gt;

&lt;p&gt;Este post es para developers que nunca tocaron serverless y quieren entender qué es en la práctica, no en teoría.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué resuelve Azure Functions
&lt;/h2&gt;

&lt;p&gt;El modelo tradicional para desplegar código en la nube implica provisionar un servidor o contenedor, configurarlo, mantenerlo activo aunque no haya tráfico, y pagar por ese tiempo aunque esté idle. Funciona bien para aplicaciones que reciben tráfico constante, pero es ineficiente para tareas que se ejecutan cuando pasa algo: llega un archivo, alguien hace un request HTTP, se cumple un horario, llega un mensaje a una cola.&lt;/p&gt;

&lt;p&gt;Azure Functions invierte ese modelo. Escribís el código, definís qué lo dispara, y Azure se encarga de ejecutarlo cuando eso pasa. No pagás cuando no corre. No configurás servidores. No pensás en escalado porque el servicio escala solo en respuesta a la demanda.&lt;/p&gt;

&lt;p&gt;El modelo tiene un nombre técnico más preciso: Functions as a Service (FaaS). Serverless es el paraguas más amplio, pero FaaS es lo que Azure Functions implementa concretamente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lenguajes soportados
&lt;/h2&gt;

&lt;p&gt;Azure Functions soporta C#, Java, JavaScript, TypeScript, Python y PowerShell de forma oficial. También tiene soporte para Go y Rust, aunque con menos integración nativa. Si venís del mundo web y trabajás con JavaScript o TypeScript, podés empezar con lo que ya sabés.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los dos conceptos que tenés que entender bien
&lt;/h2&gt;

&lt;p&gt;Antes de escribir una sola línea de código, hay que tener claros dos conceptos que van a aparecer en toda la documentación.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Triggers&lt;/strong&gt; son los eventos que hacen correr tu función. Una función tiene exactamente un trigger. Algunos ejemplos reales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un request HTTP llega a una URL (HTTP trigger)&lt;/li&gt;
&lt;li&gt;Se cumple un horario definido, por ejemplo todos los días a las 3am (Timer trigger)&lt;/li&gt;
&lt;li&gt;Llega un mensaje a una cola de Azure Storage (Queue trigger)&lt;/li&gt;
&lt;li&gt;Se sube un archivo a un blob container (Blob trigger)&lt;/li&gt;
&lt;li&gt;Llega un evento de Azure Event Grid o Event Hubs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Bindings&lt;/strong&gt; son declaraciones que conectan tu función con otros servicios de Azure sin que tengas que escribir el código de esa conexión a mano. Hay bindings de entrada (leer datos) y de salida (escribir datos).&lt;/p&gt;

&lt;p&gt;Un ejemplo concreto: tenés una función que se dispara cuando llega un archivo a Blob Storage, lee ese archivo, lo procesa, y escribe el resultado en una tabla de Cosmos DB. En lugar de escribir el SDK de Blob y el SDK de Cosmos DB dentro de la función, declarás esas conexiones como bindings y Azure se encarga de la autenticación y la inyección de los datos.&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;// Azure Function con HTTP trigger en JavaScript&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;app&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@azure/functions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;http&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;miPrimeraFuncion&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;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&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="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;authLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&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;context&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="s2"&gt;Función ejecutada&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nombre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nombre&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mundo&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Hola, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nombre&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Los planes de hosting y cuál elegir hoy
&lt;/h2&gt;

&lt;p&gt;Esta es la parte donde más gente se pierde porque la documentación lista muchas opciones y no siempre queda claro cuándo usar cada una.&lt;/p&gt;

&lt;p&gt;En 2026, Microsoft recomienda explícitamente el &lt;strong&gt;Flex Consumption plan&lt;/strong&gt; para nuevos proyectos serverless. Es importante saberlo porque mucho material en internet todavía habla del Consumption plan clásico como si fuera la opción estándar, y eso está cambiando.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumption plan&lt;/strong&gt; es el más conocido. Incluye un free grant mensual de 1 millón de ejecuciones y 400,000 GB-s por suscripción. El problema es que no tiene integración con redes virtuales, tiene un límite de ejecución de 10 minutos, y los cold starts en .NET aislado pueden llegar a entre 2 y 7 segundos. Microsoft anunció el retiro del plan Linux Consumption para septiembre de 2028.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flex Consumption plan&lt;/strong&gt; es el reemplazo recomendado. Tiene cold starts más rápidos, integración con virtual networks, escalado por función individual (no todas las funciones del app escalan juntas), concurrencia configurable y múltiples tamaños de instancia. El free grant es de 250,000 ejecuciones y 100,000 GB-s por mes por suscripción. Tiene SLA de 99.95%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Premium plan&lt;/strong&gt; elimina los cold starts completamente con instancias pre-calentadas. El costo es de alrededor de 146 dólares por mes como mínimo, lo que lo hace justificable solo cuando los cold starts afectan la experiencia de usuarios reales.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dedicated plan&lt;/strong&gt; corre en un App Service existente. Tiene sentido si ya tenés infraestructura pagada y querés aprovecharla, pero pierde gran parte del valor de serverless.&lt;/p&gt;

&lt;p&gt;La decisión práctica para alguien que arranca es esta: si el proyecto es nuevo, usá Flex Consumption. Si tenés funciones existentes en el Consumption plan clásico sobre Linux, lo correcto es migrarlas antes de que el plan se retire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Algo que tenés que saber si usás .NET
&lt;/h2&gt;

&lt;p&gt;El modelo in-process de Azure Functions, donde el código corre en el mismo proceso que el host, está programado para retirarse en noviembre de 2026. El modelo que hay que usar hoy es el isolated worker model, que corre el código en un proceso separado. Esto le da más flexibilidad en versiones de .NET y mejor aislamiento, pero si encontrás documentación que habla de in-process como si fuera la opción vigente, ese material está desactualizado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Durable Functions para cuando una ejecución no alcanza
&lt;/h2&gt;

&lt;p&gt;Azure Functions tiene un límite de tiempo de ejecución. Si necesitás orquestar un flujo de trabajo más largo, como procesar miles de archivos en secuencia, coordinar múltiples llamadas a APIs externas, o manejar aprobaciones humanas que pueden tardar horas, existe Durable Functions.&lt;/p&gt;

&lt;p&gt;Durable Functions es una extensión que permite escribir workflows con estado en un entorno serverless. El runtime maneja checkpoints, reintentos y recuperación automáticamente. Si una instancia falla en el medio de un workflow, el sistema retoma desde donde estaba.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Ejemplo de orquestación con Durable Functions en Python
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;azure.durable_functions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;orchestrator_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DurableOrchestrationContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resultado1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ProcesarDatos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resultado2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EnviarEmail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resultado1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resultado2&lt;/span&gt;

&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orchestrator_function&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Azure Functions y agentes de IA en 2026
&lt;/h2&gt;

&lt;p&gt;Una tendencia real en 2026 es usar Azure Functions como infraestructura serverless para agentes de IA. El soporte para servidores MCP (Model Context Protocol) alcanzó disponibilidad general con autenticación OBO (On-Behalf-Of), lo que permite que agentes de IA accedan a herramientas y datos externos de forma segura usando identidades de Microsoft Entra ID.&lt;/p&gt;

&lt;p&gt;En términos prácticos, esto significa que podés deployar un servidor MCP en Azure Functions y que agentes de GitHub Copilot, Microsoft 365 Copilot u otros clientes compatibles puedan invocar las herramientas que exponés, con autenticación empresarial incluida y sin gestionar infraestructura propia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde practicar sin gastar nada
&lt;/h2&gt;

&lt;p&gt;La cuenta gratuita de Azure incluye créditos y el free grant del Consumption plan cubre ampliamente el uso de aprendizaje. Para el Flex Consumption plan, las primeras 250,000 ejecuciones del mes no tienen costo.&lt;/p&gt;

&lt;p&gt;Microsoft Learn tiene un módulo oficial de introducción a Azure Functions que podés seguir en el navegador con un sandbox gratuito, sin necesitar suscripción propia:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://learn.microsoft.com/azure?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure?wt.mc_id=studentamb_510930&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;La curva de entrada de Azure Functions es más suave de lo que parece. Si sabés escribir una función en cualquier lenguaje y entendés qué es un evento, ya tenés lo necesario para empezar. El resto se aprende construyendo cosas reales.&lt;/p&gt;

&lt;p&gt;Carlos José Castro Galante es Desarrollador Full Stack y Azure AI Engineer certificado por Microsoft (AI-102, AI-900, AZ-900) e ITBA. Disponible para proyectos freelance desde Argentina.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>serverless</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
