<?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>How to authenticate Azure apps without storing credentials</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Sat, 05 Sep 2026 18:22:22 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/why-azure-managed-identity-replaces-stored-credentials-and-how-to-use-it-in-2026-29cj</link>
      <guid>https://dev.to/carlosjcastrog/why-azure-managed-identity-replaces-stored-credentials-and-how-to-use-it-in-2026-29cj</guid>
      <description>&lt;p&gt;Every Azure project eventually has the same conversation. Where do we store the connection string? Someone suggests an environment variable, and someone else points out that environment variables end up in deployment pipelines, in Docker compose files, in Terraform state, and occasionally in accidental commits. A secret manager gets proposed, and the secret manager needs its own credentials to access the secrets. The problem recurses.&lt;/p&gt;

&lt;p&gt;Managed Identity doesn't solve the secret manager problem by adding another layer. It removes the credential from the equation entirely for workloads running inside Azure, so the application doesn't authenticate with a stored credential but as itself, using an identity that Azure manages automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Managed Identity actually does
&lt;/h2&gt;

&lt;p&gt;When you enable a Managed Identity on an Azure resource, Azure creates an identity in Microsoft Entra ID tied to that resource's lifecycle. The resource can then request short-lived tokens from the Azure Instance Metadata Service endpoint at &lt;code&gt;169.254.169.254&lt;/code&gt;, which is only reachable from within Azure infrastructure, and those tokens are what the resource uses to authenticate against other Azure services. There's nothing to store, nothing to rotate manually, and nothing that can be leaked in a repository because the credential never exists as a static string anywhere in your codebase or configuration.&lt;/p&gt;

&lt;p&gt;The key property from Microsoft's documentation is precise: managed identities give code running on an Azure resource access to other resources without developers needing to handle or put credentials directly into code. The emphasis on "code running on an Azure resource" matters because Managed Identity only works from within Azure. A local development machine can't reach the Instance Metadata Service endpoint, which means the local development flow still needs an alternative authentication mechanism, typically &lt;code&gt;az login&lt;/code&gt; or a service principal configured for development only.&lt;/p&gt;

&lt;h2&gt;
  
  
  System-assigned vs user-assigned
&lt;/h2&gt;

&lt;p&gt;There are two types of Managed Identity, and Microsoft's current recommendation, updated in its official best practice documentation, is that user-assigned identities are more efficient in a broader range of scenarios.&lt;/p&gt;

&lt;p&gt;A system-assigned identity is created directly on a resource and its lifecycle is tied to that resource, so when the resource is deleted, the identity is deleted too. This sounds convenient but creates a management problem at scale: every resource gets its own identity, every identity needs its own role assignments, and if you have twenty App Services that all need read access to the same storage account, you end up managing twenty separate identities with twenty separate role assignments that have to stay synchronized.&lt;/p&gt;

&lt;p&gt;A user-assigned identity is created as a standalone resource in Azure and can be assigned to multiple resources simultaneously, with its lifecycle independent of any particular resource. If you delete the App Service, the identity persists. You can pre-define what a user-assigned identity can access, get it approved by whoever owns your access control policy, and then assign it to new resources as they're created without going through a new approval cycle each time.&lt;/p&gt;

&lt;p&gt;Microsoft's naming recommendation makes the operational difference clear: name identities after their permission set rather than after the consumer. An identity named &lt;code&gt;id-blogreader-prod-eastus&lt;/code&gt; outlives any specific workload and its purpose stays readable in audit logs, while an identity named &lt;code&gt;id-appsvc-01&lt;/code&gt; doesn't communicate what it can do and its permissions tend to drift over time as the team changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The security mistake most developers make
&lt;/h2&gt;

&lt;p&gt;Turning on Managed Identity and removing the hardcoded credential is the right first step, but stopping there is where most teams leave a significant gap.&lt;/p&gt;

&lt;p&gt;Managed identities do not make a workload secure. They make it credential-free, and that distinction matters because the identity still holds whatever permissions you've granted it, and those permissions are available to anything running on the resource the identity is assigned to. Microsoft's own documentation states this explicitly: if a user has access to install or execute code on a resource with a managed identity, that user has access to everything the identity can reach, even if they have no direct access to those target resources.&lt;/p&gt;

&lt;p&gt;This means that granting a Managed Identity broad permissions like Contributor on a storage account and then assigning it to a shared compute resource where multiple teams run code effectively elevates every team's access to that storage account. The credential problem is gone but the permission problem remains and is now implicit rather than visible in a secrets manager.&lt;/p&gt;

&lt;p&gt;The practical fix is applying least-privilege role assignments: instead of Contributor, assign Storage Blob Data Reader if the workload only reads blobs, and instead of Key Vault Administrator, assign Key Vault Secrets User if the workload only reads secrets. Azure RBAC has purpose-specific built-in roles for most common scenarios, and using them reduces the blast radius if a workload is ever compromised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using DefaultAzureCredential
&lt;/h2&gt;

&lt;p&gt;The practical implementation for most teams is &lt;code&gt;DefaultAzureCredential&lt;/code&gt;, part of the Azure Identity SDK and available for Python, JavaScript, Java, and .NET. It tries a sequence of authentication methods in order and uses the first one that succeeds, which means the same code works in both local development and in Azure without any environment-specific branching.&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.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.storage.blob&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BlobServiceClient&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;BlobServiceClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;account_url&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://mystorageaccount.blob.core.windows.net&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a local environment, &lt;code&gt;DefaultAzureCredential&lt;/code&gt; typically picks up the credentials from &lt;code&gt;az login&lt;/code&gt;, and on an Azure resource with Managed Identity enabled it picks up the managed identity token from the Instance Metadata Service. The same line of code handles both cases.&lt;/p&gt;

&lt;p&gt;The order of credential resolution matters when debugging authentication issues. &lt;code&gt;DefaultAzureCredential&lt;/code&gt; tries EnvironmentCredential, WorkloadIdentityCredential, ManagedIdentityCredential, SharedTokenCacheCredential, VisualStudioCodeCredential, AzureCliCredential, AzurePowerShellCredential, and AzureDeveloperCliCredential in that sequence. If you're seeing unexpected authentication behavior in local development, it's usually because an earlier credential in the chain is being picked up unexpectedly, typically an environment variable in the shell that's overriding the &lt;code&gt;az login&lt;/code&gt; credential.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DefaultAzureCredential&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="s2"&gt;@azure/identity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SecretClient&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="s2"&gt;@azure/keyvault-secrets&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;credential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DefaultAzureCredential&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SecretClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://mykeyvault.vault.azure.net&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;credential&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;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-database-connection-string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where Managed Identity doesn't work
&lt;/h2&gt;

&lt;p&gt;Two scenarios where a service principal with a client secret or certificate is still the right choice: workloads running outside Azure that can't reach the Instance Metadata Service, and federated scenarios where the workload needs to authenticate to Azure from a non-Azure environment like GitHub Actions or another cloud provider. For GitHub Actions specifically, Microsoft supports OIDC-based federation that eliminates the stored secret problem without requiring the workload to run in Azure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2026 shift toward identity-first
&lt;/h2&gt;

&lt;p&gt;Azure's direction in 2026 is increasingly identity-first by default, with system-assigned managed identities now the default for Kubernetes workloads on AKS, replacing API keys in that context. The pattern is becoming a baseline expectation rather than an advanced configuration, and for new projects starting on Azure, configuring Managed Identity from the beginning is significantly easier than retrofitting it into an existing codebase that passes credentials around as strings.&lt;/p&gt;

&lt;p&gt;For the full Microsoft documentation on Managed Identity and how to configure it for specific Azure services:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;Managed identities for Azure resources - Overview&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/managed-identity-best-practice-recommendations?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;Best practice recommendations for managed identities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview-for-developers?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;Managed identities for Azure resources - Developer introduction and guidelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/azure/developer/python/sdk/authentication/credential-chains?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;Use DefaultAzureCredential in an application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/azure/app-service/overview-managed-identity?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;Managed identity for Azure App Service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/azure/security/fundamentals/identity-management-best-practices?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;Azure identity management and access control best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Information based on official Microsoft documentation and verified sources as of September 2026. Azure services and recommendations may change. Verify current guidance at Microsoft Learn before implementing in production environments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>security</category>
      <category>cloud</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to activate Azure for Students without a university email in 2026</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Wed, 02 Sep 2026 14:07:32 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/how-to-activate-azure-for-students-without-a-university-email-in-2026-5d98</link>
      <guid>https://dev.to/carlosjcastrog/how-to-activate-azure-for-students-without-a-university-email-in-2026-5d98</guid>
      <description>&lt;p&gt;Azure for Students gives enrolled university students $100 in Azure credits, access to free services, and no credit card requirement. The standard path to activate it assumes your university email domain is registered in Microsoft's verification system, and for many institutions, especially in regions outside North America and Europe, it simply isn't. When you enter your university email and get the message "your email domain is not currently registered with us", that's not a rejection. It's the system telling you to use a different verification method.&lt;/p&gt;

&lt;p&gt;There are two paths that work when your institutional domain isn't recognized: verifying with official academic documentation, or using the GitHub Student Developer Pack as proof of student status. Both are supported by Microsoft and SheerID, the verification provider behind Azure for Students.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need before starting either path
&lt;/h2&gt;

&lt;p&gt;Regardless of which path you take, you need a Microsoft account. If you already have an Outlook or Hotmail account, that works. If you don't, create one at account.microsoft.com before anything else. This is the account that will hold your Azure for Students subscription and the same one you will use later if you apply to the Microsoft Student Ambassadors program, so use one you plan to keep active.&lt;/p&gt;

&lt;p&gt;Your Microsoft account does not need to be linked to your university email. A personal Gmail cannot be a Microsoft account on its own, but you can create a Microsoft account using a Gmail address at signup.live.com. Either way, what matters is that you have a stable Microsoft account ready before starting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 1: Documentation-based verification
&lt;/h2&gt;

&lt;p&gt;This path works when you don't have access to a recognized institutional email and you're not yet verified on GitHub Education. It requires uploading an official document that proves your current enrollment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Go to the Azure for Students page&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Navigate to &lt;code&gt;azure.microsoft.com/free/students&lt;/code&gt; and click the "Activate now" button. Sign in with your Microsoft account when prompted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Enter your information&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The form will ask for your email address. Enter your Microsoft account email, not your university email. When the system cannot verify the domain automatically, it will present an alternative verification option where you can submit academic documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Upload your enrollment document&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The document that works is an official enrollment certificate or student registration certificate issued by your university. It needs to show your full name, your institution's name, and confirmation of your current enrollment status. A student ID card alone is generally not sufficient because it doesn't always show the enrollment year or current academic status. An official letter or certificate from the registrar's office is the correct document.&lt;/p&gt;

&lt;p&gt;In my case I uploaded a constancia de alumno regular, which is the standard enrollment certificate issued by Argentine universities, and the verification was accepted without issues.&lt;/p&gt;

&lt;p&gt;The verification is processed by SheerID. In most cases it completes within minutes, but it can take up to 48 hours. If it hasn't resolved after 48 hours, you can contact Azure Education Support at &lt;code&gt;azureforeducation.microsoft.com/institutions/contact&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Confirm activation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once verified, you'll receive a confirmation email and your Azure for Students subscription will appear in the Azure portal at &lt;code&gt;portal.azure.com&lt;/code&gt;. The subscription includes $100 in credits valid for 12 months and access to a set of always-free services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 2: GitHub Student Developer Pack
&lt;/h2&gt;

&lt;p&gt;This path works if you already have or plan to get the GitHub Student Developer Pack. GitHub Education accepts enrollment documents and student ID cards for verification, and once approved, the Pack can be used to activate Azure for Students directly without repeating the documentation process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Apply for the GitHub Student Developer Pack&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to &lt;code&gt;education.github.com/pack&lt;/code&gt; and click "Get student benefits". Sign in with your GitHub account and follow the verification flow. You'll need to provide your university name, your enrollment email if you have one, and upload a proof of enrollment document if your email isn't recognized. A student ID, enrollment certificate, or official letter from your university all work here. GitHub's verification usually takes up to 72 hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Wait for confirmation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GitHub sends a confirmation email when your student status is verified. The Pack and all its benefits become active at that point. Don't try to activate Azure before receiving that confirmation because the verification link won't work until GitHub completes the process on their end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Activate Azure for Students through the Pack&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once verified, go to &lt;code&gt;signup.azure.com/studentverification&lt;/code&gt; and sign in with your Microsoft account. When the page asks how you want to verify your student status, select the option to connect with GitHub and complete the GitHub OAuth step. This confirms to Microsoft that you're a verified student through GitHub Education.&lt;/p&gt;

&lt;p&gt;After completing the OAuth step, wait a few minutes and then open the Azure portal at &lt;code&gt;portal.azure.com&lt;/code&gt;. Your Azure for Students subscription should appear active in the subscriptions list. If it doesn't appear after 10 minutes, try signing out and back in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you get an error saying your GitHub account doesn't have student benefits enabled&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This error can appear even when your Student Developer Pack is active and verified. The most reliable fix is to open a support ticket with GitHub Education by going to &lt;code&gt;support.github.com&lt;/code&gt; and selecting "I am a verified student having trouble redeeming my Global Campus offers." GitHub support typically resolves these within a few hours.&lt;/p&gt;

&lt;p&gt;Do not try to activate Azure multiple times before resolving this error. Azure for Students is limited to one activation per Microsoft account. If you trigger multiple failed attempts, the offer may become unavailable for that account and you'll need to contact Azure Education Support to reset it.&lt;/p&gt;

&lt;h2&gt;
  
  
  If both paths fail: manual support ticket
&lt;/h2&gt;

&lt;p&gt;Neither path above is guaranteed to work in every situation. The official Azure for Students documentation states that verification requires an institutional email address, and the document-upload option in the self-service flow is not available in all regions or for all institutions. If you complete both paths and still cannot activate the subscription, a manual support ticket is the next step.&lt;/p&gt;

&lt;p&gt;This process is not officially documented as a standard path, but multiple Microsoft Q&amp;amp;A threads from 2026 confirm that Microsoft's education support team can manually review student eligibility when the automated system fails. The outcome is not guaranteed, but it is the correct escalation channel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to prepare before opening a ticket:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You will need clear scans or photos of your student ID card showing your name and current academic year, an official enrollment certificate or letter from your university's registrar office showing your full name, institution name, and current enrollment period, and a screenshot of the error or rejection message you received during the self-service verification flow. If you completed the GitHub Student Developer Pack, include a screenshot confirming your approved status there as well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to open the ticket:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you already have any Azure account (including a free account), go to &lt;code&gt;portal.azure.com&lt;/code&gt;, click "Help + Support" in the top right corner, and create a new support request. Select "Subscription Management" as the issue type, then "Account" and "Azure for Students verification" as the subcategories. Attach your documents and explain that your institution does not provide an institutional email address and that you are requesting manual verification.&lt;/p&gt;

&lt;p&gt;If you do not have any Azure account yet and cannot access the portal, use the Azure for Education contact form directly:&lt;/p&gt;

&lt;p&gt;👉 &lt;code&gt;azureforeducation.microsoft.com/institutions/contact&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You can also post your situation with full details in the official Microsoft Q&amp;amp;A forum at &lt;code&gt;learn.microsoft.com/answers&lt;/code&gt;, where Microsoft moderators actively respond to student verification questions and can escalate your case internally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alternative if manual verification is not granted:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If Microsoft confirms that your institution is not supported and manual verification cannot be completed, the Azure for Students Starter program at &lt;code&gt;azure.microsoft.com/free/students/starter&lt;/code&gt; provides access to a limited set of free Azure services without requiring student verification or a credit card. It does not include the $100 credit, but it gives access to services like App Service, Azure Functions, and Azure SQL Database at no cost, which may be sufficient for learning purposes.&lt;/p&gt;




&lt;p&gt;Once your Azure for Students subscription is active, the Microsoft account holding it is the one you use for everything related to Microsoft programs. If you plan to apply to the Microsoft Student Ambassadors program, you'll register at &lt;code&gt;studentambassadors.microsoft.com&lt;/code&gt; using this same Microsoft account. The program verifies your Azure for Students subscription during registration, and the button to proceed only becomes active once that subscription is confirmed.&lt;/p&gt;

&lt;p&gt;To check your credit balance, remaining time, and the services included in your subscription, go to the Azure portal at &lt;code&gt;portal.azure.com&lt;/code&gt; and look for the &lt;strong&gt;Education&lt;/strong&gt; section in the left navigation menu. That is where Azure for Students accounts track their credits and usage, separately from the general Subscriptions section that paid accounts use. The $100 in credits is valid for 12 months from activation, and unused credits do not roll over, so it's important starting to explore Azure services once the subscription is active.&lt;/p&gt;

&lt;p&gt;For a full guide on the Microsoft Student Ambassadors program and how to get started after activating Azure for Students:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://learn.microsoft.com/azure/education-hub/azure-dev-tools-teaching/azure-students-program?wt.mc_id=studentamb_510930" rel="noopener noreferrer"&gt;https://learn.microsoft.com/azure/education-hub/azure-dev-tools-teaching/azure-students-program?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 Microsoft documentation, GitHub Education documentation, and personal experience as of September 2026. Verification processes, eligibility requirements, and available benefits may change at any time without notice. This article is provided for informational purposes only. The author is not responsible for any issues arising from following these steps, including but not limited to failed verifications, credit losses, or account restrictions. Always verify current requirements directly on the official Azure for Students page and the GitHub Student Developer Pack page before proceeding.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>microsoft</category>
      <category>cloud</category>
      <category>learning</category>
    </item>
    <item>
      <title>Azure OpenAI Service vs OpenAI API, which to use and when in 2026</title>
      <dc:creator>Carlos José Castro Galante</dc:creator>
      <pubDate>Wed, 26 Aug 2026 00:12:06 +0000</pubDate>
      <link>https://dev.to/carlosjcastrog/azure-openai-service-vs-openai-api-which-to-use-and-when-in-2026-1nic</link>
      <guid>https://dev.to/carlosjcastrog/azure-openai-service-vs-openai-api-which-to-use-and-when-in-2026-1nic</guid>
      <description>&lt;p&gt;When someone asks whether to use Azure OpenAI Service or the direct OpenAI API, the starting point is this: the models running on both platforms are identical. GPT-4o, GPT-5, and the o-series models you deploy on Azure have the same weights, the same capabilities, and the same output quality as the ones you call from platform.openai.com, and what changes between the two platforms is the infrastructure where they run, the authentication mechanism, and the compliance guarantees the provider can offer on those requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in 2026
&lt;/h2&gt;

&lt;p&gt;Azure AI Foundry was renamed Microsoft Foundry on January 1, 2026, and Azure OpenAI Service now lives inside that unified platform alongside the model catalog, development tooling, and agents. References to Microsoft Foundry in new documentation point to what used to be Azure AI Foundry.&lt;/p&gt;

&lt;p&gt;In July 2026, the GPT-5.6 family arrived with Sol, Terra, and Luna available on Azure the same day as on the direct OpenAI API. Historically Azure lagged four to eight weeks behind new model releases because Microsoft validates them within their compliance frameworks before making them available, and while that gap still exists for some specific features and APIs, for the main models in the GPT-5 family availability is converging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where data is processed
&lt;/h2&gt;

&lt;p&gt;When you call GPT-4o from the OpenAI API, the request goes to OpenAI's own infrastructure, which is centralized and gives you no control over which region processes your data. For most use cases that doesn't matter, but for organizations with data residency requirements, regulatory compliance needs, or industries like healthcare, banking, or government, that detail can determine whether the service is usable at all.&lt;/p&gt;

&lt;p&gt;Azure OpenAI runs the same models within the boundary of your Azure tenant, so the data you send in prompts doesn't leave to OpenAI's infrastructure but processes in the Azure regions you choose. That's what makes it possible to meet HIPAA, SOC 2, EU data residency, and other certifications that companies in regulated industries need before they can deploy to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication
&lt;/h2&gt;

&lt;p&gt;The OpenAI API uses API keys, strings you need to store, rotate, distribute, and protect from ending up in a repository. Azure OpenAI can authenticate using &lt;code&gt;DefaultAzureCredential&lt;/code&gt;, which delegates authentication to Microsoft Entra ID and can use Managed Identity so the service obtains tokens automatically without any hardcoded or stored credentials anywhere.&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.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;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AzureOpenAI&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;token&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="nf"&gt;get_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://cognitiveservices.azure.com/.default&lt;/span&gt;&lt;span class="sh"&gt;"&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;AzureOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;azure_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://my-resource.openai.azure.com/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;azure_ad_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-04-01-preview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In environments where a security review is required before deploying anything to production, being able to remove API keys from the authentication flow carries practical weight in the approval process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing and billing modes
&lt;/h2&gt;

&lt;p&gt;Prices for the main models are comparable between the two platforms. The GPT-5.6 family in Global Standard on Azure follows the same OpenAI list rates, with ranges from $0.20 to $5 per million input tokens for mainstream models in the current catalog. For the GPT-5.6 Sol model, Azure announced promotional pricing of $4.00 per million input tokens and $20.00 per million output tokens from September 1 through at least November 30, 2026.&lt;/p&gt;

&lt;p&gt;Azure has Provisioned Throughput Units (PTUs), reserved capacity blocks you pay for hourly rather than per token. When your application's sustained usage exceeds 60-70% of a PTU's capacity, that mode starts making economic sense. Below that threshold, pay-as-you-go per token is more efficient.&lt;/p&gt;

&lt;p&gt;Something most pricing guides don't mention is that in enterprise deployments with private networking, Azure adds supporting infrastructure costs including Azure AI Search, Blob Storage, private endpoints, and network egress that can add 15 to 40% on top of token costs in full private networking production deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content filtering
&lt;/h2&gt;

&lt;p&gt;OpenAI has baseline moderation that runs on all requests. Azure OpenAI lets you configure custom filters through Azure AI Content Safety, with granular control over the thresholds for each content category based on your application's specific needs. For medical use cases where certain clinical terms might trigger general filters, that granularity can be necessary for the service to work correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use each one
&lt;/h2&gt;

&lt;p&gt;The direct OpenAI API makes sense when you're prototyping and need setup speed without bureaucracy, when you want immediate access to new features without waiting for Azure's validation cycle, or when you're building a consumer application where compliance requirements aren't a blocker. It has no quota gate on the standard tier, so you can start calling the API the same day without requesting additional capacity.&lt;/p&gt;

&lt;p&gt;Azure OpenAI makes sense when the project needs to meet specific regulations for the industry, when you need data to process within a specific Azure region, when the organization already has contracts and governance on Azure and needs the AI service integrated into that ecosystem, or when you need credential-free authentication through Managed Identity. Quota requests for high-demand models in specific regions can take a week or more to process, so starting that process early helps.&lt;/p&gt;

&lt;p&gt;A common and perfectly valid scenario is using both: you prototype with the OpenAI API which gives immediate access to everything, and when the project moves to production with compliance or private networking requirements, you migrate to Azure OpenAI with minimal code changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating from one to the other
&lt;/h2&gt;

&lt;p&gt;If you already have code pointing to the OpenAI API and want to move it to Azure, the change is configuration rather than logic. The client changes from &lt;code&gt;OpenAI&lt;/code&gt; to &lt;code&gt;AzureOpenAI&lt;/code&gt;, you add your Azure resource endpoint and &lt;code&gt;api_version&lt;/code&gt;, and the rest of the code stays the same. Model names change because in Azure you deploy named instances rather than calling the model by its global name.&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;# Before (direct OpenAI)
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&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;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After (Azure OpenAI)
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AzureOpenAI&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;AzureOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;azure_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://my-resource.openai.azure.com/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-04-01-preview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Managed Identity, the &lt;code&gt;api_key&lt;/code&gt; disappears from the code entirely and is replaced by the Entra ID credential flow shown above.&lt;/p&gt;

&lt;p&gt;To explore Azure OpenAI Service and the model catalog available in Microsoft Foundry:&lt;/p&gt;

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




&lt;p&gt;&lt;em&gt;Information based on official Microsoft documentation and verified sources as of August 25, 2026. Prices and model availability may change. Verify current figures on the official Azure OpenAI pricing page before making architecture or budget decisions.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>webdev</category>
    </item>
    <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>
  </channel>
</rss>
