<?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: &lt;devtips/&gt;</title>
    <description>The latest articles on DEV Community by &lt;devtips/&gt; (@dev_tips).</description>
    <link>https://dev.to/dev_tips</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2901662%2F1dcce5de-7920-43a0-a337-e1dfb375b204.png</url>
      <title>DEV Community: &lt;devtips/&gt;</title>
      <link>https://dev.to/dev_tips</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dev_tips"/>
    <language>en</language>
    <item>
      <title>I replaced GitHub Copilot with 3 tools. My team noticed within a week.</title>
      <dc:creator>&lt;devtips/&gt;</dc:creator>
      <pubDate>Thu, 21 May 2026 06:27:02 +0000</pubDate>
      <link>https://dev.to/dev_tips/i-replaced-github-copilot-with-3-tools-my-team-noticed-within-a-week-244k</link>
      <guid>https://dev.to/dev_tips/i-replaced-github-copilot-with-3-tools-my-team-noticed-within-a-week-244k</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="d477"&gt;&lt;strong&gt;Cursor, Claude Code, and Windsurf didn’t just change how I code they changed what my PRs look like.&lt;/strong&gt;&lt;/h2&gt;
&lt;span&gt;&lt;/span&gt;&lt;h2 id="34a1"&gt;The Copilot breakup nobody talks about&lt;/h2&gt;
&lt;p id="f80a"&gt;I didn’t plan to replace GitHub Copilot. It was fine. It’s still fine. But “fine” stopped being good enough somewhere around month three of watching teammates ship faster than me while I was still waiting for an autocomplete suggestion that missed the point.&lt;/p&gt;
&lt;p id="84a1"&gt;So I started experimenting. Not with one tool. With everything. Forty-something IDEs, agents, plugins, and CLI tools over four months, run against real work not demos, not tutorials, actual PRs that had to pass review. Most of them lasted a week. A few lasted a day. One deleted a file I needed and I don’t want to talk about it.&lt;/p&gt;
&lt;p id="7f36"&gt;Three survived.&lt;/p&gt;
&lt;p id="d4a8"&gt;Cursor for multi-file feature work. Claude Code for everything that lives in the terminal. Windsurf for the days when I need to stay in flow without managing the AI every five minutes. By the end of week one my team lead asked what I’d changed. By week three two teammates had switched.&lt;/p&gt;
&lt;p id="057d"&gt;This isn’t a sponsored comparison post. None of these companies know I exist. It’s just what happened when I stopped treating AI coding tools as a category and started treating them as team members with different strengths.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="fd63"&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Copilot is fine for autocomplete. If that’s still your whole AI coding stack in 2026 you’re leaving serious velocity on the table. Cursor, Claude Code, and Windsurf each own a different slot in the workflow and together they cover everything Copilot never could.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="1365"&gt;Why Copilot stopped being enough&lt;/h2&gt;
&lt;p id="1430"&gt;Let me be fair to Copilot first. It’s not bad. For a dev who mostly works in one file at a time, writes straightforward code, and doesn’t need much more than smart autocomplete it does the job. I used it for over a year and I was mostly happy.&lt;/p&gt;
&lt;p id="e0d1"&gt;The problem isn’t what Copilot does. It’s what it doesn’t do.&lt;/p&gt;
&lt;p id="777e"&gt;Copilot watches what you’re typing and tries to finish the sentence. That’s the whole model. It doesn’t know why you’re writing what you’re writing. It doesn’t know what the rest of the codebase looks like. It doesn’t know that the function you’re building has to fit into an auth system three files away or that your team has a convention for error handling that isn’t in any docs. It makes educated guesses based on what’s visible in the current file and what it saw during training.&lt;/p&gt;
&lt;p id="5c97"&gt;For a while that’s enough. Then your codebase grows. Your features get more interconnected. A change to one interface ripples through six files. A refactor touches the API layer, the service layer, and the tests simultaneously. And suddenly you’re doing all the thinking that the AI should be helping with, while it cheerfully suggests the wrong variable name.&lt;/p&gt;
&lt;p id="8a03"&gt;The real cost isn’t the bad suggestions. It’s what you do with them. You stop, evaluate, reject, retype. You context switch out of the problem you were solving to manage the tool that was supposed to help you solve it. That friction is small per instance and significant over a day.&lt;/p&gt;
&lt;p id="c286"&gt;I started noticing it when I’d spend twenty minutes on something I expected to take five. Not because the problem was hard. Because I was fighting my tools to get there.&lt;/p&gt;
&lt;p id="c17e"&gt;That’s when I started looking for something different. Not a better autocomplete. A different category of help entirely.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="a575"&gt;Cursor the IDE that started arguing back&lt;/h2&gt;
&lt;p id="25f9"&gt;I thought AI-assisted coding meant better autocomplete. Then Cursor refactored a function I didn’t ask it to touch, the PR passed review without a single comment, and I had to sit with that for a minute.&lt;/p&gt;
&lt;p id="d6f4"&gt;Cursor isn’t a smarter Copilot. It’s a different thing entirely. Where Copilot watches what you’re typing and tries to finish the sentence, Cursor reads your whole codebase and forms opinions about it. You can ask it to build a feature and it’ll touch six files, write the tests, and explain what it changed and why.&lt;/p&gt;
&lt;h3 id="ef22"&gt;What I actually use it for:&lt;/h3&gt;
&lt;ul&gt;

&lt;li id="2569"&gt;

&lt;strong&gt;Multi-file edits&lt;/strong&gt; This is where it earns its keep. I don’t use Cursor for writing single functions. I use it when a change needs to ripple across the codebase updating an interface, migrating an API, refactoring auth logic. It plans the changes, shows you the diff across every affected file, and lets you approve before anything gets written.&lt;/li&gt;

&lt;li id="9cf9"&gt;

&lt;strong&gt;.cursorrules&lt;/strong&gt; Drop this file in the root of your project. Cursor reads it at the start of every session preferred patterns, things to avoid, naming conventions and actually respects that context every time.&lt;/li&gt;

&lt;/ul&gt;
&lt;pre&gt;&lt;span id="c241"&gt;# .cursorrules&lt;br&gt;You are working on a Node.js REST API.&lt;br&gt;Always use async/await. Never use callbacks.&lt;br&gt;Prefer explicit error handling. Never swallow errors silently.&lt;br&gt;Add JSDoc comments to all exported functions.&lt;/span&gt;&lt;/pre&gt;
&lt;p id="b56e"&gt;&lt;em&gt;This alone cuts the back-and-forth in half.&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="aca7"&gt;

&lt;strong&gt;Cmd+K inline editing&lt;/strong&gt; Highlight a block, hit Cmd+K, describe what you want. Rewrites in place. No sidebar, no context switching, no copy-paste. Fastest way to refactor something small without losing your train of thought.&lt;/li&gt;

&lt;li id="1e18"&gt;

&lt;strong&gt;Agent window Cursor 3&lt;/strong&gt; April 2026, Cursor shipped a new interface built from scratch around agents. Multiple agents running in parallel across different repos, all visible in one sidebar. You dispatch tasks, review diffs, approve changes. It looks less like a code editor and more like an engineering manager’s dashboard. In a good way.&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="d055"&gt;I still write code manually. Cursor doesn’t replace that. But for anything involving more than one file, more than one decision, or more than five minutes of thinking I hand it off and review the output. That’s a different relationship with your tools than most devs are used to, and it takes about a week to actually trust it.&lt;/p&gt;
&lt;p id="fc2e"&gt;Worth it.&lt;/p&gt;
&lt;p id="7055"&gt;&lt;a href="https://cursor.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;cursor.com&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="700" height="382" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A700%2F1%2AkOVbgHHwXHsuAxxqYYEzdw.png"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="2eae"&gt;Claude Code the terminal agent I didn’t know I needed&lt;/h2&gt;
&lt;p id="c445"&gt;Most AI coding tools live inside your editor. Claude Code lives in your terminal. No IDE. No sidebar. No chat window. You talk to it like a senior engineer who already read the repo, and it writes files, runs commands, fixes test failures, and ships. It’s not a chatbot with code awareness. It’s a collaborator.&lt;/p&gt;
&lt;p id="97aa"&gt;The first time it ran a full test suite, found a failing edge case I hadn’t noticed, fixed it, and committed while I was reviewing a completely different PR I had to close my laptop and think about my life choices for a moment.&lt;/p&gt;
&lt;p id="6e94"&gt;&lt;strong&gt;What I actually use it for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li id="15ff"&gt;

&lt;strong&gt;Natural language task execution&lt;/strong&gt; You describe what you want in plain English. It reads the repo, makes a plan, executes it, and tells you what it did. No hand-holding, no step-by-step prompting.&lt;/li&gt;&lt;/ul&gt;
&lt;pre&gt;&lt;span id="19ff"&gt;$ claude &lt;span&gt;"refactor the auth middleware to use JWT RS256,&lt;br&gt;run the tests, and fix anything that breaks"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="2e9f"&gt;&lt;em&gt;It reads the codebase, plans the changes, runs the tests, iterates on failures without you touching anything.&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="523d"&gt;

&lt;strong&gt;Terminal-native workflow&lt;/strong&gt; Claude Code lives where backend and DevOps work actually happens. No tab switching, no copy-pasting output into a chat window, no losing context. You stay in the terminal, it stays in the terminal, and the whole session feels like pairing with someone who never gets tired or distracted.&lt;/li&gt;

&lt;li id="5aff"&gt;

&lt;strong&gt;MCP tool integrations&lt;/strong&gt; Claude Code connects to external tools via MCP GitHub, databases, deployment pipelines. You can give it real reach beyond just the codebase and it handles multi-step workflows that would normally take you 20 manual commands.&lt;/li&gt;

&lt;li id="1da8"&gt;

&lt;strong&gt;Computer use&lt;/strong&gt; On Mac, Claude Code can open apps, click through UI, take a screenshot, and verify the result. Build a feature, launch it, test it visually from one terminal session. That one still gets me every time.&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="fb33"&gt;I still keep human oversight on anything production-critical. But for local dev, test environments, and deploy pipelines it runs, I review. That’s the whole loop.&lt;/p&gt;
&lt;p id="4801"&gt;&lt;a href="https://docs.anthropic.com/en/docs/claude-code/overview" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;docs.anthropic.com/claude-code&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="cdad"&gt;Windsurf the dark horse nobody warned me about&lt;/h2&gt;
&lt;p id="339c"&gt;Everyone I know is on Cursor. Windsurf kept coming up in my Discord as the tool people switched to and then got annoyingly quiet about like they’d found something they didn’t want to share yet.&lt;/p&gt;
&lt;p id="88f7"&gt;I tried it out of mild spite. They were right and I was annoyed about it.&lt;/p&gt;
&lt;p id="55b0"&gt;Windsurf isn’t trying to beat Cursor on features. It’s trying to beat it on feel. The whole thing is built around Cascade an agentic AI that doesn’t wait for you to ask it something. It watches what you’re doing, understands the context, and acts. The difference between using Cursor and using Windsurf is the difference between having a very capable assistant and having a very capable assistant who also pays attention.&lt;/p&gt;
&lt;p id="d6e0"&gt;&lt;strong&gt;What I actually use it for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="a686"&gt;

&lt;strong&gt;Cascade agent&lt;/strong&gt; Multi-file edits, terminal commands, test runs all without you directing every step. You describe the goal, Cascade figures out the path. It feels less like “here’s what I’m going to do, approve?” and more like “here’s what I did, check it.” That distinction matters more than it sounds when you’re deep in a feature and don’t want to break flow.&lt;/li&gt;

&lt;li id="01bd"&gt;

&lt;strong&gt;Codemaps&lt;/strong&gt; Windsurf indexes your repo and builds a visual map of your architecture how files relate, where the entry points are, what connects to what. Genuinely useful when you’re jumping into a codebase you didn’t write, and it gives the AI accurate context on large projects without you having to explain the structure manually.&lt;/li&gt;

&lt;li id="c4d3"&gt;

&lt;strong&gt;Drag-and-drop screenshot → UI generation&lt;/strong&gt; Drop a screenshot of a design into Cascade and it generates the frontend code. For anyone doing UI work this is the kind of feature that makes you feel like you skipped two steps.&lt;/li&gt;

&lt;li id="a3c9"&gt;

&lt;strong&gt;$15/month&lt;/strong&gt; Cursor Pro is $20. Windsurf Pro is $15. Same capability tier, lower price. Not the reason to pick it, but not nothing either.&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="8776"&gt;As of February 2026 Windsurf sits at number one in the LogRocket AI Dev Tool Power Rankings ahead of Cursor and GitHub Copilot. And with the Cognition AI acquisition bringing Devin integration into the roadmap, it’s about to get significantly more powerful.&lt;/p&gt;
&lt;p id="5e4e"&gt;I run Windsurf when I want to stay in flow on a feature without stopping to manage the AI. It gets out of the way in a way that Cursor, for all its power, sometimes doesn’t.&lt;/p&gt;
&lt;p id="d791"&gt;&lt;a href="https://windsurf.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;windsurf.com&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="4569"&gt;How Cursor, Claude Code, and Windsurf work together&lt;/h2&gt;
&lt;p id="118a"&gt;Each tool on its own is solid. Stacked right they cover every layer of the workflow without overlap and without gaps. It’s not about having three tools open at once it’s about each one owning a different job.&lt;/p&gt;
&lt;p id="4756"&gt;&lt;strong&gt;Here’s how they actually fit into my day:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="a1ad"&gt;

&lt;strong&gt;Windsurf for active feature work&lt;/strong&gt; When I’m building something new and want to stay in flow, Windsurf is open. Cascade handles the context, suggests the next move, runs the changes. I steer, it executes. I don’t stop to manage it and it doesn’t ask me to.&lt;/li&gt;

&lt;li id="3bc9"&gt;

&lt;strong&gt;Cursor for multi-file refactors and reviews&lt;/strong&gt; When a change is complex, touches multiple services, or needs careful diff review before anything gets committed Cursor. The agent window and &lt;code&gt;.cursorrules&lt;/code&gt; context make it the right tool for surgical, deliberate work where I want to see exactly what's changing before it changes.&lt;/li&gt;

&lt;li id="8818"&gt;

&lt;strong&gt;Claude Code for everything terminal&lt;/strong&gt; Tests, deploys, migrations, environment setup, CI debugging. Anything that lives in the command line. Claude Code handles the full sequence while I’m doing something else, then tells me what it did.&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="2c42"&gt;&lt;strong&gt;The real-world flow looks like this:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="1459"&gt;Feature branch&lt;br&gt;→ Windsurf/Cascade writes the feature&lt;br&gt;→ Cursor agent reviews multi-file diffs&lt;br&gt;→ Claude Code runs tests + deploys to staging&lt;br&gt;→ Push PR&lt;/span&gt;&lt;/pre&gt;
&lt;p id="acae"&gt;&lt;em&gt;Three tools, zero browser tabs open to paste errors into&lt;/em&gt;&lt;/p&gt;
&lt;p id="7fd1"&gt;The week my team lead asked what I’d changed, this was the answer. Not one tool. Not a new IDE. A stack where every part of the workflow had the right tool behind it. Windsurf for flow, Cursor for precision, Claude Code for the terminal. Nothing falling through the cracks.&lt;/p&gt;
&lt;p id="fc74"&gt;That’s the whole thing. It took four months and forty tools to figure out. Now it just runs.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="6595"&gt;Final thoughts: you don’t need 40 tools&lt;/h2&gt;
&lt;p id="dce2"&gt;I didn’t set out to replace Copilot. I set out to stop feeling like my tools were slowing me down. Those are different problems and they lead to different solutions.&lt;/p&gt;
&lt;p id="045b"&gt;Copilot isn’t the villain here. It’s a solid tool that does exactly what it promises. The issue is that what it promises stopped being enough once the rest of the ecosystem caught up and then kept going. Standing still while everything around you accelerates is its own kind of falling behind.&lt;/p&gt;
&lt;p id="a6c1"&gt;&lt;strong&gt;After four months and forty experiments the three tools that actually changed my output were:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="01c8"&gt;Cursor for multi-file feature work and agentic refactors&lt;/li&gt;

&lt;li id="2123"&gt;Claude Code for terminal tasks, deploys, and anything CLI&lt;/li&gt;

&lt;li id="7389"&gt;Windsurf for staying in flow with Cascade running alongside you&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="80b6"&gt;None of them require you to change how you think about code. They just remove the parts that were slowing you down the context switching, the tab juggling, the manual command sequences, the back-and-forth with a tool that doesn’t know what you’re building or why.&lt;/p&gt;
&lt;p id="de72"&gt;My PRs got cleaner. My review cycles got shorter. My team noticed before I even said anything. That’s the only metric that matters.&lt;/p&gt;
&lt;p id="4464"&gt;If you’re still on vanilla Copilot and shipping just fine genuinely, keep going. But if you’ve been feeling the friction and just assumed that was normal, it isn’t. The gap between what Copilot does and what this stack does is real and it’s only getting wider.&lt;/p&gt;
&lt;p id="3efd"&gt;Start with one. Cursor if you spend most of your time in the editor. Claude Code if you live in the terminal. Windsurf if you keep getting pulled out of flow. Run it for two weeks on real work. You’ll know by then.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="16b4"&gt;&lt;strong&gt;Helpful resources and links&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;

&lt;li id="2fa1"&gt;

&lt;a href="https://cursor.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Cursor&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;multi-file AI editor with agent window and .cursorrules support&lt;/li&gt;

&lt;li id="4929"&gt;

&lt;a href="https://cursor.com/blog/cursor-3" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Cursor 3 announcement&lt;/strong&gt;&lt;/a&gt; the April 2026 agent-first rebuild&lt;/li&gt;

&lt;li id="fd9e"&gt;

&lt;a href="https://docs.anthropic.com/en/docs/claude-code/overview" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/a&gt; terminal-native AI coding agent&lt;/li&gt;

&lt;li id="9a4f"&gt;

&lt;a href="https://windsurf.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Windsurf&lt;/strong&gt;&lt;/a&gt; agentic IDE with Cascade and Codemaps&lt;/li&gt;

&lt;li id="86f9"&gt;

&lt;a href="https://github.com/features/copilot" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;GitHub Copilot&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;still worth knowing what you’re moving away from&lt;/li&gt;

&lt;li id="ffe1"&gt;

&lt;a href="https://blog.logrocket.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;LogRocket AI Dev Tool Power Rankings&lt;/strong&gt;&lt;/a&gt; February 2026 edition&lt;/li&gt;

&lt;li id="a401"&gt;

&lt;a href="https://www.reddit.com/r/cursor/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;r/cursor&lt;/strong&gt;&lt;/a&gt; real-world Cursor workflows and community feedback&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>github</category>
    </item>
    <item>
      <title>Cursor, Claude Code, Windsurf?! My AI coding stack after 40 dev experiments</title>
      <dc:creator>&lt;devtips/&gt;</dc:creator>
      <pubDate>Tue, 19 May 2026 03:22:53 +0000</pubDate>
      <link>https://dev.to/dev_tips/cursor-claude-code-windsurf-my-ai-coding-stack-after-40-dev-experiments-2llk</link>
      <guid>https://dev.to/dev_tips/cursor-claude-code-windsurf-my-ai-coding-stack-after-40-dev-experiments-2llk</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="fb5e"&gt;&lt;strong&gt;For devs drowning in AI tool hype who just want to know what actually stuck&lt;/strong&gt;&lt;/h2&gt;
&lt;span&gt;&lt;/span&gt;&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;h2 id="8e99"&gt;40 tools, 3 survivors&lt;/h2&gt;
&lt;p id="1f27"&gt;If you’ve ever installed a new AI coding tool on a Monday, spent the whole evening configuring it, been genuinely impressed for about three days, and then quietly gone back to your old setup by Thursday you’re not alone. Some devs binge Netflix. I install AI tools.&lt;/p&gt;
&lt;p id="5903"&gt;I went through a phase (fine, a four-month spiral) where I tested nearly every tool promising to make me ship faster, code smarter, or finally stop copy-pasting stack traces into a browser tab. Most of them were fine. A few were actually good. One deleted a file I needed. But out of the 40+ IDEs, agents, plugins, extensions, and CLI tools I ran through real work projects not toy repos, actual PRs three made it through.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="94fa"&gt;Cursor. Claude Code. Windsurf.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="f83a"&gt;This isn’t a “best of 2026” roundup written by someone who ran each tool for 20 minutes. It’s what survived four months of daily use across real backend work, DevOps tasks, and the occasional frontend emergency. Tools I open every morning without thinking about it. The ones that made my workflow cleaner, faster, and way less frustrating.&lt;/p&gt;
&lt;p id="4477"&gt;If you’re still on vanilla Copilot wondering why things still feel slow, or you’re deep in comparison hell and just want someone to cut through it this one’s for you.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="0115"&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Cursor handles multi-file feature work and agentic refactors. Claude Code owns the terminal tests, deploys, migrations, anything CLI. Windsurf runs Cascade in the background while you stay in flow. Together they cover every slot in a serious dev workflow. Separately, each one is still better than most of the 37 tools I’m not writing about.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="0903"&gt;Let’s get into it.&lt;/p&gt;
&lt;h3 id="0308"&gt;&lt;strong&gt;Table of contents&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;

&lt;li id="5d2c"&gt;Why your AI tool stack actually matters now&lt;/li&gt;

&lt;li id="32b5"&gt;

&lt;strong&gt;Cursor &lt;/strong&gt;the IDE that started arguing back&lt;/li&gt;

&lt;li id="7a3d"&gt;

&lt;strong&gt;Claude Code&lt;/strong&gt; the terminal agent I didn’t know I needed&lt;/li&gt;

&lt;li id="ab5a"&gt;

&lt;strong&gt;Windsurf &lt;/strong&gt;the dark horse nobody warned me about&lt;/li&gt;

&lt;li id="36b5"&gt;How Cursor, Claude Code, and Windsurf work together&lt;/li&gt;

&lt;li id="73c3"&gt;

&lt;strong&gt;Final thoughts:&lt;/strong&gt; you don’t need 40 tools&lt;/li&gt;

&lt;li id="0612"&gt;&lt;strong&gt;Helpful resources and links&lt;/strong&gt;&lt;/li&gt;

&lt;/ul&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="e748"&gt;Why your AI tool stack actually matters now&lt;/h2&gt;
&lt;p id="4302"&gt;Here’s the thing nobody tells you when you first install an AI coding tool: the tool isn’t the hard part. The hard part is figuring out which problems you actually have, and whether what you just installed solves any of them.&lt;/p&gt;
&lt;p id="6880"&gt;Most devs treat AI tooling like a plugin they’ll figure out later. They install Copilot, use it for autocomplete, occasionally ask it to explain a regex, and call it done. That worked fine in 2024. In 2026 it’s the equivalent of using a GPS only to check if it’s raining.&lt;/p&gt;
&lt;p id="d28b"&gt;The conversation has moved. Agents are the standard now, not the novelty. The tools worth your time aren’t the ones that finish your line of code they’re the ones that read your whole codebase, plan a multi-step task, execute it, run your tests, fix the failures, and come back when it’s done. That’s not autocomplete. That’s a different category of tool entirely.&lt;/p&gt;
&lt;p id="cf27"&gt;And the cost of getting this wrong isn’t just a wasted subscription. It’s the context-switching penalty. Every time you break flow to copy an error into a chat window, switch tabs to ask a question you should be able to ask inside your editor, or manually run a command sequence an agent could handle that’s compounding friction. The &lt;a href="https://dora.dev/research/2024/dora-report/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;DORA 2025 report&lt;/strong&gt;&lt;/a&gt; found that high-performing engineering teams are pulling significantly ahead of the rest, and tooling decisions are a real part of that gap.&lt;/p&gt;
&lt;p id="02db"&gt;The developers figuring out the right AI stack right now aren’t the ones with the most tools installed. They’re the ones who stopped treating AI like a fancy tab completion and started treating it like a collaborator with a job description. You wouldn’t hire one person to do your backend, frontend, DevOps, and code review. Same logic applies here.&lt;/p&gt;
&lt;p id="fd6c"&gt;That’s the frame for everything that follows not which AI tool is best, but which tool owns which job, and how you stack them so nothing falls through the cracks.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="8aae"&gt;Cursor the IDE that started arguing back&lt;/h2&gt;
&lt;p id="969d"&gt;I thought AI-assisted coding meant better autocomplete. Then Cursor refactored a function I didn’t ask it to touch, the PR passed review without a single comment, and I had to sit with that for a minute.&lt;/p&gt;
&lt;p id="00b9"&gt;Cursor isn’t a smarter Copilot. It’s a different thing entirely. Where Copilot watches what you’re typing and tries to finish the sentence, Cursor reads your whole codebase and forms opinions about it. You can ask it to build a feature and it’ll touch six files, write the tests, and explain what it changed and why.&lt;/p&gt;
&lt;p id="3857"&gt;&lt;strong&gt;What I actually use it for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="f430"&gt;

&lt;strong&gt;Multi-file edits&lt;/strong&gt; This is where it earns its keep. I don’t use Cursor for writing single functions. I use it when a change needs to ripple across the codebase updating an interface, migrating an API, refactoring auth logic. It plans the changes, shows you the diff across every affected file, and lets you approve before anything gets written.&lt;/li&gt;

&lt;li id="9c37"&gt;

&lt;strong&gt;cursorrules&lt;/strong&gt; Drop this file in the root of your project. Cursor reads it at the start of every session preferred patterns, things to avoid, naming conventions and actually respects that context every time.&lt;/li&gt;

&lt;/ul&gt;
&lt;pre&gt;&lt;span id="cf2b"&gt;# .cursorrules&lt;br&gt;You are working on a Node.js REST API.&lt;br&gt;Always use async/await. Never use callbacks.&lt;br&gt;Prefer explicit error handling. Never swallow errors silently.&lt;br&gt;Add JSDoc comments to all exported functions.&lt;/span&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p id="f226"&gt;&lt;em&gt;This alone cuts the back-and-forth in half.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;ul&gt;

&lt;li id="4fc3"&gt;

&lt;strong&gt;Cmd+K inline editing&lt;/strong&gt; Highlight a block, hit Cmd+K, describe what you want. Rewrites in place. No sidebar, no context switching, no copy-paste. Fastest way to refactor something small without losing your train of thought.&lt;/li&gt;

&lt;li id="f837"&gt;

&lt;strong&gt;Agent window Cursor 3&lt;/strong&gt; April 2026, Cursor shipped a new interface built from scratch around agents. Multiple agents running in parallel across different repos, all visible in one sidebar. You dispatch tasks, review diffs, approve changes. It looks less like a code editor and more like an engineering manager’s dashboard. In a good way.&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="2c47"&gt;I still write code manually. Cursor doesn’t replace that. But for anything involving more than one file, more than one decision, or more than five minutes of thinking I hand it off and review the output. That’s a different relationship with your tools than most devs are used to, and it takes about a week to actually trust it.&lt;/p&gt;
&lt;p id="d1d0"&gt;Worth it.&lt;/p&gt;
&lt;p id="d9cd"&gt;&lt;a href="https://cursor.com" rel="noopener ugc nofollow noreferrer"&gt;cursor.com&lt;/a&gt;&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="700" height="385" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A700%2F1%2AZArXTN6layGEiF0u7NeoXA.png"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="34a0"&gt;Claude Code the terminal agent I didn’t know I needed&lt;/h2&gt;
&lt;p id="20da"&gt;Most AI coding tools live inside your editor. Claude Code lives in your terminal. No IDE. No sidebar. No chat window. You talk to it like a senior engineer who already read the repo, and it writes files, runs commands, fixes test failures, and ships. It’s not a chatbot with code awareness. It’s a collaborator.&lt;/p&gt;
&lt;p id="cac0"&gt;The first time it ran a full test suite, found a failing edge case I hadn’t noticed, fixed it, and committed while I was reviewing a completely different PR I had to close my laptop and think about my career choices for a moment.&lt;/p&gt;
&lt;p id="b0ed"&gt;&lt;strong&gt;Why I stuck with it:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="967a"&gt;

&lt;strong&gt;Terminal-native workflow&lt;/strong&gt; Claude Code lives where backend and DevOps work actually happens. No tab switching, no copy-pasting output into a chat window, no losing context. You stay in the terminal, it stays in the terminal, and the whole session feels like pairing with someone who never gets tired or distracted.&lt;/li&gt;

&lt;li id="bde2"&gt;

&lt;strong&gt;Natural language task execution&lt;/strong&gt; You describe what you want in plain English. It reads the repo, makes a plan, executes it, and tells you what it did.&lt;/li&gt;

&lt;/ul&gt;
&lt;pre&gt;&lt;span id="c2af"&gt;$ claude &lt;span&gt;"refactor the auth middleware to use JWT RS256,&lt;br&gt;run the tests, and fix anything that breaks"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p id="1a5c"&gt;&lt;em&gt;It reads the codebase, plans the changes, runs the tests, iterates on failures without you touching anything.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;ul&gt;

&lt;li id="7ddc"&gt;

&lt;strong&gt;MCP tool integrations&lt;/strong&gt; Claude Code connects to external tools via MCP GitHub, databases, deployment pipelines. You can give it real reach beyond just the codebase and it handles multi-step workflows that would normally take you 20 manual commands.&lt;/li&gt;

&lt;li id="3775"&gt;

&lt;strong&gt;Computer use&lt;/strong&gt; On Mac, Claude Code can open apps, click through UI, screenshot the result, and verify it worked. Build a feature, launch it, test it visually from one terminal session. That one still gets me every time.&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="1078"&gt;I still use it with human oversight on anything production-critical. But for local dev, test environments, and deploy pipelines it runs. I review. That’s the whole loop.&lt;/p&gt;
&lt;p id="dd6c"&gt;&lt;a href="https://docs.anthropic.com/en/docs/claude-code/overview" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;docs.anthropic.com/claude-code&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="794" height="505" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A794%2F1%2ADLKd9VHOXWQcIMu6WRI-Pw.png"&gt;&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="507" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1029%2F1%2APPV6ww80gZP1AdHOjjenTQ.png"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="bb51"&gt;Windsurf the dark horse nobody warned me about&lt;/h2&gt;
&lt;p id="ba85"&gt;Everyone I know is on Cursor. Windsurf kept coming up in my Discord as the tool that people switched to and then got annoyingly quiet about like they’d found something they didn’t want to share yet.&lt;/p&gt;
&lt;p id="03ea"&gt;I tried it out of mild spite. They were right and I was annoyed about it.&lt;/p&gt;
&lt;p id="fbb5"&gt;Windsurf isn’t trying to beat Cursor on features. It’s trying to beat it on &lt;em&gt;feel&lt;/em&gt;. The whole thing is built around Cascade an agentic AI that doesn’t wait for you to ask it something. It watches what you’re doing, understands the context, and acts. The difference between using Cursor and using Windsurf is the difference between having a very capable assistant and having a very capable assistant who also pays attention.&lt;/p&gt;
&lt;p id="8771"&gt;&lt;strong&gt;What makes it essential:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="863d"&gt;

&lt;strong&gt;Cascade agent&lt;/strong&gt; Multi-file edits, terminal commands, test runs all without you directing every step. You describe the goal, Cascade figures out the path. It’s similar to Cursor’s agent mode but the flow feels less interrupted. Less “here’s what I’m going to do, approve?” and more “here’s what I did, check it.”&lt;/li&gt;

&lt;li id="d4b6"&gt;

&lt;strong&gt;Codemaps&lt;/strong&gt; Windsurf indexes your repo and builds a visual map of your architecture how files relate, where the entry points are, what connects to what. Useful when you’re jumping into a codebase you didn’t write, and genuinely helpful for giving the AI accurate context on large projects.&lt;/li&gt;

&lt;li id="f5ba"&gt;

&lt;strong&gt;Drag-and-drop screenshot → UI generation&lt;/strong&gt; Drop a screenshot of a design into Cascade and it generates the frontend code. For anyone doing UI work this is the kind of feature that makes you feel like you skipped two steps.&lt;/li&gt;

&lt;li id="a3e8"&gt;

&lt;strong&gt;$15/month&lt;/strong&gt; Cursor Pro is $20. Windsurf Pro is $15. Same tier, same power level, lower price. Not the reason to pick it, but not nothing either.&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="65dd"&gt;As of February 2026, Windsurf sits at number one in the LogRocket AI Dev Tool Power Rankings ahead of Cursor and GitHub Copilot. And with the Cognition AI acquisition bringing Devin integration into the roadmap, it’s about to get significantly more powerful.&lt;/p&gt;
&lt;p id="1c6c"&gt;I run Windsurf when I want to stay in flow on a feature without stopping to manage the AI. It gets out of the way in a way that Cursor, for all its power, sometimes doesn’t.&lt;/p&gt;
&lt;p id="6a4e"&gt;&lt;a href="https://windsurf.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;windsurf.com&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="700" height="384" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A700%2F1%2ALQhsuzIkqjp3UkeytDl8vA.png"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="83f8"&gt;How Cursor, Claude Code, and Windsurf work together&lt;/h2&gt;
&lt;p id="4a25"&gt;Each tool on its own is solid. Stacked right, they cover every layer of the workflow without overlap and without gaps. It’s not about having three tools open at once it’s about each one owning a different job.&lt;/p&gt;
&lt;p id="3a4b"&gt;&lt;strong&gt;Here’s how they fit into my actual day:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="404f"&gt;

&lt;strong&gt;Windsurf for active feature work&lt;/strong&gt; When I’m building something new and want to stay in flow, Windsurf is open. Cascade handles the context, suggests the next move, runs the changes. I steer, it executes.&lt;/li&gt;

&lt;li id="b941"&gt;

&lt;strong&gt;Cursor for multi-file refactors and reviews&lt;/strong&gt; When a change is complex, touches multiple services, or needs careful diff review before anything gets committed Cursor. The agent window and &lt;code&gt;.cursorrules&lt;/code&gt; context make it the right tool for surgical, deliberate work.&lt;/li&gt;

&lt;li id="5e23"&gt;

&lt;strong&gt;Claude Code for everything terminal&lt;/strong&gt; Tests, deploys, migrations, environment setup, CI debugging. Anything that lives in the command line. Claude Code handles the full sequence while I’m doing something else.&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="0df9"&gt;&lt;strong&gt;The real-world flow looks like this:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="77e4"&gt;Feature branch&lt;br&gt;→ Windsurf/Cascade writes the feature&lt;br&gt;→ &lt;span&gt;Cursor&lt;/span&gt; agent reviews multi-file diffs&lt;br&gt;→ Claude &lt;span&gt;Code&lt;/span&gt; runs tests + deploys &lt;span&gt;to&lt;/span&gt; staging&lt;br&gt;→ Push PR&lt;/span&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p id="695a"&gt;&lt;em&gt;Three tools, zero browser tabs open to paste errors into.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="a846"&gt;I stopped thinking of these as “AI tools” and started thinking of them as team members with specializations. Different strengths, different contexts, different jobs. Once you frame it that way the stack stops feeling like overkill and starts feeling obvious.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="e1a8"&gt;Final thoughts: you don’t need 40 tools&lt;/h2&gt;
&lt;p id="c5fa"&gt;I didn’t set out to build some ultimate AI coding stack. I just wanted something that worked without me having to think about it every week.&lt;/p&gt;
&lt;p id="6ba6"&gt;&lt;strong&gt;After testing more than 40 tools, the three that actually made my workflow better were:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="1b23"&gt;Cursor for multi-file feature work and agentic refactors&lt;/li&gt;

&lt;li id="5b85"&gt;Claude Code for terminal tasks, deploys, and anything CLI&lt;/li&gt;

&lt;li id="598c"&gt;Windsurf for staying in flow with Cascade running alongside you&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="15cd"&gt;They’re not trying to do the same thing. They don’t step on each other. And none of them require you to change how you code they just remove the parts that were slowing you down.&lt;/p&gt;
&lt;p id="2523"&gt;If you’re still running vanilla Copilot and calling it your AI stack that’s fine. But you’re leaving a lot on the table. The tooling gap between devs who’ve figured this out and devs who haven’t is real, and it’s only getting wider.&lt;/p&gt;
&lt;p id="eb27"&gt;Start with one. Get comfortable. Add the next. By the time you’ve run all three for a month you won’t remember what the friction felt like.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="945a"&gt;&lt;strong&gt;Helpful resources and links&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;

&lt;li id="6cae"&gt;

&lt;a href="https://cursor.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Cursor&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;multi-file AI editor with agent window and .cursorrules support&lt;/li&gt;

&lt;li id="74b6"&gt;

&lt;a href="https://cursor.com/blog/cursor-3" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Cursor 3 announcement&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;the April 2026 agent-first interface rebuild&lt;/li&gt;

&lt;li id="c53d"&gt;

&lt;a href="https://docs.anthropic.com/en/docs/claude-code/overview" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/a&gt; terminal-native AI coding agent&lt;/li&gt;

&lt;li id="8058"&gt;

&lt;a href="https://windsurf.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Windsurf&lt;/strong&gt;&lt;/a&gt; agentic IDE with Cascade, Codemaps, and Devin integration incoming&lt;/li&gt;

&lt;li id="6e8a"&gt;

&lt;a href="https://dora.dev/research/2024/dora-report/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;DORA 2025 report&lt;/strong&gt;&lt;/a&gt; state of DevOps and AI-assisted development&lt;/li&gt;

&lt;li id="1c37"&gt;

&lt;a href="https://blog.logrocket.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;LogRocket AI Dev Tool Power Rankings&lt;/strong&gt;&lt;/a&gt; February 2026 rankings&lt;/li&gt;

&lt;li id="ee92"&gt;

&lt;a href="https://www.reddit.com/r/cursor/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;r/cursor&lt;/strong&gt;&lt;/a&gt; community discussion and real-world Cursor workflows&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>webdev</category>
      <category>powerplatform</category>
    </item>
    <item>
      <title>You’re the reason your React app is slow</title>
      <dc:creator>&lt;devtips/&gt;</dc:creator>
      <pubDate>Mon, 18 May 2026 05:09:41 +0000</pubDate>
      <link>https://dev.to/dev_tips/youre-the-reason-your-react-app-is-slow-aim</link>
      <guid>https://dev.to/dev_tips/youre-the-reason-your-react-app-is-slow-aim</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="9707"&gt;You didn’t hit a framework limit. You wrote the bottleneck yourself and it’s been quietly billing you in FPS ever since.&lt;/h2&gt;
&lt;span&gt;&lt;/span&gt;&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p id="09b6"&gt;There’s a specific kind of suffering that happens when you open the React DevTools Profiler for the first time on a project that’s been “running fine.” You hit record. You click a button. You stop recording. And then you just sit there, staring at a flame graph that looks like a city on fire, wondering how a todo app is re-rendering 47 components when you clicked “add item.”&lt;/p&gt;
&lt;p id="91f5"&gt;That was me, about three years into thinking I was pretty decent at React.&lt;/p&gt;
&lt;p id="1d41"&gt;I wasn’t bad. My components looked clean. My PRs got approved. The app shipped. But under the hood, I was doing roughly eight things wrong simultaneously, and the only reason nobody noticed was that our user base was small enough that the jank felt like a “network thing.” It was not a network thing.&lt;/p&gt;
&lt;p id="b0d2"&gt;The React ecosystem has an interesting culture around performance: everyone knows it matters, most articles cover the same four hooks, and almost nobody talks about the architectural decisions that create the problem in the first place. The React Compiler landing in React 19 is going to paper over some of this it automatically memoizes components and values, essentially applying &lt;code&gt;useMemo&lt;/code&gt; and &lt;code&gt;useCallback&lt;/code&gt; everywhere it's safe to do so but here's the honest truth: it won't save you from architectural issues like overly broad context providers or massive component trees. You can't compile your way out of a bad design. &lt;a href="https://dev.to/alex_bobes/react-performance-optimization-15-best-practices-for-2025-17l9" rel="noopener ugc nofollow"&gt;&lt;strong&gt;DEV CommunityDEV Community&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p id="44d0"&gt;This article is the one I wish someone had thrown at me back then. No cargo-cult hooks. No “just add &lt;code&gt;React.memo&lt;/code&gt;" advice. Just the actual mistakes, why they happen, and what they cost you in the real world.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="7aed"&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; React is fast by default. You are often the problem. These 10 mistakes are the most common ways engineers including experienced ones quietly murder their own app’s performance, and most of them have nothing to do with the hooks you’ve been memorizing.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="15dc"&gt;The re-render killers&lt;/h2&gt;
&lt;p id="fa34"&gt;Let’s start with the category that accounts for maybe 60% of React performance complaints I’ve seen in the wild. Not slow APIs. Not bad algorithms. Just components re-rendering when they absolutely did not need to, because of decisions made in the five seconds it took to write a JSX prop.&lt;/p&gt;
&lt;p id="0ee0"&gt;React’s re-render model is simple enough that it’s easy to underestimate. React re-renders when state or props change by reference, not by value. That one sentence is responsible for more production slowdowns than any framework bug ever was. It sounds obvious until you realize how many ways you’re accidentally creating new references on every render without thinking about it. &lt;a href="https://dev.to/ar_abid_641aa302d5c68b2ae/react-app-re-renders-too-much-the-hidden-performance-bug-and-the-correct-fix-3a3e" rel="noopener ugc nofollow"&gt;&lt;strong&gt;DEV Community&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="62a5"&gt;Mistake 1: Inline functions in JSX&lt;/h2&gt;
&lt;p id="434c"&gt;This is the one that gets everyone eventually, usually when they’re moving fast and the code looks clean.&lt;/p&gt;
&lt;pre&gt;&lt;span id="67e9"&gt;&lt;span&gt;// you write this and it feels fine&lt;/span&gt;&lt;br&gt;&amp;lt;&lt;span&gt;Button&lt;/span&gt; onClick={&lt;span&gt;() =&amp;gt;&lt;/span&gt; &lt;span&gt;handleDelete&lt;/span&gt;(item.&lt;span&gt;id&lt;/span&gt;)} label=&lt;span&gt;"Delete"&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="6167"&gt;Here’s what’s actually happening: every time the parent component renders, that arrow function is a brand new function object in memory. React does a shallow comparison on props. New reference equals “props changed” equals re-render even if &lt;code&gt;item.id&lt;/code&gt; hasn't moved an inch. JavaScript creates a new object or function reference on every render. React does a shallow comparison when deciding whether to re-render a child, and since the reference is always new, the child always re-renders, even when nothing meaningful has changed. &lt;a href="https://dev.to/khris_breezy/5-react-performance-mistakes-that-are-slowing-your-app-down-4b2n" rel="noopener ugc nofollow"&gt;&lt;strong&gt;DEV Community&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p id="be5a"&gt;The fix is boring and correct: move static handlers outside the component, and for dynamic ones that depend on state or props, reach for &lt;code&gt;useCallback&lt;/code&gt;. But there's a catch which brings us directly to mistake number two.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="47c1"&gt;Mistake 2: Using &lt;code&gt;useCallback&lt;/code&gt; as a good luck charm&lt;/h2&gt;
&lt;p id="5837"&gt;So you read about inline functions, you start wrapping everything in &lt;code&gt;useCallback&lt;/code&gt;, and you feel like you've leveled up. You haven't. You've just moved the problem around and added overhead.&lt;/p&gt;
&lt;p id="8de4"&gt;&lt;code&gt;useCallback&lt;/code&gt; only does anything useful when the component receiving that function is actually memoized wrapped in &lt;code&gt;React.memo&lt;/code&gt;. Without that, you're paying the cost of memoization (React has to store the previous function, compare dependencies, and make a decision) while getting zero benefit, because the child rerenders anyway. &lt;code&gt;useCallback&lt;/code&gt; only helps if the child component is memoized (&lt;code&gt;React.memo&lt;/code&gt;) or uses the callback in its own dependency arrays. Otherwise, you're adding overhead for no benefit. &lt;a href="https://dev.to/pockit_tools/why-your-react-app-re-renders-too-much-a-deep-dive-into-performance-optimization-2oh3" rel="noopener ugc nofollow"&gt;&lt;strong&gt;DEV Community&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p id="0262"&gt;I’ve seen codebases where someone went on a &lt;code&gt;useCallback&lt;/code&gt; spree across the entire app, felt productive for a day, and then wondered why nothing got faster. There is a cost to memoization. React must store the previous props, compare them, and make a decision this adds overhead. If your component is fast to render and frequently changing, this comparison step may become more expensive than the render itself. &lt;a href="https://www.growin.com/blog/react-performance-optimization-2025/" rel="noopener ugc nofollow noreferrer"&gt;Growin&lt;/a&gt;&lt;/p&gt;
&lt;p id="f1a3"&gt;&lt;strong&gt;The actual rule:&lt;/strong&gt; &lt;code&gt;useCallback&lt;/code&gt; is a tool for reference stability, not a performance incantation. Use it when you have a memoized child that receives the function as a prop, or when the function is in a &lt;code&gt;useEffect&lt;/code&gt; dependency array and you want control over when the effect fires. That's basically it. Profile first, reach for it second.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="201f"&gt;Mistake 3: Using array index as &lt;code&gt;key&lt;/code&gt; in lists&lt;/h2&gt;
&lt;p id="3447"&gt;This one feels harmless until you have a list that changes items get added, removed, or reordered and suddenly your UI starts doing weird things. State ends up in the wrong component. Inputs keep the wrong value. Animations fire on the wrong element. You spend an hour blaming a library that did nothing wrong.&lt;/p&gt;
&lt;p id="ae43"&gt;The &lt;code&gt;key&lt;/code&gt; prop is React's identity system for list items. When you use the array index, you're telling React "the first item is always the first item, regardless of what it actually is." Reorder the list, and React thinks all the same items are still there just with different content. It patches the DOM in place instead of remounting, which is fast but wrong.&lt;/p&gt;
&lt;pre&gt;&lt;span id="9670"&gt;&lt;span&gt;// this looks fine and is not fine&lt;/span&gt;&lt;br&gt;{items.&lt;span&gt;map&lt;/span&gt;(&lt;span&gt;(&lt;span&gt;item, i&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &amp;lt;Card key={i} data={item} /&amp;gt;)}&lt;br&gt;&lt;br&gt;&lt;span&gt;// this is fine&lt;/span&gt;&lt;br&gt;{items.&lt;span&gt;map&lt;/span&gt;(&lt;span&gt;&lt;span&gt;item&lt;/span&gt; =&amp;gt;&lt;/span&gt; &amp;lt;Card key={item.id} data={item} /&amp;gt;)}&lt;/span&gt;&lt;/pre&gt;
&lt;p id="33a0"&gt;If your data genuinely has no stable IDs which happens more than it should generate them when the data is created, not at render time. A &lt;code&gt;crypto.randomUUID()&lt;/code&gt; call in your fetch handler costs nothing. A &lt;code&gt;Math.random()&lt;/code&gt; call inside &lt;code&gt;map&lt;/code&gt; gives every item a new key on every render, which tells React to unmount and remount the entire list. That costs a lot.&lt;/p&gt;
&lt;blockquote&gt;

&lt;p id="579b"&gt;All three of these mistakes share the same root: React’s rendering model is predictable once you understand it, but it punishes you quietly. No errors. No warnings. Just a Profiler graph that looks increasingly unwell.&lt;/p&gt;

&lt;p id="708a"&gt;The good news is that the React DevTools Profiler will catch all three almost immediately. Record a session, look for components highlighted in yellow or red, and ask yourself: “did this actually need to re-render?” Usually the answer is no, and usually one of these three is why.&lt;/p&gt;


&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="e2c5"&gt;State architecture sins&lt;/h2&gt;
&lt;p id="1abe"&gt;Re-renders from inline functions are annoying. State architecture mistakes are a different category of problem entirely. They’re the ones that survive a code review, pass all your tests, and then slowly make your app feel like it’s running through wet concrete as the feature count grows. They’re structural. And they’re almost always invisible until you’re already in pain.&lt;/p&gt;
&lt;p id="faf6"&gt;The pattern is consistent across every codebase I’ve seen it in: someone makes a reasonable decision early, the app grows around that decision, and by the time the jank is obvious there are forty components depending on the thing that’s wrong. Refactoring it feels like surgery on a patient who’s still running a marathon.&lt;/p&gt;
&lt;p id="af68"&gt;Understanding why these happen is more useful than just memorizing the fix.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="2c44"&gt;Mistake 4: Putting state too high up the tree&lt;/h2&gt;
&lt;p id="763d"&gt;This is the most common architectural mistake in React, and it’s almost always made with good intentions. You want state to be accessible from multiple places, so you lift it up to a common ancestor. Reasonable. Except that ancestor is now &lt;code&gt;App&lt;/code&gt;, and every time a checkbox toggles in a deeply nested form, your entire component tree re-renders.&lt;/p&gt;
&lt;p id="a9d0"&gt;If your App component’s state changes, every child component re-renders even if their props didn’t change. This cascading effect kills performance at scale. The mental model that helps here: state should live as close as possible to the components that actually use it. Not one level above. Not in a global provider “just in case something else needs it later.” Right next to the thing that needs it. This is called state colocation, and it’s one of those ideas that sounds obvious until you see how rarely it’s actually practiced.&lt;/p&gt;
&lt;p id="f175"&gt;If only two components in a subtree share a piece of state, their nearest common ancestor is the right home for it not the root. If state is only used by one component, it belongs inside that component, full stop. Split your components into two types: container components that handle the logic, and presentational components that are pure display. Because they have no local state, presentational components only re-render when their props actually change.&lt;/p&gt;
&lt;p id="2045"&gt;The performance difference between “state at the root” and “state colocated” can be dramatic in a large component tree. It’s also the kind of change that makes every subsequent optimization easier, because you’re no longer fighting against a re-render cascade every time you try to fix something specific.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="0dc5"&gt;Mistake 5: Context without memoization&lt;/h2&gt;
&lt;p id="c5d5"&gt;React Context is one of those features that feels like a complete solution right up until it isn’t. You set up a provider, everything can access your global state, PRs get merged, life is good. Then someone notices that updating the user’s theme preference is somehow causing the entire dashboard to re-render, including the charts, the sidebar, and the table that has nothing to do with theming.&lt;/p&gt;
&lt;p id="6ea9"&gt;Here’s why. When you pass an object as the context value which almost everyone does that object is recreated on every render of the provider component. Every consumer sees a new reference. Every consumer re-renders. Even the ones that only care about one field in that object that didn’t change.&lt;/p&gt;
&lt;pre&gt;&lt;span id="3a52"&gt;&lt;span&gt;// every render of AppProvider creates a new value object&lt;/span&gt;&lt;br&gt;&lt;span&gt;// every consumer re-renders every time anything changes&lt;/span&gt;&lt;br&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;AppProvider&lt;/span&gt;(&lt;span&gt;{ children }&lt;/span&gt;) {&lt;br&gt;  &lt;span&gt;const&lt;/span&gt; [user, setUser] = &lt;span&gt;useState&lt;/span&gt;(&lt;span&gt;null&lt;/span&gt;);&lt;br&gt;  &lt;span&gt;const&lt;/span&gt; [theme, setTheme] = &lt;span&gt;useState&lt;/span&gt;(&lt;span&gt;'dark'&lt;/span&gt;);&lt;br&gt;  &lt;span&gt;const&lt;/span&gt; value = { user, setUser, theme, setTheme };&lt;br&gt;  &lt;span&gt;return&lt;/span&gt; &amp;lt;AppContext.Provider value={value}&amp;gt;{children}&amp;lt;/AppContext.Provider&amp;gt;;&lt;br&gt;}&lt;/span&gt;&lt;/pre&gt;
&lt;p id="83b8"&gt;Every state update created a new value object. Every context consumer re-rendered. Including components that only cared about the theme, not the user.&lt;/p&gt;
&lt;p id="3828"&gt;Two fixes, and you’ll usually want both. First, wrap the context value in &lt;code&gt;useMemo&lt;/code&gt; so the reference only changes when the actual data changes. Second, split large contexts into smaller ones by concern a &lt;code&gt;UserContext&lt;/code&gt; and a &lt;code&gt;ThemeContext&lt;/code&gt; rather than one &lt;code&gt;AppContext&lt;/code&gt; that holds everything. A component that only reads theme should never re-render because the user object updated.&lt;/p&gt;
&lt;p id="36b9"&gt;This one is particularly brutal in larger apps because context is often set up early, before the component tree is complex enough to make the problem visible. By the time you feel it, the context is load-bearing and everything’s wired to it.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="447" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A840%2F1%2ABtXc4zf0H4KPMoOEFttuhA.jpeg"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="dbd7"&gt;Mistake 6: &lt;code&gt;useEffect&lt;/code&gt; doing too much&lt;/h2&gt;
&lt;p id="195a"&gt;&lt;code&gt;useEffect&lt;/code&gt; is the Swiss Army knife of React hooks, which is both its strength and the reason developers use it to solve problems it was never designed for. The classic version of this mistake: a &lt;code&gt;useEffect&lt;/code&gt; with a dependency array that's either wrong, empty when it shouldn't be, or so long it fires on basically every render anyway.&lt;/p&gt;
&lt;p id="bd59"&gt;The subtler version is using &lt;code&gt;useEffect&lt;/code&gt; for things that are actually derived state or event-driven logic. If you're running an effect to compute a value from existing state, that should probably be &lt;code&gt;useMemo&lt;/code&gt;. If you're running an effect in response to a user action, that logic belongs in the event handler not in a side effect that triggers after the render. Using &lt;code&gt;useEffect&lt;/code&gt; without proper dependencies, or too many, can trigger unnecessary logic or cause infinite loops.&lt;/p&gt;
&lt;p id="e275"&gt;The infinite loop variant is a rite of passage. You set state inside a &lt;code&gt;useEffect&lt;/code&gt;, that state triggers a re-render, the effect fires again, sets state again, render again, and so on until your browser tab starts sweating. It happens to everyone. The less dramatic version an effect that fires twice as often as it should because the dependency array includes an object that gets recreated on every render is more insidious because it's harder to notice and easy to shrug off as "just how React works."&lt;/p&gt;
&lt;p id="4a91"&gt;It’s not just how React works. It’s a dependency array that needs fixing.&lt;/p&gt;
&lt;p id="c556"&gt;A useful heuristic: if you can’t explain in one sentence what your &lt;code&gt;useEffect&lt;/code&gt; is synchronizing with the outside world, there's a decent chance it shouldn't be a &lt;code&gt;useEffect&lt;/code&gt; at all.&lt;/p&gt;
&lt;blockquote&gt;

&lt;p id="75fc"&gt;These three mistakes compound each other in particularly ugly ways. State too high up means more components consuming context. Context without memoization means all those components re-render together. Overloaded effects means those re-renders trigger more side effects. By the time you feel it, the performance problem isn’t one thing it’s a system of bad decisions that arrived gradually and now all need untangling at once.&lt;/p&gt;

&lt;p id="d8ab"&gt;The good news: fixing any one of them improves things measurably. Fixing all three feels like discovering your app had been running with the handbrake on.&lt;/p&gt;


&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="418e"&gt;Bundle crimes&lt;/h2&gt;
&lt;p id="b304"&gt;Everything so far has been about what happens at runtime components re-rendering when they shouldn’t, state living in the wrong place, effects misfiring. These next two mistakes happen before a single line of your React code executes. They happen at load time, and they’re the reason some apps feel slow before the user has even done anything.&lt;/p&gt;
&lt;p id="3b69"&gt;The bundle is the thing you’re shipping. It’s the JavaScript your users have to download, parse, and execute before they can interact with your product. Most developers think about it roughly once during the initial setup and then stop thinking about it entirely as the codebase grows. Features get added, dependencies get installed, and the bundle gets quietly heavier with every sprint until your Lighthouse score starts looking embarrassing.&lt;/p&gt;
&lt;p id="aadd"&gt;According to HTTP Archive data, the median JavaScript payload for desktop users is over 500 KB, with mobile users often downloading significantly more. That’s the median. Plenty of production React apps are shipping multiples of that. On a mid-range phone on a decent connection, that’s a real wait before anything is interactive and most users won’t stick around for it. &lt;a href="https://www.growin.com/blog/react-performance-optimization-2025/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Growin&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="5925"&gt;Mistake 7: Not using &lt;code&gt;React.lazy&lt;/code&gt; and &lt;code&gt;Suspense&lt;/code&gt;&lt;br&gt;
&lt;/h2&gt;
&lt;p id="7055"&gt;The default behavior in a React app without code splitting is simple: everything ships together. Your landing page bundle includes the code for your settings panel, your admin dashboard, your onboarding flow, and every modal you’ve ever built. The user visiting your homepage for the first time downloads all of it, even though they haven’t navigated anywhere yet and statistically might never open half of those routes.&lt;/p&gt;
&lt;p id="a628"&gt;&lt;code&gt;React.lazy&lt;/code&gt; and &lt;code&gt;Suspense&lt;/code&gt; exist specifically for this. They let you split your bundle at the route or component level, loading chunks only when they're actually needed.&lt;/p&gt;
&lt;pre&gt;&lt;span id="ec24"&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;AdminDashboard&lt;/span&gt; = &lt;span&gt;React&lt;/span&gt;.&lt;span&gt;lazy&lt;/span&gt;(&lt;span&gt;() =&amp;gt;&lt;/span&gt; &lt;span&gt;import&lt;/span&gt;(&lt;span&gt;'./AdminDashboard'&lt;/span&gt;));&lt;br&gt;&lt;br&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;App&lt;/span&gt;() {&lt;br&gt;  &lt;span&gt;return&lt;/span&gt; (&lt;br&gt;    &amp;lt;Suspense fallback={&amp;lt;Spinner /&amp;gt;}&amp;gt;&lt;br&gt;      &amp;lt;AdminDashboard /&amp;gt;&lt;br&gt;    &amp;lt;/Suspense&amp;gt;&lt;br&gt;  );&lt;br&gt;}&lt;/span&gt;&lt;/pre&gt;
&lt;p id="d881"&gt;&lt;code&gt;React.lazy()&lt;/code&gt; and &lt;code&gt;Suspense&lt;/code&gt;, when combined with route-level code splitting or dynamic imports, offer a reliable and modern solution to optimize loading behavior. &lt;a href="https://www.growin.com/blog/react-performance-optimization-2025/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Growin&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p id="2744"&gt;The gains here can be significant. A route-level split on a mid-sized app commonly cuts the initial bundle by 30–50%, which translates directly into faster time-to-interactive. The user landing on your homepage gets only what they need to render that page. Everything else loads on demand, in the background, when it becomes relevant.&lt;/p&gt;
&lt;p id="7382"&gt;The reason developers skip this is usually that the app worked fine without it during development. Local development has no network latency and no cold cache, so a 2MB bundle feels instantaneous. Your users on mobile networks in the real world are having a measurably worse time, and the Profiler won’t show you that you have to look at your bundle analyzer and your Core Web Vitals to see it.&lt;/p&gt;
&lt;p id="7ef8"&gt;If you’re on Next.js or Remix, a lot of this is handled for you at the framework level. If you’re on a custom Vite or Webpack setup, route-level lazy loading is one of the highest-leverage changes you can make with the least amount of code.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="6b36"&gt;Mistake 8: Importing entire libraries when you need one function&lt;/h2&gt;
&lt;p id="b1f9"&gt;This one has been a known issue for years and it still shows up constantly. The pattern looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;span id="305e"&gt;&lt;span&gt;import&lt;/span&gt; _ &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'lodash'&lt;/span&gt;;&lt;br&gt;&lt;br&gt;&lt;span&gt;const&lt;/span&gt; result = _.&lt;span&gt;groupBy&lt;/span&gt;(data, &lt;span&gt;'category'&lt;/span&gt;);&lt;/span&gt;&lt;/pre&gt;
&lt;p id="0feb"&gt;You needed &lt;code&gt;groupBy&lt;/code&gt;. You imported all of lodash. Lodash is around 70KB minified and gzipped which is not catastrophic on its own, but it's also not free, and it compounds with every other library you're doing the same thing with.&lt;/p&gt;
&lt;p id="8d54"&gt;Importing an entire library rather than just one or more components can dramatically increase your bundle size. A large bundle can slow download times, thereby negatively affecting the user experience. Use named imports rather than default imports, and use code-splitting so you can load only the code you need. &lt;a href="https://www.tftus.com/blog/common-bugs-in-reactjs-development" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;TFTUS Official Blog&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p id="e8cb"&gt;&lt;strong&gt;The fix is named imports, which tree-shake correctly:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="c9eb"&gt;&lt;span&gt;import&lt;/span&gt; groupBy &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'lodash/groupBy'&lt;/span&gt;;&lt;br&gt;&lt;span&gt;// or&lt;/span&gt;&lt;br&gt;&lt;span&gt;import&lt;/span&gt; { groupBy } &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'lodash-es'&lt;/span&gt;;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="ae53"&gt;The &lt;code&gt;lodash-es&lt;/code&gt; variant is the ESM version of lodash, which plays nicely with modern bundlers and tree-shaking. Only the functions you actually import end up in your bundle.&lt;/p&gt;
&lt;p id="2db9"&gt;Lodash is just the most common example. The same mistake gets made with &lt;code&gt;moment.js&lt;/code&gt; a library that is famously large and should almost always be replaced with &lt;code&gt;date-fns&lt;/code&gt; or the native &lt;code&gt;Intl&lt;/code&gt; API at this point. It gets made with UI component libraries that export everything from a single entry point. It gets made with icon packs where someone imports the entire icon set to use three icons.&lt;/p&gt;
&lt;p id="d463"&gt;The way to catch this at scale is the &lt;a href="https://github.com/webpack-contrib/webpack-bundle-analyzer" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Webpack Bundle Analyzer&lt;/strong&gt;&lt;/a&gt; or the equivalent for Vite. Run it once on your production build and look at what’s actually inside your bundle. The results are often surprising in ways that are equal parts educational and mortifying. You will almost certainly find at least one dependency in there that you forgot you installed, and at least one that you installed for something you ended up not shipping.&lt;/p&gt;
&lt;p id="663b"&gt;This is worth doing before you spend time on any runtime optimization. It doesn’t matter how well-memoized your components are if you’re making users wait three seconds to download the JavaScript before any of that code runs.&lt;/p&gt;
&lt;blockquote&gt;

&lt;p id="84f6"&gt;These two mistakes live in a different category from the previous six because they’re invisible during development and only show up in production metrics. Your app feels fast locally. Your users experience something different. The fix for both requires adding a step to your workflow looking at bundle output, running Lighthouse against a production build, checking Core Web Vitals in Search Console rather than changing how you write components.&lt;/p&gt;

&lt;p id="69e5"&gt;That habit of looking at what you’re actually shipping is one of the things that separates engineers who ship fast apps from engineers who ship apps that feel fast on their own machine.&lt;/p&gt;


&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="045a"&gt;The tools you’ve been ignoring&lt;/h2&gt;
&lt;p id="086b"&gt;The previous eight mistakes were all things you did. These last two are things you didn’t do which somehow makes them worse. There’s a special category of performance problem that exists not because you wrote something wrong, but because you never reached for the tool that would have either prevented the problem or shown you it existed.&lt;/p&gt;
&lt;p id="23df"&gt;These aren’t obscure. They’re not advanced. They ship with React or have been in the ecosystem for years. They just require you to stop and think about scale before scale becomes the problem, and most of us don’t do that until a user complains or a Lighthouse score goes red.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="cecc"&gt;Mistake 9: Rendering a thousand items when you could render twelve&lt;/h2&gt;
&lt;p id="a261"&gt;At some point in almost every data-heavy React app, someone builds a list. The list works great with twenty items in development. It goes to production. The list now has two thousand items. Scrolling feels like dragging furniture across carpet. The component that renders each row is perfectly optimized memoized, no inline functions, stable keys and it doesn’t matter at all, because you’re rendering all two thousand of them simultaneously into the DOM whether the user can see them or not.&lt;/p&gt;
&lt;p id="d614"&gt;This is the problem that virtualization solves, and it’s one of those solutions that feels almost too simple once you understand it. Instead of rendering every item in the list, you render only the ones currently visible in the viewport plus a small buffer above and below for smooth scrolling. As the user scrolls, items that leave the viewport get unmounted, new ones get mounted. The DOM stays small. Performance stays flat regardless of dataset size.&lt;/p&gt;
&lt;pre&gt;&lt;span id="cab9"&gt;&lt;span&gt;import&lt;/span&gt; { &lt;span&gt;FixedSizeList&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; &lt;span&gt;List&lt;/span&gt; } &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'react-window'&lt;/span&gt;;&lt;br&gt;&lt;br&gt;&amp;lt;List&lt;br&gt;  height={600}&lt;br&gt;  itemCount={items.length}&lt;br&gt;  itemSize={72}&lt;br&gt;  width="100%"&lt;br&gt;&amp;gt;&lt;br&gt;  {({ index, style }) =&amp;gt; (&lt;br&gt;    &amp;lt;div style={style}&amp;gt;&lt;br&gt;      &amp;lt;UserCard user={items[index]} /&amp;gt;&lt;br&gt;    &amp;lt;/div&amp;gt;&lt;br&gt;  )}&lt;br&gt;&amp;lt;/List&amp;gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="d032"&gt;[&lt;code&gt;react-window&lt;/code&gt;] only renders visible items, making scrolling smooth. Rendering thousands of DOM nodes at once kills performance. &lt;a href="https://dev.to/frontendtoolstech/react-performance-optimization-best-practices-for-2025-2g6b" rel="noopener ugc nofollow"&gt;&lt;strong&gt;DEV Community&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p id="5ee9"&gt;&lt;code&gt;&lt;a href="https://github.com/bvaughn/react-window" rel="noopener ugc nofollow noreferrer"&gt;react-window&lt;/a&gt;&lt;/code&gt; is the standard library for this, maintained by Brian Vaughn who also built the React DevTools. It's small, fast, and well-documented. &lt;code&gt;&lt;a href="https://virtuoso.dev/" rel="noopener ugc nofollow noreferrer"&gt;react-virtuoso&lt;/a&gt;&lt;/code&gt; is a newer alternative with more built-in features if you need variable item heights or grouped lists. Both work on the same principle.&lt;/p&gt;
&lt;p id="dca5"&gt;The reason developers skip virtualization is usually that they don’t anticipate scale. The list has twenty items, the list works, ship it. Then the list grows. The performance cost of rendering a DOM node for every item in a list scales linearly double the items, roughly double the render time, double the memory usage, double the layout work the browser has to do on every scroll event. By the time you feel it, you often have a list that’s expensive to refactor because everything downstream depends on how it currently works.&lt;/p&gt;
&lt;p id="b91d"&gt;The rule of thumb: if a list could plausibly ever exceed fifty items in production, plan for virtualization from the start. It’s much easier to set up on a new component than to retrofit it onto a complex one that’s been in production for six months.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="e8de"&gt;Mistake 10: Never opening the React DevTools Profiler&lt;/h2&gt;
&lt;p id="5d06"&gt;This is the one that quietly enables every other mistake on this list to survive as long as it does. The Profiler has been part of React DevTools since React 16.5. It shows you exactly which components re-rendered, how long each render took, and why the render was triggered. It is, genuinely, one of the most useful debugging tools in the frontend ecosystem. Most developers have it installed and have never clicked the record button.&lt;/p&gt;
&lt;p id="fb30"&gt;The workflow is straightforward. Open DevTools. Go to the Profiler tab. Hit record. Interact with the part of your app that feels slow. Stop recording. Look at the flame graph.&lt;/p&gt;
&lt;p id="3a33"&gt;What you’re looking for: components that re-render more often than they should, and components whose render time is disproportionately long. The Profiler color-codes this for you gray means fast, yellow means moderate, orange and red mean you should probably look at this. You can click any bar in the graph to see exactly why that component rendered, including which prop or state value changed to trigger it.&lt;/p&gt;
&lt;p id="e01f"&gt;React DevTools Profiler will show you whether a component re-render is expensive enough to justify memoization. In React development, especially as applications grow more interactive and component-driven, it’s easy to introduce performance issues without realizing it. &lt;a href="https://www.growin.com/blog/react-performance-optimization-2025/" rel="noopener ugc nofollow noreferrer"&gt;Growin&lt;/a&gt;&lt;/p&gt;
&lt;p id="f1cb"&gt;This matters because performance optimization without measurement is just guessing. You add &lt;code&gt;useCallback&lt;/code&gt; somewhere because it seems like the right area. You split a component because it feels too big. You might be right. You might be optimizing something that was already fast while the actual bottleneck sits three components over, untouched. No cargo-cult programming just understanding the system and applying targeted fixes. &lt;a href="https://dev.to/pockit_tools/why-your-react-app-re-renders-too-much-a-deep-dive-into-performance-optimization-2oh3" rel="noopener ugc nofollow"&gt;&lt;strong&gt;DEV Community&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p id="0e20"&gt;The Profiler removes the guessing. It tells you where the fire actually is, not where you think it might be. Every fix on this list inline functions, context memoization, colocated state, code splitting lands differently depending on your specific app. The Profiler is how you know which ones to prioritize and whether your changes actually did anything.&lt;/p&gt;
&lt;p id="27dd"&gt;A practical habit: run the Profiler on any feature before you ship it, not after someone complains. It takes three minutes and it has saved me from shipping performance regressions more than once. It’s also genuinely interesting seeing exactly how React thinks about your component tree teaches you things about the framework that no article can.&lt;/p&gt;
&lt;blockquote&gt;

&lt;p id="4a95"&gt;These two mistakes share the same underlying cause: not thinking about scale and measurement until after the problem exists. Virtualization is what you add when you think ahead about large datasets. The Profiler is what you use when you want to stop guessing and start knowing.&lt;/p&gt;

&lt;p id="b0a7"&gt;Together, they close the loop on the entire list. The first eight mistakes give you specific patterns to avoid. These two give you the habit of catching what slips through anyway.&lt;/p&gt;


&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="4284"&gt;You’re not off the hook just because React 19 exists&lt;/h2&gt;
&lt;p id="f42c"&gt;Here’s the take that’s going to age either very well or very poorly: most of what’s on this list is going to become less relevant over the next two years, and that should make you more careful, not less.&lt;/p&gt;
&lt;p id="17b7"&gt;React 19’s biggest change is the React Compiler, which automatically optimizes components without manual &lt;code&gt;useMemo&lt;/code&gt; or &lt;code&gt;useCallback&lt;/code&gt; wrappers. It analyzes your code at build time and applies memoization where it's safe. The inline function problem, the &lt;code&gt;useCallback&lt;/code&gt; cargo-culting, a chunk of the re-render chaos the compiler handles a meaningful portion of that automatically. That's genuinely good news and the React team deserves credit for it. &lt;a href="https://dev.to/alex_bobes/react-performance-optimization-15-best-practices-for-2025-17l9" rel="noopener ugc nofollow"&gt;DEV Community&lt;/a&gt;&lt;/p&gt;
&lt;p id="c6b4"&gt;But I keep coming back to something that doesn’t change regardless of what the compiler does. Architecture isn’t a compiler problem. State living in the wrong place, context providers that re-render everything downstream, &lt;code&gt;useEffect&lt;/code&gt; being used as a catch-all for logic that should live elsewhere none of that gets fixed at build time. The compiler won't save you from architectural issues like overly broad context providers or massive component trees. You still have to understand the system well enough to design it correctly. &lt;a href="https://dev.to/alex_bobes/react-performance-optimization-15-best-practices-for-2025-17l9" rel="noopener ugc nofollow"&gt;&lt;strong&gt;DEV Community&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p id="daef"&gt;And here’s the uncomfortable part: if you learned React by reaching for &lt;code&gt;useMemo&lt;/code&gt; and &lt;code&gt;useCallback&lt;/code&gt; without understanding why, the compiler bailing you out doesn't mean you understood what it fixed. It means you got lucky. The next framework, the next abstraction, the next performance problem that falls outside what the compiler covers that's where the gap shows up.&lt;/p&gt;
&lt;p id="204a"&gt;Performance isn’t a checklist you run through once before a release. It’s a design skill. It lives in the decisions you make about where state goes, how components are composed, what you ship in the initial bundle, and whether you ever actually look at what your app is doing under load. The engineers I’ve seen consistently ship fast products aren’t the ones who know the most hooks. They’re the ones who profile before they optimize, think about scale before it’s a crisis, and treat the bundle as something they’re responsible for not something that happens automatically.&lt;/p&gt;
&lt;p id="91eb"&gt;The React compiler is a great tool. So is the Profiler. So is &lt;code&gt;react-window&lt;/code&gt;. None of them replace the judgment call about whether your component tree makes sense.&lt;/p&gt;
&lt;p id="c352"&gt;&lt;strong&gt;If this list had a single takeaway it’d be this:&lt;/strong&gt; open the Profiler on your current project today, before you read another article, before you install another dependency. Record thirty seconds of normal usage. See what’s actually happening. You might be surprised. You might be horrified. Either way, you’ll know something real and that’s where every fix on this list actually starts.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="87b2"&gt;Drop your worst React performance horror story in the comments. Bonus points if the root cause was on this list and you didn’t know it until a user complained.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="be4a"&gt;Helpful resources&lt;/h2&gt;
&lt;ul&gt;

&lt;li id="dd9f"&gt;

&lt;a href="https://react.dev/reference/react/Profiler" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;React DevTools Profiler docs&lt;/strong&gt;&lt;/a&gt; official guide to actually using the tool&lt;/li&gt;

&lt;li id="5efd"&gt;

&lt;a href="https://react.dev/reference/react/lazy" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;React.lazy and Suspense&lt;/strong&gt;&lt;/a&gt; React docs on code splitting&lt;/li&gt;

&lt;li id="f212"&gt;

&lt;strong&gt;R&lt;/strong&gt;&lt;a href="https://github.com/bvaughn/react-window" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;eact-window on GitHub&lt;/strong&gt;&lt;/a&gt; list virtualization by Brian Vaughn&lt;/li&gt;

&lt;li id="1eaa"&gt;

&lt;a href="https://github.com/webpack-contrib/webpack-bundle-analyzer" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Webpack Bundle Analyzer&lt;/strong&gt;&lt;/a&gt; see what you’re actually shipping&lt;/li&gt;

&lt;li id="02c6"&gt;

&lt;a href="https://react.dev/learn/react-compiler" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;React Compiler docs&lt;/strong&gt;&lt;/a&gt; what it does and doesn’t do&lt;/li&gt;

&lt;li id="f34f"&gt;

&lt;a href="https://web.dev/explore/learn-core-web-vitals" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;web.dev Core Web Vitals&lt;/strong&gt;&lt;/a&gt; the metrics that actually matter to users&lt;/li&gt;

&lt;li id="eb08"&gt;

&lt;a href="https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Kent C. Dodds on state colocation&lt;/strong&gt;&lt;/a&gt; the best deep-dive on mistake #4&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>react</category>
      <category>devops</category>
    </item>
    <item>
      <title>The only AI agents article you’ll ever need</title>
      <dc:creator>&lt;devtips/&gt;</dc:creator>
      <pubDate>Fri, 15 May 2026 00:47:34 +0000</pubDate>
      <link>https://dev.to/dev_tips/the-only-ai-agents-article-youll-ever-need-51f8</link>
      <guid>https://dev.to/dev_tips/the-only-ai-agents-article-youll-ever-need-51f8</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="332c"&gt;&lt;strong&gt;From ReAct loops to production multi-agent systems everything in one place, nothing left out.&lt;/strong&gt;&lt;/h2&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p id="5f35"&gt;Somewhere between the fifteenth “AI agent” LinkedIn post and the third vendor announcing their autonomous workflow platform, I stopped nodding along and started asking the obvious question nobody seemed to want to answer:&lt;/p&gt;
&lt;p id="2848"&gt;&lt;em&gt;What is actually running here?&lt;/em&gt;&lt;/p&gt;
&lt;p id="4bbe"&gt;Because the word “agent” has been doing a lot of heavy lifting lately. It’s been stretched over chatbots with a search button, over multi-step pipelines glued together with vibes, and over genuinely sophisticated systems that can decompose a task, call external APIs, reflect on their own output, and course-correct all without you touching a keyboard. Those are not the same thing. Not even close.&lt;/p&gt;
&lt;p id="e8ac"&gt;I’ve spent the last few months going deep on this. Built agents that failed in embarrassing ways. Read the research, the docs, the Reddit threads where someone’s agent looped itself into a $600 API bill at midnight. Took the courses. Talked to people shipping this stuff in production at scale. And I distilled all of it down into this article.&lt;/p&gt;
&lt;p id="b2d2"&gt;This is not a hype piece. There are no screenshots of a ChatGPT conversation doing something mildly impressive. This is the full picture from the first-principles question of what an agent actually is, through the design patterns that separate demos from real systems, all the way to the production concerns that nobody tweets about: evaluation, latency, cost, observability, and security.&lt;/p&gt;
&lt;p id="530f"&gt;Whether you’re just trying to understand what your team keeps talking about in standups, or you’re actively building agent systems and hitting walls, this is the article you keep open in a tab and come back to.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="7a52"&gt;&lt;strong&gt;&lt;em&gt;TL;DR:&lt;/em&gt;&lt;/strong&gt; An AI agent is an LLM inside a loop, equipped with tools, memory, and decision-making logic. Building one that demos well takes an afternoon. Building one you’d actually trust with real work takes a different mindset closer to distributed systems design than prompt engineering. This article covers both ends of that spectrum and everything in between.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="4b47"&gt;What an agent actually is&lt;/h2&gt;
&lt;p id="0f51"&gt;If you’ve used ChatGPT to write an email, you’ve used an LLM. You gave it a prompt, it gave you an output, done. One shot. Linear. The model doesn’t remember what it did, doesn’t check its own work, doesn’t go looking for missing information. It just generates the next most likely tokens until it hits the end and stops.&lt;/p&gt;
&lt;p id="aecf"&gt;An agent is what happens when you take that same model and put it inside a loop.&lt;/p&gt;
&lt;p id="2e37"&gt;Instead of prompt-in, response-out, you get something closer to how a human actually tackles a non-trivial task. You plan a little. You gather some information. You do a first pass. You read it back, notice what’s wrong, and fix it. You check one more thing. You finish. That back-and-forth, that iterative reasoning over multiple steps that’s the core of what makes something an agent rather than a fancy autocomplete.&lt;/p&gt;
&lt;p id="2c9b"&gt;The technical name for this loop is the &lt;strong&gt;ReAct pattern&lt;/strong&gt;: Reason, Act, Observe, repeat. The model reasons about what to do next. It acts usually by calling a tool, running a search, querying a database, executing some code. It observes the result. Then it either gives you a final answer or loops back to reason again based on what it just learned. That cycle is the engine underneath almost every agent system you’ll encounter, from the simplest LangChain pipeline to Claude Code rewriting your entire test suite.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A840%2F1%2Ai4R5izLDEXWzeABGuCpg4g.jpeg"&gt;&lt;p id="9e56"&gt;Here’s what makes this more powerful than it sounds. Each pass through the loop adds depth. The model isn’t trying to solve everything in one shot under the pressure of a single context window it’s allowed to work iteratively, to gather information it didn’t have at step one, to catch mistakes it made in step two. The output at the end of three loops is almost always better than the output of one. Not because the model got smarter, but because the architecture gave it room to think.&lt;/p&gt;
&lt;p id="a6ba"&gt;The practical upside of this shows up immediately in tasks that need accuracy and sourcing. Legal research where you have to cite specific cases. Customer support that requires pulling account details before responding. Code generation that needs to run the code, read the error, and try again. Any domain where a single-pass answer is almost certainly incomplete or wrong that’s where agents earn their keep.&lt;/p&gt;
&lt;p id="d242"&gt;Here’s the mental model I use: a regular LLM call is a consultant who reads your brief once and writes a report on the plane. An agent is that same consultant who actually does the research, drafts something, reads it back, realizes they missed a key detail, goes and finds it, then rewrites the section. Same intelligence, different process. The process is what changes the output quality.&lt;/p&gt;
&lt;p id="babd"&gt;One thing worth clearing up early: the model itself isn’t what makes something an agent. You can build a mediocre agent on GPT-4 and a great one on a smaller, faster model with a well-designed loop and the right tools. The architecture and the task decomposition matter more than the leaderboard position of the underlying LLM. Remember that when someone tries to sell you on “the most agentic model” the model is one part of the system, not the whole thing.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="8764"&gt;The core building blocks&lt;/h2&gt;
&lt;p id="2c79"&gt;Before you write a single line of agent code, you need to understand four things. Get these right and everything else becomes easier to reason about. Get them wrong and you’ll spend weeks debugging behavior that feels random but isn’t.&lt;/p&gt;
&lt;p id="344b"&gt;&lt;strong&gt;Context is the agent’s entire world.&lt;/strong&gt; Whatever isn’t in the context window doesn’t exist as far as the model is concerned. Context engineering deciding what goes in there is one of the most underrated skills in agent development. It includes the task description, the agent’s role, any memory from previous steps, available tools, and relevant background knowledge. A poorly engineered context produces an agent that hallucinates, repeats itself, or completely ignores the instructions you thought were obvious. Most agent bugs aren’t model bugs. They’re context bugs.&lt;/p&gt;
&lt;p id="58bc"&gt;&lt;strong&gt;Memory comes in two flavors.&lt;/strong&gt; Short-term memory is what the agent writes down as it works intermediate results, tool outputs, notes to itself. Long-term memory is lessons from previous runs, stored and loaded at the start of each new task. The combination is what lets an agent improve over time rather than starting from zero on every execution. Knowledge is different from both it’s static reference material you load upfront. Documentation, PDFs, database access. The agent reads from it but doesn’t update it.&lt;/p&gt;
&lt;p id="fce3"&gt;&lt;strong&gt;Task decomposition is the part nobody talks about enough.&lt;/strong&gt; The rule is simple: break each step down until a single LLM call or a single tool can handle it cleanly. If a step is too big, the output gets sloppy. The exercise is to think about how you’d do the task yourself what are the actual discrete steps then figure out which of those steps map to an LLM call, which map to a tool call, and which map to a bit of regular code. When something isn’t working, nine times out of ten a step is too coarse.&lt;/p&gt;
&lt;p id="e30f"&gt;&lt;strong&gt;Guardrails are the bouncer at the door.&lt;/strong&gt; Because LLMs are non-deterministic, you can’t assume the output will always be in the right format, the right length, or factually consistent with the sources the agent just retrieved. Guardrails are the layer that catches these failures before they reach the user or before they get passed to the next step in the pipeline and silently corrupt everything downstream. Some guardrails are just code: check the output format, validate the schema, enforce length limits. Others use a second LLM to judge quality. And sometimes the right guardrail is a human checkpoint especially for anything irreversible.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A840%2F1%2AqO9GCQwhvRGryKoeCusIsQ.jpeg"&gt;&lt;p id="968f"&gt;Four concepts. Everything else in agent design is built on top of them.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="6fbf"&gt;Four design patterns that actually work&lt;/h2&gt;
&lt;p id="f8cd"&gt;Once your building blocks are solid, the next question is how you structure the actual behavior of the agent. There are four patterns that show up in almost every serious agent system. You don’t always need all four, but you need to know all four.&lt;/p&gt;
&lt;h3 id="2a93"&gt;&lt;strong&gt;Reflection&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="f441"&gt;The simplest and most effective upgrade you can make to any agent. Instead of shipping the first output, the agent critiques it and rewrites it.&lt;/p&gt;
&lt;p id="ef85"&gt;The model produces something, reads it back with a prompt like “what’s wrong with this and how would you fix it,” then revises. That second pass almost always improves the result not because the model is smarter on round two, but because reviewing is an easier cognitive task than generating from scratch. You’re offloading the hard part across two steps instead of cramming it into one.&lt;/p&gt;
&lt;p id="e4f5"&gt;Reflection is especially powerful when you can add external feedback to the loop. Write code, run it, feed the error back, try again. Generate JSON, validate it against a schema, send the validation errors back if it fails. That concrete feedback signal is what separates reflection from just asking the model to “try harder.”&lt;/p&gt;
&lt;p id="1fa5"&gt;The tradeoff is latency and cost you’re doing multiple passes. Test with and without it before you commit.&lt;/p&gt;
&lt;h3 id="4953"&gt;&lt;strong&gt;Tool use&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="ee79"&gt;An LLM by itself is a text generator. It doesn’t know what time it is, can’t query your database, can’t run code, and has no idea what’s in your company’s internal docs. Tools fix that.&lt;/p&gt;
&lt;p id="9ebc"&gt;You define a set of functions web search, database query, code execution, calendar access, whatever your use case needs and the model decides when and which ones to call. Under the hood, the model doesn’t actually execute anything. It outputs a structured request, your code runs the function, and the result gets fed back into the context. The model uses that result to continue.&lt;/p&gt;
&lt;p id="349b"&gt;Well-designed tools have clear names, plain-English descriptions of when to use them, typed input schemas, and clean error handling. Think of them as an API your agent uses. Document them like one.&lt;/p&gt;
&lt;h3 id="b7aa"&gt;&lt;strong&gt;Planning&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="2934"&gt;Instead of following a hardcoded sequence of steps, the agent decides what to do and in what order.&lt;/p&gt;
&lt;p id="96d9"&gt;You give it a toolkit, prompt it to create a step-by-step plan, and execute that plan running each tool, feeding results back, and repeating until the task is done. The model acts as its own project manager. This is powerful for tasks where you can’t anticipate every possible path upfront, like a customer service agent handling wildly different request types.&lt;/p&gt;
&lt;p id="208a"&gt;The catch: more autonomy means more unpredictability. Planning agents need tight guardrails, permission checks, and good logging. The strongest current use case is agentic coding systems where the task space is well-defined even if the exact steps aren’t.&lt;/p&gt;
&lt;h3 id="e9cf"&gt;&lt;strong&gt;Multi-agent collaboration&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="71f5"&gt;Some tasks are too complex, too long, or too varied for one agent to handle well. The answer is the same one humans figured out a long time ago: build a team.&lt;/p&gt;
&lt;p id="ff95"&gt;Each agent gets a specific role and only the tools that role needs. A researcher agent does web search and retrieval. A writer agent handles drafting. A reviewer agent checks quality. A manager agent coordinates the others. Specialization produces better output than one generalist trying to do everything inside a single sprawling context window.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A840%2F1%2AmxOW-Al381je8GLuLiPq3g.jpeg"&gt;&lt;p id="be0a"&gt;The coordination patterns range from simple sequential handoffs researcher finishes, passes to writer, writer passes to reviewer to parallel execution where independent agents run simultaneously and merge results. Most production systems start sequential and add parallelism only where latency actually matters.&lt;/p&gt;
&lt;p id="3da0"&gt;Multi-agent systems are not the default answer. They add real complexity: agents can conflict, communication overhead adds up, and debugging a failure that happened three agents deep is genuinely painful. Start with one agent. Add a second only when the first one has a clear ceiling it can’t break through.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="0dd3"&gt;Shipping to production&lt;/h2&gt;
&lt;p id="f7df"&gt;This is the section that doesn’t make it into the demo videos. Everything up to this point gets you a working agent. This is what gets you a trustworthy one.&lt;/p&gt;
&lt;h3 id="b63a"&gt;&lt;strong&gt;Evaluate before you optimize&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="90f8"&gt;The most common mistake people make with agents is trying to improve something they haven’t measured. Before you touch a prompt, swap a model, or restructure a pipeline, you need to know what’s actually failing and how often.&lt;/p&gt;
&lt;p id="6080"&gt;Some evals are simple.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="1f46"&gt;&lt;strong&gt;&lt;em&gt;Does the customer service agent correctly identify whether an item is in stock?&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="295d"&gt;That’s a pass/fail check you can automate. Others are harder is this research report actually good? For those, use a second LLM as a judge. Give it a consistent rubric, have it score outputs on a 1–5 scale, and track that score across runs.&lt;/p&gt;
&lt;p id="20cb"&gt;Evaluate at two levels. Component-level tells you which specific step is underperforming. End-to-end tells you whether the final output is actually good. If end-to-end scores are low but every component scores fine, the problem is in the handoffs between steps that’s a different fix than a bad prompt.&lt;/p&gt;
&lt;p id="5edd"&gt;Start evaluating on day one. An imperfect eval that exists beats a perfect eval you’re still designing.&lt;/p&gt;
&lt;h3 id="11f2"&gt;&lt;strong&gt;Latency and cost are the same problem&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="1291"&gt;Every extra LLM call costs time and money. In agent systems, those calls stack up fast.&lt;/p&gt;
&lt;p id="aa64"&gt;The fix is the same for both: measure each step, then attack the biggest buckets. Parallelize anything that doesn’t depend on the step before it multiple web searches, multiple document fetches, multiple sub-tasks that can run simultaneously. Right-size your models use a smaller, faster model for simple steps like keyword extraction or format validation, and reserve the expensive one for actual reasoning. Cache aggressively search results, embeddings, intermediate summaries. If the input is identical, don’t recompute.&lt;/p&gt;
&lt;p id="9b1b"&gt;One research agent run might cost a few cents. At a thousand runs a day that’s hundreds of dollars a month. Know your per-run cost before you scale.&lt;/p&gt;
&lt;h3 id="c00b"&gt;&lt;strong&gt;Log everything, assume nothing&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="905d"&gt;Traditional software fails with stack traces. Agent systems fail silently the output looks plausible, the logs show no errors, and something is still wrong.&lt;/p&gt;
&lt;p id="1ff4"&gt;Observability for agents means tracing every decision: what did the agent plan to do, what tool did it call, what came back, what did it decide next. Tools like &lt;a href="https://www.langchain.com/langsmith" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;LangSmith&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://wandb.ai" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Weights &amp;amp; Biases&lt;/strong&gt;&lt;/a&gt; are built for exactly this. When something breaks and it will you want to be able to replay the exact sequence of steps that produced the bad output and see precisely where it went sideways.&lt;/p&gt;
&lt;p id="3dd6"&gt;Beyond individual traces, track aggregate metrics over time. Hallucination rate. Task success rate. Average cost per run. These trend lines tell you whether your changes are actually helping or just moving the problem around.&lt;/p&gt;
&lt;p id="d30d"&gt;&lt;strong&gt;Sandbox your code execution&lt;/strong&gt;&lt;/p&gt;
&lt;p id="54fe"&gt;If your agent can write and run code and the useful ones usually can you need to treat that capability like a loaded gun. Run all code in an isolated container that gets destroyed after each execution. Set hard timeouts and memory limits. Whitelist the libraries it’s allowed to use. Never let agent-generated code write to anywhere that matters or reach the network unless you explicitly decided it should.&lt;/p&gt;
&lt;p id="a649"&gt;The failure mode here isn’t theoretical. An agent with unrestricted code execution and a bad prompt is a very expensive, very fast way to ruin your afternoon.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A840%2F1%2AXd0Ii0m-AlKhBz_wbSBFSA.jpeg"&gt;&lt;p id="e7fd"&gt;Production isn’t a different version of your demo. It’s a different discipline entirely.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="96e9"&gt;The job changed. Most people haven’t caught up yet.&lt;/h2&gt;
&lt;p id="5c06"&gt;Here’s the take I’ll leave you with, and you can disagree with me in the comments: the bottleneck in AI development right now isn’t the models. The models are good. The bottleneck is engineers who understand how to build reliable systems around them.&lt;/p&gt;
&lt;p id="eb60"&gt;Prompting was never the skill. It was always the entry point. The actual work designing context, decomposing tasks, wiring tools, evaluating outputs, controlling costs, tracing failures that’s systems design. It always was. The wrapper just changed.&lt;/p&gt;
&lt;p id="ecdc"&gt;The developers who are going to do interesting things with agents in the next few years aren’t the ones who found the best jailbreak or the cleverest chain-of-thought trick. They’re the ones who treat agents the way they treat any other distributed system: with logging, with testing, with failure modes they planned for, with an understanding of what happens when one component does something unexpected.&lt;/p&gt;
&lt;p id="3f92"&gt;That’s not a pessimistic take. If anything it’s the opposite. It means the skills you already have debugging, systems thinking, knowing when to add complexity and when not to transfer directly. You’re not starting from zero. You’re applying what you know to a new kind of component.&lt;/p&gt;
&lt;p id="01d7"&gt;Agents are going to keep getting more capable, more autonomous, and more embedded in real workflows. The tooling is improving fast. The patterns are stabilizing. This is a good time to actually understand the stack rather than just use the abstraction on top of it.&lt;/p&gt;
&lt;p id="2ecd"&gt;Build something small. Evaluate it honestly. Add one pattern at a time. Log everything from day one. That’s the whole playbook.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="a7d7"&gt;Helpful resources&lt;/h2&gt;
&lt;ul&gt;

&lt;li id="52e3"&gt;

&lt;a href="https://www.anthropic.com/research/building-effective-agents" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Anthropic’s guide to building effective agents&lt;/strong&gt;&lt;/a&gt; the clearest first-principles breakdown of agent design patterns available&lt;/li&gt;

&lt;li id="0223"&gt;

&lt;a href="https://docs.langchain.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;LangChain documentation&lt;/strong&gt;&lt;/a&gt; practical starting point for building agent pipelines in Python&lt;/li&gt;

&lt;li id="1663"&gt;

&lt;a href="https://www.langchain.com/langsmith" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;LangSmith&lt;/strong&gt;&lt;/a&gt; tracing and evaluation tooling built specifically for LLM applications&lt;/li&gt;

&lt;li id="fe64"&gt;

&lt;a href="https://gerred.github.io/building-an-agentic-system/core-architecture.html" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Building an agentic system&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;deep technical breakdown of how tools like Claude Code are architected under the hood&lt;/li&gt;

&lt;li id="4bb6"&gt;

&lt;a href="https://wandb.ai" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Weights &amp;amp; Biases&lt;/strong&gt;&lt;/a&gt; production monitoring and experiment tracking for ML systems&lt;/li&gt;

&lt;li id="5e8f"&gt;

&lt;a href="https://cookbook.openai.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;OpenAI Cookbook agent examples&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;real code examples for tool use, multi-agent patterns, and evals&lt;/li&gt;

&lt;li id="b0f6"&gt;

&lt;a href="https://www.reddit.com/r/LocalLLaMA/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;r/LocalLLaMA&lt;/strong&gt;&lt;/a&gt; where practitioners actually talk about what’s working and what isn’t&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Go vs Rust: the only backend language debate that actually matters in 2026</title>
      <dc:creator>&lt;devtips/&gt;</dc:creator>
      <pubDate>Thu, 14 May 2026 08:02:23 +0000</pubDate>
      <link>https://dev.to/dev_tips/go-vs-rust-the-only-backend-language-debate-that-actually-matters-in-2026-4foi</link>
      <guid>https://dev.to/dev_tips/go-vs-rust-the-only-backend-language-debate-that-actually-matters-in-2026-4foi</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="a41c"&gt;&lt;strong&gt;You don’t need to pick one. You need to know which fight each one was built for.&lt;/strong&gt;&lt;/h2&gt;
&lt;span&gt;&lt;/span&gt;&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p id="2baa"&gt;There’s a certain kind of developer who treats language choice like a religion.&lt;/p&gt;
&lt;p id="4298"&gt;Go devs will tell you Rust is overkill.&lt;/p&gt;
&lt;p id="ee09"&gt;Rust devs will tell you Go is for people who don’t understand memory.&lt;/p&gt;
&lt;p id="b992"&gt;Both are partially right. Both are completely missing the point.&lt;/p&gt;
&lt;p id="0535"&gt;Here’s the thing: choosing between Go and Rust in 2026 isn’t really a language debate anymore. It’s a system design decision. And most of the hot takes you’ll find online are arguing about the wrong thing comparing raw benchmarks and borrow checker frustration instead of asking the actual question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="8ba8"&gt;&lt;strong&gt;Where in your architecture does this choice matter, and when does it stop mattering?&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="171a"&gt;I’ve watched teams rewrite entire Go services in Rust because a benchmarks blog post made someone nervous. I’ve also watched Rust codebases grind sprint velocity to a halt because the team picked the wrong tool for a job that really just needed a couple of goroutines and a coffee break.&lt;/p&gt;
&lt;p id="c63b"&gt;Neither is winning. Both are expensive.&lt;/p&gt;
&lt;p id="b4b4"&gt;The honest answer in 2026 is boring:&lt;/p&gt;
&lt;p id="7691"&gt;Go and Rust aren’t competing. They’re slotting into different layers of the same system. One builds the thing. The other makes the thing survive.&lt;/p&gt;
&lt;p id="715e"&gt;Understanding &lt;em&gt;which layer needs which tool&lt;/em&gt; is the only skill that actually pays out.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="c7b7"&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Go is your default. Fast enough, ships fast, scales horizontally, and your team won’t hate you for picking it. Rust enters the picture when specific parts of your system hit a wall that Go can’t resolve without throwing more money at AWS. The debate isn’t Go &lt;em&gt;or&lt;/em&gt; Rust it’s Go &lt;em&gt;first&lt;/em&gt;, Rust &lt;em&gt;where it earns its keep&lt;/em&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="2901"&gt;Go: the default loadout every backend team starts with&lt;/h2&gt;
&lt;p id="9e49"&gt;There’s a reason Go became the lingua franca of cloud-native backend development.&lt;/p&gt;
&lt;p id="3188"&gt;It’s not because Google has a good marketing department.&lt;/p&gt;
&lt;p id="1b0c"&gt;&lt;strong&gt;It’s because Go made a specific, opinionated bet:&lt;/strong&gt;&lt;/p&gt;
&lt;p id="2378"&gt;&lt;em&gt;Developer velocity and system predictability matter more than raw performance.&lt;/em&gt;&lt;/p&gt;
&lt;p id="5b1b"&gt;For most production systems, that bet pays out every single time.&lt;/p&gt;
&lt;p id="5503"&gt;Think of it like picking your starter in an RPG. Go is balanced stats across the board. Not the highest damage output, not the tankiest build but the one that gets you through 80% of the game without hitting a wall. You pick it, you learn the basics, and you’re shipping features by the end of the week.&lt;/p&gt;
&lt;p id="124e"&gt;The concurrency model is where Go genuinely earns its reputation.&lt;/p&gt;
&lt;p id="87e2"&gt;Goroutines are cheap enough to spin up thousands without sweating memory. The channel model gives you a mental framework for concurrent work that doesn’t require a degree in threading theory to reason about.&lt;/p&gt;
&lt;p id="2d4c"&gt;&lt;strong&gt;Building an SQS consumer that fans out to 20 parallel workers?&lt;/strong&gt;&lt;/p&gt;
&lt;p id="f782"&gt;That’s an afternoon in Go.&lt;/p&gt;
&lt;p id="b0d9"&gt;Writing a gRPC service sitting behind an ALB on ECS?&lt;/p&gt;
&lt;p id="4ecd"&gt;Go makes that boring in the best possible way. And boring is exactly what you want in a production system .&lt;/p&gt;
&lt;p id="83e6"&gt;&lt;strong&gt;The AWS ecosystem leans into this hard:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="d9f0"&gt;Fast startup times mean tighter Lambda cold starts&lt;/li&gt;

&lt;li id="dc38"&gt;Low memory footprint means cheaper ECS containers&lt;/li&gt;

&lt;li id="7fac"&gt;The &lt;a href="https://github.com/aws/aws-sdk-go-v2" rel="noopener ugc nofollow noreferrer"&gt;AWS SDK for Go v2&lt;/a&gt; covers everything from S3 to EventBridge without fighting you&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="c290"&gt;The broader ecosystem is settled too. &lt;a href="https://github.com/gin-gonic/gin" rel="noopener ugc nofollow noreferrer"&gt;Gin&lt;/a&gt; and &lt;a href="https://github.com/go-chi/chi" rel="noopener ugc nofollow noreferrer"&gt;Chi&lt;/a&gt; for HTTP routing, &lt;a href="https://sqlc.dev/" rel="noopener ugc nofollow noreferrer"&gt;sqlc&lt;/a&gt; for type-safe queries, &lt;a href="https://github.com/google/wire" rel="noopener ugc nofollow noreferrer"&gt;Wire&lt;/a&gt; for dependency injection if that’s your thing. The compiler errors are readable. Onboarding a new engineer onto a Go codebase takes days, not weeks.&lt;/p&gt;
&lt;p id="8a16"&gt;That last point matters more than most architecture blogs admit.&lt;/p&gt;
&lt;p id="5ab6"&gt;The best language for your system is the one your team can debug at midnight without wanting to quit their job. Go clears that bar repeatedly.&lt;/p&gt;
&lt;p id="b857"&gt;Where Go starts showing cracks is predictable if you know where to look.&lt;/p&gt;
&lt;p id="129f"&gt;The garbage collector has improved dramatically but GC pauses are still a reality in latency-sensitive workloads. Memory efficiency plateaus when you’re doing genuinely CPU-heavy work. And if you’re processing high-throughput data streams where every microsecond of predictability matters, Go’s runtime is making decisions for you that you might not agree with.&lt;/p&gt;
&lt;p id="0708"&gt;That’s not a criticism. That’s Go doing exactly what it was designed to do.&lt;/p&gt;
&lt;p id="d3b3"&gt;The tradeoff is explicit. Most teams never hit the ceiling.&lt;/p&gt;
&lt;p id="2c9d"&gt;The ones that do need a different tool for that specific corner of the system.&lt;/p&gt;
&lt;p id="a2eb"&gt;That tool has a crab mascot and a notoriously opinionated compiler.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="3130"&gt;Rust: the late-game unlock you didn’t know you needed&lt;/h2&gt;
&lt;p id="5a22"&gt;Nobody picks up Rust on day one.&lt;/p&gt;
&lt;p id="5ad1"&gt;You come to Rust after you’ve shipped something. After you’ve scaled something. After you’ve sat in an incident review trying to explain why your Go service started spiking p99 latency at a completely unpredictable interval and the only honest answer was “the GC decided it was time.”&lt;/p&gt;
&lt;p id="0bbd"&gt;That’s the origin story for most Rust adoption in production. Not ideology. Not a rewrite-everything agenda. Just a specific part of the system that stopped behaving and needed a tool with harder guarantees.&lt;/p&gt;
&lt;p id="5fe4"&gt;Think of it like unlocking a late-game weapon in an RPG. You don’t get it at the start. You earn it. And once you have it, you don’t use it on every enemy you save it for the boss fights.&lt;/p&gt;
&lt;p id="c3b1"&gt;The core promise of Rust is control.&lt;/p&gt;
&lt;p id="c1b2"&gt;No garbage collector means no GC pauses. No runtime surprises. What you write is what runs, with predictable memory behavior from the first request to the millionth. The borrow checker the thing everyone complains about until they don’t is just the compiler enforcing that promise at build time instead of letting it blow up in production at 3am.&lt;/p&gt;
&lt;p id="3dc8"&gt;&lt;strong&gt;In AWS terms, this matters in very specific places:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="2f77"&gt;High-throughput &lt;a href="https://aws.amazon.com/kinesis/" rel="noopener ugc nofollow noreferrer"&gt;Kinesis&lt;/a&gt; or Kafka consumers where processing latency compounds&lt;/li&gt;

&lt;li id="f3f7"&gt;Lambda functions where cold start time and memory ceiling are both constrained&lt;/li&gt;

&lt;li id="6afd"&gt;ECS services doing CPU-heavy transformation work on tight instance budgets&lt;/li&gt;

&lt;li id="1445"&gt;Real-time fraud detection or risk scoring pipelines where a GC pause is a business problem&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="e810"&gt;The async ecosystem has matured to the point where this is actually enjoyable to build now. &lt;a href="https://tokio.rs/" rel="noopener ugc nofollow noreferrer"&gt;Tokio&lt;/a&gt; is the async runtime most production Rust services are built on, and &lt;a href="https://github.com/tokio-rs/axum" rel="noopener ugc nofollow noreferrer"&gt;Axum&lt;/a&gt; gives you an ergonomic HTTP layer that won’t make you miss Go’s simplicity quite as much as you’d expect.&lt;/p&gt;
&lt;p id="4cb5"&gt;&lt;a href="https://crates.io/" rel="noopener ugc nofollow noreferrer"&gt;Crates.io&lt;/a&gt; has filled in the gaps that used to make Rust feel incomplete for backend work. There’s a crate for almost everything now serialization, database access, observability, AWS integrations. It’s not Go’s ecosystem in terms of breadth, but it’s not the frontier territory it was three years ago either.&lt;/p&gt;
&lt;p id="7b33"&gt;The WASM angle is worth mentioning too.&lt;/p&gt;
&lt;p id="98b8"&gt;Rust compiles to WebAssembly better than almost anything else, which opens up edge compute scenarios &lt;a href="https://workers.cloudflare.com/" rel="noopener ugc nofollow noreferrer"&gt;Cloudflare Workers&lt;/a&gt;, &lt;a href="https://www.fastly.com/products/edge-compute" rel="noopener ugc nofollow noreferrer"&gt;Fastly Compute&lt;/a&gt; where you want near-native performance in a serverless container with a sub-millisecond cold start. Go can do this too, but Rust’s output is leaner and the toolchain support is stronger.&lt;/p&gt;
&lt;p id="9689"&gt;The honest cost of Rust is team velocity at least upfront.&lt;/p&gt;
&lt;p id="ce87"&gt;The borrow checker has opinions. Strong ones. Loudly expressed. Onboarding an engineer who hasn’t written Rust before is a multi-week investment, not a multi-day one. Code review takes longer. Simple things that would take an hour in Go can take a morning in Rust while you negotiate with the compiler about who owns what.&lt;/p&gt;
&lt;p id="366f"&gt;&lt;strong&gt;But here’s the flip side nobody puts in the benchmarks post:&lt;/strong&gt;&lt;/p&gt;
&lt;p id="1d8a"&gt;Once the code compiles, it tends to just work.&lt;/p&gt;
&lt;p id="a8c8"&gt;Not “works in staging” work. Not “works until load testing” work. The class of bugs that Rust’s type system eliminates at compile time null pointer dereferences, data races, use-after-free are the exact bugs that cause 2am incidents in Go and every other language that trusts you more than the compiler does.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="7aef"&gt;The tradeoff isn’t really speed vs safety. It’s upfront cost vs long-term stability.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="4567"&gt;For the right part of your system, that’s an easy trade.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="1667"&gt;Real architecture patterns: how teams actually use both&lt;/h2&gt;
&lt;p id="c7bc"&gt;Here’s what nobody tells you in the language comparison posts:&lt;/p&gt;
&lt;p id="e973"&gt;Most production systems that use Rust don’t replace Go.&lt;/p&gt;
&lt;p id="e74e"&gt;They add Rust to specific coordinates in the architecture where Go stopped being enough. The rest stays Go. The team keeps shipping. Nobody rewrites everything and nobody has an existential crisis about the stack.&lt;/p&gt;
&lt;p id="f75f"&gt;These are the three patterns that show up repeatedly in real systems.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A840%2F1%2AYBjyE6plETSD-FthMyOu_Q.jpeg"&gt;&lt;h3 id="7a93"&gt;Pattern 1: Go orchestrates, Rust crunches&lt;/h3&gt;
&lt;p id="eaf8"&gt;This is the most common one and the cleanest.&lt;/p&gt;
&lt;p id="c653"&gt;Go handles everything that talks to other things the API layer, the service-to-service communication, the SQS consumers that fan out work, the schedulers that trigger jobs. It’s the connective tissue of the system. Fast enough, easy to reason about, easy to change.&lt;/p&gt;
&lt;p id="552e"&gt;Rust handles the part that actually does the heavy lifting.&lt;/p&gt;
&lt;p id="fd4e"&gt;A real example: a data pipeline that ingests raw events from Kinesis, runs enrichment and scoring logic, and writes results to DynamoDB. The Go service manages the consumer group, handles retries, and pushes work into a processing queue. The Rust binary does the actual scoring CPU-bound, memory-intensive, latency-sensitive. Two languages, one coherent system, each doing the job it was built for.&lt;/p&gt;
&lt;h3 id="7511"&gt;Pattern 2: Cost optimization at scale&lt;/h3&gt;
&lt;p id="e409"&gt;This one sneaks up on you.&lt;/p&gt;
&lt;p id="3af5"&gt;Your Go services are fast enough. Response times are fine. Users aren’t complaining. But the AWS bill is climbing faster than your traffic is growing, and when you dig in, you find a handful of services that are just eating CPU and memory disproportionately.&lt;/p&gt;
&lt;p id="62ee"&gt;That’s the cost optimization signal.&lt;/p&gt;
&lt;p id="67ab"&gt;Rewriting those specific services in Rust not everything, just the ones burning resources can drop memory footprint significantly and increase throughput per instance. Fewer instances needed. Smaller instance sizes. Same traffic handled for less money.&lt;/p&gt;
&lt;p id="b3af"&gt;At low scale this is a rounding error. At high scale this is a conversation your CFO notices.&lt;/p&gt;
&lt;p id="2ed2"&gt;The &lt;a href="https://benchmarksgame-team.pages.debian.net/benchmarksgame/" rel="noopener ugc nofollow noreferrer"&gt;Benchmarks Game&lt;/a&gt; numbers aren’t perfectly representative of production workloads, but the directional signal is real: Rust is consistently 2–5x more efficient than Go on CPU-bound work, and the memory story is even more pronounced.&lt;/p&gt;
&lt;h3 id="f9b6"&gt;Pattern 3: Latency-sensitive systems where GC pauses are a product problem&lt;/h3&gt;
&lt;p id="2738"&gt;Some systems can’t tolerate unpredictability at the tail.&lt;/p&gt;
&lt;p id="fe68"&gt;Financial systems where a p99 spike causes a missed execution window. Voice and video processing where a pause means a glitch the user hears. Real-time analytics dashboards where a stall breaks the illusion of live data.&lt;/p&gt;
&lt;p id="61a0"&gt;In these cases Go’s GC even with tuning introduces a variance floor that you can’t engineer away. You can shrink it. You can schedule around it. You can’t eliminate it.&lt;/p&gt;
&lt;p id="3e89"&gt;Rust eliminates it.&lt;/p&gt;
&lt;p id="ead1"&gt;No runtime. No collector. The memory behavior of a Rust service under load is the same as the memory behavior of a Rust service at rest deterministic, predictable, boring in exactly the way your SLA needs it to be.&lt;/p&gt;
&lt;p id="6ad1"&gt;This isn’t about raw speed. It’s about the shape of the latency distribution. Go might average faster on a given workload and still lose this comparison because the tail is worse.&lt;/p&gt;
&lt;p id="afbe"&gt;&lt;strong&gt;The common thread across all three patterns is the same:&lt;/strong&gt;&lt;/p&gt;
&lt;p id="6bc2"&gt;You don’t choose Rust instead of Go. You choose Rust for the specific part of the system where Go’s tradeoffs stop working in your favor. Everything else stays exactly as it was.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="bfa9"&gt;Build with Go. Optimize with Rust. Ship both.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="69f7"&gt;The wall: when Go stops being enough&lt;/h2&gt;
&lt;p id="67f5"&gt;Every backend system hits a wall.&lt;/p&gt;
&lt;p id="8308"&gt;At first, everything is fine.&lt;/p&gt;
&lt;p id="927d"&gt;You spin up services, deploy to ECS, wire up queues, add a scheduler, scale horizontally things just work. APIs respond fast enough. The team ships features. The infra bill is acceptable.&lt;/p&gt;
&lt;p id="3d30"&gt;&lt;strong&gt;Then growth happens.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="1a17"&gt;Traffic increases&lt;/li&gt;

&lt;li id="3034"&gt;Latency spikes in weird places&lt;/li&gt;

&lt;li id="92a0"&gt;Costs start climbing faster than usage&lt;/li&gt;

&lt;li id="0bb0"&gt;Debugging gets harder&lt;/li&gt;

&lt;li id="3dd1"&gt;Small inefficiencies compound into real problems&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="6330"&gt;And suddenly the question changes.&lt;/p&gt;
&lt;p id="4dfa"&gt;&lt;strong&gt;It’s no longer:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="ca8f"&gt;&lt;em&gt;“How fast can we build this?”&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="1a19"&gt;&lt;strong&gt;It becomes:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="4cc3"&gt;&lt;em&gt;“How do we keep this system predictable, efficient, and scalable without slowing the team down?”&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="0f8d"&gt;That’s where the Go vs Rust decision actually starts to matter. Not at the beginning. Right when you hit that wall.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A840%2F1%2A9h-HKQk7LWmitkUr4JK5LA.jpeg"&gt;&lt;h3 id="708a"&gt;How to know if you’ve actually hit it&lt;/h3&gt;
&lt;p id="8540"&gt;Before you start a Rust migration conversation, run this check first.&lt;/p&gt;
&lt;p id="504a"&gt;Most systems that feel slow aren’t CPU-bound. They’re waiting on databases, on network calls, on downstream services that are slower than they should be. Rewriting a Go service in Rust doesn’t fix a Postgres query that’s missing an index. It doesn’t fix an N+1 problem in your ORM. It doesn’t fix a Lambda that’s cold-starting because you gave it 128MB of memory and called it done.&lt;/p&gt;
&lt;p id="87d8"&gt;Profile before you decide.&lt;/p&gt;
&lt;p id="f528"&gt;If your bottleneck is I/O and it usually is Go is not your problem. Fix the query. Add the cache. Right-size the instance. Go home.&lt;/p&gt;
&lt;p id="a216"&gt;If your bottleneck is genuinely CPU sustained high utilization, processing-bound work, memory pressure that doesn’t resolve with horizontal scalingthen you have a real signal. That’s the wall. That’s where Rust earns the conversation.&lt;/p&gt;
&lt;h3 id="9d19"&gt;The team cost is real too&lt;/h3&gt;
&lt;p id="cddd"&gt;Here’s the part that gets skipped in every benchmark post.&lt;/p&gt;
&lt;p id="ddfe"&gt;&lt;strong&gt;Introducing Rust into a Go shop isn’t free. You’re adding a second language to your codebase, which means:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="4923"&gt;Two build pipelines to maintain&lt;/li&gt;

&lt;li id="d8c8"&gt;Two sets of idioms for new engineers to learn&lt;/li&gt;

&lt;li id="5e9e"&gt;Code review that requires Rust-literate reviewers&lt;/li&gt;

&lt;li id="4ad5"&gt;A slower ramp for anyone joining the team fresh&lt;/li&gt;

&lt;/ul&gt;
&lt;p id="7b07"&gt;None of that is disqualifying. All of it is real. The question is whether the performance gain in the specific component you’re optimizing is worth the ongoing operational tax across the whole team.&lt;/p&gt;
&lt;p id="a8c2"&gt;&lt;strong&gt;For most teams, the answer is:&lt;/strong&gt; yes, but only for a small slice of the system.&lt;/p&gt;
&lt;h3 id="5c94"&gt;The quick reference&lt;/h3&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="391" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A840%2F1%2A8HFH8J1PhAK6VOjTXAtTGw.png"&gt;&lt;h3 id="7a9a"&gt;&lt;strong&gt;The actual decision&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="135f"&gt;Use Go to build systems.&lt;/p&gt;
&lt;p id="6633"&gt;Use Rust to optimize systems.&lt;/p&gt;
&lt;p id="9cd3"&gt;Start in Go. Ship in Go. Run in Go. When a specific component starts costing you more than it should in latency, in money, in stability isolate it. Profile it. And if the data points at a genuine CPU or memory problem that Go can’t resolve without throwing more hardware at it, that’s your Rust entry point.&lt;/p&gt;
&lt;p id="7771"&gt;Don’t migrate the whole system. Rewrite the one service that earned it.&lt;/p&gt;
&lt;p id="f70c"&gt;Then go back to shipping in Go.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="945d"&gt;Conclusion: Go builds companies, Rust survives them&lt;/h2&gt;
&lt;p id="d118"&gt;&lt;strong&gt;Here’s the take nobody wants to say out loud:&lt;br&gt;&lt;/strong&gt;Most teams that argue about Go vs Rust don’t have a language problem. They have a prioritization problem. The system isn’t slow because of the runtime. It’s slow because of the decisions made three sprints ago that nobody had time to revisit.&lt;/p&gt;
&lt;p id="e2c4"&gt;Language choice is downstream of system design. Always.&lt;/p&gt;
&lt;p id="f28a"&gt;Go became the default backend language of the cloud-native era not because it’s the fastest or the most elegant or the most technically impressive thing you can put on a resume. It became the default because it consistently produces systems that teams can build quickly, reason about clearly, and operate without a dedicated platform engineering team just to keep the lights on.&lt;/p&gt;
&lt;p id="db8d"&gt;That’s genuinely hard to beat.&lt;/p&gt;
&lt;p id="b17d"&gt;Rust earns its place in the stack the same way any good tool earns its place by solving a specific problem better than everything else available. Not because it’s newer. Not because the crab mascot is charming. Because when you need deterministic memory behavior, zero GC overhead, and the kind of compile-time guarantees that let you sleep through the night without a PagerDuty notification, nothing else in the backend ecosystem comes close.&lt;/p&gt;
&lt;p id="6b3d"&gt;The developers who will win in the next few years aren’t the ones who picked a side in this debate. They’re the ones who got comfortable moving between both who can ship a Go service on a Tuesday and drop into a Rust codebase on a Thursday without losing a step.&lt;/p&gt;
&lt;p id="8be8"&gt;Polyglot isn’t a buzzword anymore. It’s a survival skill.&lt;/p&gt;
&lt;p id="0f20"&gt;The question was never Go &lt;em&gt;or&lt;/em&gt; Rust. It was always Go &lt;em&gt;and&lt;/em&gt; Rust, applied with enough discipline to know which one the problem actually needs.&lt;/p&gt;
&lt;p id="a549"&gt;Pick the tool that fits the job. Ship the thing. Move on.&lt;/p&gt;
&lt;p id="70a8"&gt;And if someone on your team is still writing LinkedIn posts about which language is objectively better in 2026 send them this article and go touch grass.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="7f27"&gt;&lt;strong&gt;What do you think? Is your team running both in production, or did you go all-in on one? Drop your take in the comments I read every one.&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="0699"&gt;Helpful resources&lt;/h2&gt;
&lt;ul&gt;

&lt;li id="2c44"&gt;

&lt;a href="https://go.dev/tour/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;A Tour of Go&lt;/strong&gt;&lt;/a&gt; best starting point if you’re new to Go&lt;/li&gt;

&lt;li id="c85e"&gt;

&lt;a href="https://doc.rust-lang.org/book/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;The Rust Book&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;the official, actually-readable Rust guide&lt;/li&gt;

&lt;li id="da22"&gt;

&lt;a href="https://tokio.rs/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Tokio async runtime&lt;/strong&gt;&lt;/a&gt; where most production Rust backend work lives&lt;/li&gt;

&lt;li id="251f"&gt;

&lt;a href="https://github.com/tokio-rs/axum" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Axum web framework&lt;/strong&gt;&lt;/a&gt; ergonomic HTTP for Rust services&lt;/li&gt;

&lt;li id="6fcd"&gt;

&lt;a href="https://github.com/aws/aws-sdk-go-v2" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;AWS SDK for Go v2&lt;/strong&gt;&lt;/a&gt; the one you actually want to use&lt;/li&gt;

&lt;li id="e321"&gt;

&lt;a href="https://benchmarksgame-team.pages.debian.net/benchmarksgame/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;The Benchmarks Game&lt;/strong&gt;&lt;/a&gt; real language performance comparisons, not blog post numbers&lt;/li&gt;

&lt;li id="4e18"&gt;

&lt;a href="https://crates.io/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Crates.io&lt;/strong&gt;&lt;/a&gt; Rust package registry, wa more mature than it used to be&lt;/li&gt;

&lt;li id="d573"&gt;

&lt;a href="https://github.com/gin-gonic/gin" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Gin HTTP framework&lt;/strong&gt;&lt;/a&gt; Go’s most popular web framework&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>go</category>
      <category>rust</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Junior devs catch exceptions. Senior devs prevent them. Here’s the difference.</title>
      <dc:creator>&lt;devtips/&gt;</dc:creator>
      <pubDate>Wed, 13 May 2026 03:09:14 +0000</pubDate>
      <link>https://dev.to/dev_tips/junior-devs-catch-exceptions-senior-devs-prevent-them-heres-the-difference-241g</link>
      <guid>https://dev.to/dev_tips/junior-devs-catch-exceptions-senior-devs-prevent-them-heres-the-difference-241g</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="a7a2"&gt;&lt;strong&gt;Four patterns that replace most of the try-catch in your codebase and the mental model that makes the right choice obvious.&lt;/strong&gt;&lt;/h2&gt;
&lt;span&gt;&lt;/span&gt;&lt;p id="f648"&gt;Every developer has written this code. You’re building something, you get a compiler warning, or maybe a runtime blow-up in testing, and the fastest fix feels obvious: wrap it. Slap a try-catch around it, log the message, return null, move on. Done. Safe.&lt;/p&gt;
&lt;p id="b4d0"&gt;Except it isn’t safe. You’ve just installed a smoke detector that beeps once and then goes quiet forever.&lt;/p&gt;
&lt;p id="df5d"&gt;I’ve been inside codebases where try-catch was the default answer to every uncomfortable question the compiler asked. Controllers wrapped in it. Services wrapped in it. Utility methods wrapped in it. The team described it as “defensive programming.” What it actually was: a distributed system of silence. Errors happened. Nobody knew. The first sign of a problem was usually a customer email.&lt;/p&gt;
&lt;p id="e21b"&gt;There’s a reason tutorials teach try-catch first. It &lt;em&gt;works&lt;/em&gt;, in the narrowest possible sense. The app doesn’t crash. The stack trace disappears. The demo runs clean. What the tutorial doesn’t show you is the 3am incident six months later when a payment silently failed for two hundred users and your logs say &lt;code&gt;"Something went wrong: null"&lt;/code&gt;.&lt;/p&gt;
&lt;p id="3ccc"&gt;Senior devs don’t write more try-catch than juniors. They write significantly less. Not because they’re reckless, but because they’ve learned to ask a different question before reaching for it: &lt;em&gt;can I stop this from going wrong in the first place?&lt;/em&gt;&lt;/p&gt;
&lt;p id="5df8"&gt;This article is about the four patterns that replace most of the try-catch you’re writing. Each one handles a real category of failure invalid input, unclear errors, duplicated handling logic, and expected outcomes that aren’t actually exceptional. Together they form a mental model that changes how you read code, not just how you write it.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="5f72"&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; try-catch is a reactive tool. Most of the time, the right move is to prevent, name, centralize, or model the failure not catch it in silence.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="e3ef"&gt;&lt;strong&gt;Why junior devs default to try-catch&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="3216"&gt;Here’s the thing: defaulting to try-catch isn’t stupidity. It’s pattern recognition from incomplete data.&lt;/p&gt;
&lt;p id="b77e"&gt;You learn Java, Python, or whatever backend language the bootcamp or degree threw at you. The first exception handling example looks like this: something might fail, so you wrap it, you catch it, you print the error, you move on. The tutorial works. You internalize the shape of the solution. From that point forward, any time the compiler complains or the runtime blows up, your brain pattern-matches straight to the familiar shape.&lt;/p&gt;
&lt;p id="e06b"&gt;Stack Overflow reinforces it. You search for your error, the accepted answer has a try-catch, it has 847 upvotes, someone accepted it in 2014, and it fixes your immediate problem. You copy it. You ship it. Repeat a few hundred times across a year of coding and you’ve built a reflex.&lt;/p&gt;
&lt;p id="f4ba"&gt;The problem isn’t the reflex. The problem is what it actually does to your codebase.&lt;/p&gt;
&lt;p id="3843"&gt;Every time you wrap code in try-catch, you’re making an implicit declaration: &lt;em&gt;I don’t know what’s going to go wrong here, so I’ll just intercept whatever happens and deal with it.&lt;/em&gt; That sounds humble. It isn’t. It’s actually abdication. You’ve handed the decision to runtime and told it to figure things out while you look away.&lt;/p&gt;
&lt;p id="100c"&gt;Think of it this way: try-catch is a bucket under a leaky pipe. It catches the drip. It keeps the floor dry for now. But the pipe is still leaking, the water is still going somewhere it shouldn’t, and the leak is getting slightly worse every week. The senior move is to call a plumber. The junior move is to empty the bucket on a cron job and call it fixed.&lt;/p&gt;
&lt;p id="84ed"&gt;&lt;strong&gt;Senior developers ask a different question before they reach for try-catch:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;

&lt;p id="1782"&gt;should this be able to fail at all?&lt;/p&gt;

&lt;p id="ce96"&gt;If invalid input causes the failure is the input even allowed in?&lt;/p&gt;

&lt;p id="cefb"&gt;If a null reference causes the crash why is null a valid state here?&lt;/p&gt;


&lt;/blockquote&gt;
&lt;p id="682a"&gt;The goal isn’t to catch the explosion. It’s to make the explosion impossible, or at least to make it impossible to ignore.&lt;/p&gt;
&lt;p id="eea5"&gt;That reframe is where the four patterns come from. Each one answers a version of that question for a different category of failure.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="7d36"&gt;&lt;strong&gt;Pattern 1: validate first, catch never&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="e2a3"&gt;The single most common reason junior devs reach for try-catch is bad input. A null slips through. A string arrives where a number was expected. An email field is missing. Something downstream blows up, the catch block fires, and the problem disappears into a log message nobody reads.&lt;/p&gt;
&lt;p id="b8a4"&gt;Here’s the thing though: none of that is an exception handling problem. It’s a validation problem. The failure didn’t happen because the system encountered something genuinely unexpected it happened because you let garbage through the front door and were surprised when it broke something in the kitchen.&lt;/p&gt;
&lt;p id="bb60"&gt;I spent three hours debugging a production issue once that turned out to be a null email field on a user registration request. Three hours. The stack trace was unhelpful, the catch block had swallowed the context, and the only log entry was something like &lt;code&gt;"User creation failed"&lt;/code&gt;. The fix itself took forty seconds once I found it. The try-catch didn't protect anything it just made the debugging slower and more painful.&lt;/p&gt;
&lt;p id="7499"&gt;&lt;strong&gt;The senior pattern here is straightforward:&lt;/strong&gt; validate at the boundary. Nothing invalid gets past the entry point, so nothing downstream needs to defend against it.&lt;/p&gt;
&lt;p id="017f"&gt;In a Spring application this looks like replacing a defensive catch block with a &lt;code&gt;@Valid&lt;/code&gt; annotation and letting the framework enforce the contract:&lt;/p&gt;
&lt;pre&gt;&lt;span id="4f4a"&gt;&lt;span&gt;// what fear-based programming looks like&lt;/span&gt;&lt;br&gt;&lt;span&gt;@PostMapping("/users")&lt;/span&gt;&lt;br&gt;&lt;span&gt;public&lt;/span&gt; ResponseEntity&amp;lt;User&amp;gt; &lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;span&gt;@RequestBody&lt;/span&gt; UserRequest request)&lt;/span&gt; {&lt;br&gt;    &lt;span&gt;try&lt;/span&gt; {&lt;br&gt;        &lt;span&gt;User&lt;/span&gt; &lt;span&gt;user&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; userService.create(request);&lt;br&gt;        &lt;span&gt;return&lt;/span&gt; ResponseEntity.ok(user);&lt;br&gt;    } &lt;span&gt;catch&lt;/span&gt; (NullPointerException e) {&lt;br&gt;        &lt;span&gt;return&lt;/span&gt; ResponseEntity.badRequest().body(&lt;span&gt;null&lt;/span&gt;);&lt;br&gt;    } &lt;span&gt;catch&lt;/span&gt; (IllegalArgumentException e) {&lt;br&gt;        &lt;span&gt;return&lt;/span&gt; ResponseEntity.badRequest().body(&lt;span&gt;null&lt;/span&gt;);&lt;br&gt;    }&lt;br&gt;}&lt;/span&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;span id="4d4a"&gt;&lt;span&gt;// what confidence looks like&lt;/span&gt;&lt;br&gt;&lt;span&gt;@PostMapping("/users")&lt;/span&gt;&lt;br&gt;&lt;span&gt;public&lt;/span&gt; ResponseEntity&amp;lt;User&amp;gt; &lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/valid"&gt;@valid&lt;/a&gt;&lt;/span&gt; &lt;span&gt;@RequestBody&lt;/span&gt; UserRequest request)&lt;/span&gt; {&lt;br&gt;    &lt;span&gt;User&lt;/span&gt; &lt;span&gt;user&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; userService.create(request);&lt;br&gt;    &lt;span&gt;return&lt;/span&gt; ResponseEntity.status(HttpStatus.CREATED).body(user);&lt;br&gt;}&lt;br&gt;&lt;br&gt;&lt;span&gt;public&lt;/span&gt; &lt;span&gt;class&lt;/span&gt; &lt;span&gt;UserRequest&lt;/span&gt; {&lt;br&gt;    &lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/notblank"&gt;@notblank&lt;/a&gt;(message = "Name is required")&lt;/span&gt;&lt;br&gt;    &lt;span&gt;private&lt;/span&gt; String name;&lt;br&gt;    &lt;span&gt;@Email(message = "Must be a valid email")&lt;/span&gt;&lt;br&gt;    &lt;span&gt;@NotNull(message = "Email is required")&lt;/span&gt;&lt;br&gt;    &lt;span&gt;private&lt;/span&gt; String email;&lt;br&gt;    &lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/min"&gt;@min&lt;/a&gt;(value = 18, message = "Must be at least 18")&lt;/span&gt;&lt;br&gt;    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;int&lt;/span&gt; age;&lt;br&gt;}&lt;/span&gt;&lt;/pre&gt;
&lt;p id="673a"&gt;The second version is shorter, cleaner, and more honest. The constraints live on the data object itself, which means any developer who opens &lt;code&gt;UserRequest&lt;/code&gt; immediately knows the rules. There's no hidden catch block somewhere else in the call stack silently absorbing violations. If the input is bad, the framework rejects it immediately with a clear message before it touches a single line of your business logic.&lt;/p&gt;
&lt;p id="db71"&gt;This pattern isn’t Spring-specific either. Django has form and serializer validation. Express has middleware validators. Laravel has request classes. Every mature backend framework has a first-class answer to this problem, and that answer is never “catch the NullPointerException deeper in the stack.”&lt;/p&gt;
&lt;p id="60cd"&gt;The rule worth internalizing: if you’re catching an exception caused by bad input, you don’t have an exception handling problem. You have a validation problem. Fix it upstream, not downstream.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A840%2F1%2AcjAhmPmGcQzeu-iIlMZa3g.jpeg"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="594e"&gt;&lt;strong&gt;Pattern 2: build a custom exception hierarchy&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="cc3e"&gt;So you’ve validated your input. Clean data is flowing through the system. But things still go wrong because they always do. A record doesn’t exist. A business rule gets violated. An order is already processed. These are real failures, and they need real handling.&lt;/p&gt;
&lt;p id="29e6"&gt;The junior response to this is &lt;code&gt;catch (Exception e)&lt;/code&gt;. Log the message. Return a 500. Hope the message is descriptive enough to debug later. It never is.&lt;/p&gt;
&lt;p id="72df"&gt;I’ve been in that 3am incident call. The alert fires, you pull up the logs, and staring back at you is: &lt;code&gt;"Something failed: null"&lt;/code&gt;. That's it. No context. No indication of which service, which record, which rule. Just a generic catch block somewhere in the stack that swallowed everything meaningful and handed you nothing. You start adding log statements, redeploying, waiting for it to happen again. It is not a fun way to spend a night.&lt;/p&gt;
&lt;p id="1350"&gt;The problem is that &lt;code&gt;Exception&lt;/code&gt; is the nuclear option. It catches everything the failure you expected, the one you didn't, the one that's actually a bug in a completely different part of the system. When you catch everything, you lose the ability to distinguish between any of it.&lt;/p&gt;
&lt;p id="5cbe"&gt;Senior devs build a hierarchy instead. You start with a base application exception, then extend it into specific types that are self-describing:&lt;/p&gt;
&lt;pre&gt;&lt;span id="aaed"&gt;&lt;span&gt;// base — everything app-specific extends this&lt;/span&gt;&lt;br&gt;&lt;span&gt;public&lt;/span&gt; &lt;span&gt;class&lt;/span&gt; &lt;span&gt;AppException&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;RuntimeException&lt;/span&gt; {&lt;br&gt;    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;final&lt;/span&gt; HttpStatus status;&lt;br&gt;    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;final&lt;/span&gt; String errorCode;&lt;br&gt;&lt;br&gt;&lt;span&gt;public&lt;/span&gt; &lt;span&gt;AppException&lt;/span&gt;&lt;span&gt;(String message, HttpStatus status, String errorCode)&lt;/span&gt; {&lt;br&gt;        &lt;span&gt;super&lt;/span&gt;(message);&lt;br&gt;        &lt;span&gt;this&lt;/span&gt;.status = status;&lt;br&gt;        &lt;span&gt;this&lt;/span&gt;.errorCode = errorCode;&lt;br&gt;    }&lt;br&gt;}&lt;br&gt;&lt;span&gt;// specific types that speak for themselves&lt;/span&gt;&lt;br&gt;&lt;span&gt;public&lt;/span&gt; &lt;span&gt;class&lt;/span&gt; &lt;span&gt;NotFoundException&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;AppException&lt;/span&gt; {&lt;br&gt;    &lt;span&gt;public&lt;/span&gt; &lt;span&gt;NotFoundException&lt;/span&gt;&lt;span&gt;(String resource, Long id)&lt;/span&gt; {&lt;br&gt;        &lt;span&gt;super&lt;/span&gt;(resource + &lt;span&gt;" not found with id: "&lt;/span&gt; + id,&lt;br&gt;              HttpStatus.NOT_FOUND, &lt;span&gt;"NOT_FOUND"&lt;/span&gt;);&lt;br&gt;    }&lt;br&gt;}&lt;br&gt;&lt;span&gt;public&lt;/span&gt; &lt;span&gt;class&lt;/span&gt; &lt;span&gt;BusinessRuleViolatedException&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;AppException&lt;/span&gt; {&lt;br&gt;    &lt;span&gt;public&lt;/span&gt; &lt;span&gt;BusinessRuleViolatedException&lt;/span&gt;&lt;span&gt;(String rule)&lt;/span&gt; {&lt;br&gt;        &lt;span&gt;super&lt;/span&gt;(&lt;span&gt;"Business rule violated: "&lt;/span&gt; + rule,&lt;br&gt;              HttpStatus.CONFLICT, &lt;span&gt;"BUSINESS_RULE_VIOLATED"&lt;/span&gt;);&lt;br&gt;    }&lt;br&gt;}&lt;/span&gt;&lt;/pre&gt;
&lt;p id="4cbb"&gt;&lt;strong&gt;Now your service code becomes readable without a single try-catch in sight:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="12b8"&gt;&lt;span&gt;public&lt;/span&gt; Order &lt;span&gt;processOrder&lt;/span&gt;&lt;span&gt;(OrderRequest request)&lt;/span&gt; {&lt;br&gt;    &lt;span&gt;Order&lt;/span&gt; &lt;span&gt;order&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; orderRepository.findById(request.getOrderId())&lt;br&gt;        .orElseThrow(() -&amp;gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;NotFoundException&lt;/span&gt;(&lt;span&gt;"Order"&lt;/span&gt;, request.getOrderId()));&lt;br&gt;&lt;br&gt;&lt;span&gt;if&lt;/span&gt; (order.isAlreadyProcessed()) {&lt;br&gt;        &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;BusinessRuleViolatedException&lt;/span&gt;(&lt;span&gt;"Order already processed"&lt;/span&gt;);&lt;br&gt;    }&lt;br&gt;    &lt;span&gt;return&lt;/span&gt; orderRepository.save(order);&lt;br&gt;}&lt;br&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="60de"&gt;When &lt;code&gt;NotFoundException&lt;/code&gt; gets thrown anywhere in the system, you already know what failed, why it failed, and what HTTP status to return. You don't need to grep through logs trying to reconstruct context. The exception &lt;em&gt;is&lt;/em&gt; the context.&lt;/p&gt;
&lt;p id="19c8"&gt;Think of it like hospital triage codes versus a nurse shouting “something’s wrong with someone.” Triage codes exist because specificity saves time when time costs lives. Your exception hierarchy exists for the same reason specificity saves debugging time when production is on fire.&lt;/p&gt;
&lt;p id="550d"&gt;There’s also a less obvious benefit here: a good exception hierarchy forces you to think about your failure modes upfront. When you’re defining &lt;code&gt;BusinessRuleViolatedException&lt;/code&gt;, you're implicitly cataloguing what your business rules actually are. That clarity leaks into your design in good ways.&lt;/p&gt;
&lt;p id="5c99"&gt;The rule: if you’re catching a generic exception and then trying to figure out what actually went wrong from the message string, your exceptions aren’t specific enough. Name the failure. Make it impossible to confuse with anything else.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A840%2F1%2AKgUjxEIJJrBzOoD9aWrz3Q.jpeg"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="4244"&gt;&lt;strong&gt;Pattern 3: handle everything in one place&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="b92e"&gt;You’ve got clean input coming in. You’ve got specific, named exceptions being thrown. Now the question is: where do you actually handle them?&lt;/p&gt;
&lt;p id="c48c"&gt;The junior answer is in every controller. You write a try-catch in the first endpoint, it works, you copy the pattern to the second endpoint, and the third, and the fifteenth. Six months later you have a codebase where every controller method is forty lines long and thirty of those lines are catch blocks doing nearly identical things. Changing the error response format means touching fifteen files. Adding a new exception type means hunting through every controller to add another catch branch. It compounds fast.&lt;/p&gt;
&lt;p id="c489"&gt;This is the copy-paste catch block epidemic, and almost every team above a certain age has lived through it.&lt;/p&gt;
&lt;p id="d0b6"&gt;The senior pattern collapses all of that into one place. In Spring, that’s &lt;code&gt;@RestControllerAdvice&lt;/code&gt; a single global handler that intercepts every unhandled exception from every controller in the application:&lt;/p&gt;
&lt;pre&gt;&lt;span id="e329"&gt;&lt;span&gt;@RestControllerAdvice&lt;/span&gt;&lt;br&gt;&lt;span&gt;public&lt;/span&gt; &lt;span&gt;class&lt;/span&gt; &lt;span&gt;GlobalExceptionHandler&lt;/span&gt; {&lt;br&gt;&lt;span&gt;@ExceptionHandler(NotFoundException.class)&lt;/span&gt;&lt;br&gt;    &lt;span&gt;public&lt;/span&gt; ResponseEntity&amp;lt;ErrorResponse&amp;gt; &lt;span&gt;handleNotFound&lt;/span&gt;&lt;span&gt;(NotFoundException e)&lt;/span&gt; {&lt;br&gt;        &lt;span&gt;return&lt;/span&gt; ResponseEntity.status(HttpStatus.NOT_FOUND)&lt;br&gt;            .body(&lt;span&gt;new&lt;/span&gt; &lt;span&gt;ErrorResponse&lt;/span&gt;(e.getErrorCode(), e.getMessage()));&lt;br&gt;    }&lt;br&gt;&lt;br&gt;    &lt;span&gt;@ExceptionHandler(BusinessRuleViolatedException.class)&lt;/span&gt;&lt;br&gt;    &lt;span&gt;public&lt;/span&gt; ResponseEntity&amp;lt;ErrorResponse&amp;gt; &lt;span&gt;handleBusinessRule&lt;/span&gt;&lt;span&gt;(&lt;br&gt;            BusinessRuleViolatedException e)&lt;/span&gt; {&lt;br&gt;        &lt;span&gt;return&lt;/span&gt; ResponseEntity.status(HttpStatus.CONFLICT)&lt;br&gt;            .body(&lt;span&gt;new&lt;/span&gt; &lt;span&gt;ErrorResponse&lt;/span&gt;(e.getErrorCode(), e.getMessage()));&lt;br&gt;    }&lt;br&gt;&lt;br&gt;    &lt;span&gt;@ExceptionHandler(MethodArgumentNotValidException.class)&lt;/span&gt;&lt;br&gt;    &lt;span&gt;public&lt;/span&gt; ResponseEntity&amp;lt;ErrorResponse&amp;gt; &lt;span&gt;handleValidation&lt;/span&gt;&lt;span&gt;(&lt;br&gt;            MethodArgumentNotValidException e)&lt;/span&gt; {&lt;br&gt;        &lt;span&gt;String&lt;/span&gt; &lt;span&gt;details&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; e.getBindingResult().getFieldErrors().stream()&lt;br&gt;            .map(error -&amp;gt; error.getField() + &lt;span&gt;": "&lt;/span&gt; + error.getDefaultMessage())&lt;br&gt;            .collect(Collectors.joining(&lt;span&gt;", "&lt;/span&gt;));&lt;br&gt;        &lt;span&gt;return&lt;/span&gt; ResponseEntity.status(HttpStatus.BAD_REQUEST)&lt;br&gt;            .body(&lt;span&gt;new&lt;/span&gt; &lt;span&gt;ErrorResponse&lt;/span&gt;(&lt;span&gt;"VALIDATION_FAILED"&lt;/span&gt;, details));&lt;br&gt;    }&lt;br&gt;&lt;br&gt;    &lt;span&gt;@ExceptionHandler(Exception.class)&lt;/span&gt;&lt;br&gt;    &lt;span&gt;public&lt;/span&gt; ResponseEntity&amp;lt;ErrorResponse&amp;gt; &lt;span&gt;handleUnexpected&lt;/span&gt;&lt;span&gt;(Exception e)&lt;/span&gt; {&lt;br&gt;        logger.error(&lt;span&gt;"Unexpected error"&lt;/span&gt;, e);&lt;br&gt;        &lt;span&gt;return&lt;/span&gt; ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)&lt;br&gt;            .body(&lt;span&gt;new&lt;/span&gt; &lt;span&gt;ErrorResponse&lt;/span&gt;(&lt;span&gt;"INTERNAL_ERROR"&lt;/span&gt;, &lt;span&gt;"Something went wrong"&lt;/span&gt;));&lt;br&gt;    }&lt;br&gt;}&lt;/span&gt;&lt;/pre&gt;
&lt;p id="fc74"&gt;&lt;strong&gt;And here’s what your controllers look like after:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="b792"&gt;&lt;span&gt;@GetMapping("/orders/{id}")&lt;/span&gt;&lt;br&gt;&lt;span&gt;public&lt;/span&gt; Order &lt;span&gt;getOrder&lt;/span&gt;&lt;span&gt;(&lt;span&gt;@PathVariable&lt;/span&gt; Long id)&lt;/span&gt; {&lt;br&gt;    &lt;span&gt;return&lt;/span&gt; orderService.findById(id);&lt;br&gt;}&lt;/span&gt;&lt;/pre&gt;
&lt;p id="0eec"&gt;That’s it. Three lines. The controller does exactly one thing: call the service and return the result. If &lt;code&gt;NotFoundException&lt;/code&gt; gets thrown, the global handler catches it, formats it correctly, and returns the right HTTP status. The controller never needed to know about any of that.&lt;/p&gt;
&lt;p id="9512"&gt;This is the bouncer analogy applied to architecture. You don’t put a bouncer at every table in a restaurant. You put one at the door. One point of entry, one set of rules, consistently enforced. Your &lt;code&gt;GlobalExceptionHandler&lt;/code&gt; is the door. Every exception has to pass through it, which means every exception gets handled the same way, every time, from one file you can actually reason about.&lt;/p&gt;
&lt;p id="7483"&gt;The practical benefits stack up quickly. Want to change your error response format? One file. Adding a new custom exception type? One new method in the handler. Need to add request ID tracking to every error response for distributed tracing? One change, instantly applied everywhere. What used to be a fifteen-file refactor becomes a three-line addition.&lt;/p&gt;
&lt;p id="ee50"&gt;The framework doesn’t matter much here either the pattern exists everywhere. Django has exception middleware. Express has error-handling middleware with the four-argument signature. Laravel has the &lt;code&gt;Handler&lt;/code&gt; class in &lt;code&gt;app/Exceptions&lt;/code&gt;. The idea is universal: centralize your exception handling, stop duplicating it.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="47bf"&gt;&lt;strong&gt;The rule:&lt;/strong&gt; if you’re writing the same catch logic in more than one place, you need a centralized handler. Every duplicate catch block is future maintenance debt you’re taking out a loan on right now.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="5a90"&gt;&lt;strong&gt;Pattern 4: result objects for expected failures&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="687e"&gt;This is the pattern that tends to produce the most argument in code reviews, which is usually a sign it’s worth understanding properly.&lt;/p&gt;
&lt;p id="5263"&gt;The premise is simple: not every failure is exceptional. “User not found” isn’t a system error it’s a Tuesday. “Payment declined” isn’t a catastrophe it’s an expected outcome of the payment flow. “Item out of stock” isn’t a bug it’s a valid state your business logic needs to handle gracefully.&lt;/p&gt;
&lt;p id="4251"&gt;When you model these as exceptions, you’re using the wrong tool. Exceptions exist for genuinely unexpected conditions things that shouldn’t happen under normal operation. Using them for routine business outcomes is like calling an ambulance because you got a parking ticket. Technically it gets the job done, but it’s expensive, it misleads everyone involved, and it trains the system to treat normal outcomes as emergencies.&lt;/p&gt;
&lt;p id="2db4"&gt;&lt;strong&gt;The result object pattern makes expected outcomes explicit in the return type instead:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="d0f7"&gt;&lt;span&gt;public&lt;/span&gt; &lt;span&gt;class&lt;/span&gt; &lt;span&gt;Result&lt;/span&gt;&amp;lt;T&amp;gt; {&lt;br&gt;    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;final&lt;/span&gt; T value;&lt;br&gt;    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;final&lt;/span&gt; String error;&lt;br&gt;    &lt;span&gt;private&lt;/span&gt; &lt;span&gt;final&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt; success;&lt;br&gt;&lt;br&gt;&lt;span&gt;private&lt;/span&gt; &lt;span&gt;Result&lt;/span&gt;&lt;span&gt;(T value, String error, &lt;span&gt;boolean&lt;/span&gt; success)&lt;/span&gt; {&lt;br&gt;        &lt;span&gt;this&lt;/span&gt;.value = value;&lt;br&gt;        &lt;span&gt;this&lt;/span&gt;.error = error;&lt;br&gt;        &lt;span&gt;this&lt;/span&gt;.success = success;&lt;br&gt;    }&lt;br&gt;&lt;br&gt;    &lt;span&gt;public&lt;/span&gt; &lt;span&gt;static&lt;/span&gt; &amp;lt;T&amp;gt; Result&amp;lt;T&amp;gt; &lt;span&gt;ok&lt;/span&gt;&lt;span&gt;(T value)&lt;/span&gt; {&lt;br&gt;        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Result&lt;/span&gt;&amp;lt;&amp;gt;(value, &lt;span&gt;null&lt;/span&gt;, &lt;span&gt;true&lt;/span&gt;);&lt;br&gt;    }&lt;br&gt;&lt;br&gt;    &lt;span&gt;public&lt;/span&gt; &lt;span&gt;static&lt;/span&gt; &amp;lt;T&amp;gt; Result&amp;lt;T&amp;gt; &lt;span&gt;failure&lt;/span&gt;&lt;span&gt;(String error)&lt;/span&gt; {&lt;br&gt;        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Result&lt;/span&gt;&amp;lt;&amp;gt;(&lt;span&gt;null&lt;/span&gt;, error, &lt;span&gt;false&lt;/span&gt;);&lt;br&gt;    }&lt;br&gt;&lt;br&gt;    &lt;span&gt;public&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt; &lt;span&gt;isSuccess&lt;/span&gt;&lt;span&gt;()&lt;/span&gt; { &lt;span&gt;return&lt;/span&gt; success; }&lt;br&gt;    &lt;span&gt;public&lt;/span&gt; T &lt;span&gt;getValue&lt;/span&gt;&lt;span&gt;()&lt;/span&gt; { &lt;span&gt;return&lt;/span&gt; value; }&lt;br&gt;    &lt;span&gt;public&lt;/span&gt; String &lt;span&gt;getError&lt;/span&gt;&lt;span&gt;()&lt;/span&gt; { &lt;span&gt;return&lt;/span&gt; error; }&lt;br&gt;}&lt;/span&gt;&lt;/pre&gt;
&lt;p id="4613"&gt;&lt;strong&gt;Now your service communicates both possible outcomes in its signature:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="d118"&gt;&lt;span&gt;// before — the caller has no idea this can fail&lt;/span&gt;&lt;br&gt;&lt;span&gt;// until the exception surprises them at runtime&lt;/span&gt;&lt;br&gt;&lt;span&gt;public&lt;/span&gt; User &lt;span&gt;findUser&lt;/span&gt;&lt;span&gt;(Long id)&lt;/span&gt; {&lt;br&gt;    &lt;span&gt;return&lt;/span&gt; userRepository.findById(id)&lt;br&gt;        .orElseThrow(() -&amp;gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;NotFoundException&lt;/span&gt;(&lt;span&gt;"User"&lt;/span&gt;, id));&lt;br&gt;}&lt;br&gt;&lt;br&gt;&lt;span&gt;// after - the return type tells the whole story upfront&lt;/span&gt;&lt;br&gt;&lt;span&gt;public&lt;/span&gt; Result&amp;lt;User&amp;gt; &lt;span&gt;findUser&lt;/span&gt;&lt;span&gt;(Long id)&lt;/span&gt; {&lt;br&gt;    &lt;span&gt;return&lt;/span&gt; userRepository.findById(id)&lt;br&gt;        .map(Result::ok)&lt;br&gt;        .orElse(Result.failure(&lt;span&gt;"User not found with id: "&lt;/span&gt; + id));&lt;br&gt;}&lt;/span&gt;&lt;/pre&gt;
&lt;p id="d244"&gt;&lt;strong&gt;And the caller handles both paths without needing a try-catch anywhere:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="3615"&gt;Result&amp;lt;User&amp;gt; result = userService.findUser(&lt;span&gt;123L&lt;/span&gt;);&lt;br&gt;&lt;span&gt;if&lt;/span&gt; (result.isSuccess()) {&lt;br&gt;    &lt;span&gt;return&lt;/span&gt; ResponseEntity.ok(result.getValue());&lt;br&gt;}&lt;br&gt;&lt;br&gt;&lt;span&gt;return&lt;/span&gt; ResponseEntity.notFound().build();&lt;/span&gt;&lt;/pre&gt;
&lt;p id="d7df"&gt;The difference in readability is significant. In the exception-throwing version, a developer reading the method signature sees &lt;code&gt;User findUser(Long id)&lt;/code&gt; and assumes it always returns a user. The failure mode is invisible until it bites them. In the result version, &lt;code&gt;Result&amp;lt;User&amp;gt; findUser(Long id)&lt;/code&gt; makes the contract obvious this operation produces either a user or an explanation of why it didn't. The caller is forced to acknowledge both possibilities.&lt;/p&gt;
&lt;p id="4ddc"&gt;This isn’t a new idea and it definitely isn’t Java-specific. Rust builds this directly into the language with &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt; if your function can fail, the type system requires you to say so and requires the caller to handle it. Go uses the idiomatic &lt;code&gt;(value, error)&lt;/code&gt; return pair for the same reason. Haskell has &lt;code&gt;Maybe&lt;/code&gt;. These languages treat explicit failure modeling as a first-class concern, not an afterthought.&lt;/p&gt;
&lt;p id="74c4"&gt;The Java ecosystem is slowly catching up. Libraries like &lt;a href="https://www.vavr.io/" rel="noopener ugc nofollow noreferrer"&gt;Vavr&lt;/a&gt; bring proper &lt;code&gt;Either&lt;/code&gt; and &lt;code&gt;Try&lt;/code&gt; types to the JVM. The direction the industry is moving is clear: make failure visible in the type signature, not hidden behind runtime surprises.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="6032"&gt;&lt;strong&gt;The rule:&lt;/strong&gt; if a scenario is expected not a bug, not a system fault, just a normal outcome your business logic needs to handle don’t throw an exception. Return a result. Make the failure a first-class citizen of your API, not a trap waiting for the next developer.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A840%2F1%2As-CChVWn-5gi5k4eAWmMDg.jpeg"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="6654"&gt;&lt;strong&gt;The mental model that ties it all together&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="08a1"&gt;Four patterns is a lot to hold in your head at once. The good news is there’s a single question underneath all of them that makes the right choice obvious once you’ve internalized it:&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="23f5"&gt;&lt;em&gt;Is this failure exceptional, or is it expected?&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="0549"&gt;That’s it. Two buckets. Everything goes in one of them.&lt;/p&gt;
&lt;p id="adc9"&gt;&lt;strong&gt;Exceptional failures&lt;/strong&gt; are things that shouldn’t happen under normal operating conditions. The database connection drops mid-request. A third-party API times out after working fine for months. The server runs out of memory. A file that should always exist gets deleted by something outside your application. These are infrastructure-level surprises conditions your code didn’t cause and can’t reasonably prevent. They deserve try-catch, proper logging, alerting, and a human looking at them.&lt;/p&gt;
&lt;p id="b8fb"&gt;&lt;strong&gt;Expected failures&lt;/strong&gt; are outcomes that are entirely normal within your business domain. A user searches for a record that doesn’t exist. A payment gets declined because the card is expired. Someone tries to book a seat that just got taken. An order is submitted twice. These aren’t bugs. They’re valid states your system needs to navigate gracefully. They deserve validation, result objects, and clean control flow not try-catch, not stack traces, not PagerDuty alerts at midnight.&lt;/p&gt;
&lt;p id="5933"&gt;Once you start sorting failures into these two buckets automatically, the right pattern follows almost without thinking. Bad input coming in? That’s preventable validate it at the boundary. A business rule getting violated? Name it explicitly with a custom exception. Error handling logic spreading across controllers? Centralize it. A routine “not found” outcome? Model it as a result, not an emergency.&lt;/p&gt;
&lt;p id="23c1"&gt;The reason most codebases end up with try-catch everywhere is that nobody ever drew this line. Everything got treated as equally unpredictable, equally dangerous, equally worthy of the same blunt instrument. The bucket metaphor is simple, but the discipline of actually applying it consistently is what separates a codebase you can debug at 3am from one that makes you want to update your resume at 3am.&lt;/p&gt;
&lt;p id="50fb"&gt;Think of it like your notification system. A fire alarm and a calendar reminder are both alerts. But they belong to completely different categories of urgency, and you’d never design a system that treated them the same way. Your exception handling is your application’s notification system. Design it with the same intentionality.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A840%2F1%2AK957r6bZ_STvVH-goPwBdg.jpeg"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="fb85"&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="4451"&gt;Here’s the uncomfortable truth the tutorials won’t tell you: most of the try-catch in production codebases today exists because the people who wrote it were taught a pattern without being taught the &lt;em&gt;reasoning&lt;/em&gt; behind it. Copy, paste, ship. It works until it doesn’t, and when it doesn’t, it fails in the worst possible way silently, slowly, invisibly.&lt;/p&gt;
&lt;p id="3442"&gt;The four patterns in this article aren’t advanced techniques. They’re not senior-level secrets. They’re just what happens when you start asking “why is this failing?” instead of “how do I make this stop crashing?” Validate the input. Name the failure. Centralize the handling. Model the expected outcomes. None of it is complicated. All of it compounds.&lt;/p&gt;
&lt;p id="94c9"&gt;The industry is already moving in this direction. Languages like Rust make explicit error modeling non-negotiable at the compiler level. TypeScript’s ecosystem is increasingly leaning on discriminated unions for the same purpose. The direction is clear: failures belong in the type signature, not hiding behind runtime surprises in catch blocks nobody reads.&lt;/p&gt;
&lt;h3 id="7935"&gt;&lt;strong&gt;Do this tomorrow one change at a time:&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="b78d"&gt;Find one try-catch in your codebase that’s catching bad input. Replace it with proper validation a &lt;code&gt;@Valid&lt;/code&gt; annotation, a schema check, a guard clause. Just one. Find one &lt;code&gt;catch (Exception e)&lt;/code&gt; that's swallowing everything. Replace it with a specific custom exception that names the actual failure. If you have catch blocks duplicated across multiple controllers or handlers, build a centralized exception handler and delete the copies. Find one place where you're throwing an exception for an outcome that's entirely expected. Replace it with a result object.&lt;/p&gt;
&lt;p id="4386"&gt;Four changes. One codebase that’s measurably more honest about what it does and what can go wrong.&lt;/p&gt;
&lt;p id="e924"&gt;The worst try-catch I ever saw in production wrapped an entire service class constructor and returned null on failure. The service would silently initialize as null, work fine for a while, then NullPointerException somewhere completely unrelated, no trail, no context, nothing. Three days of debugging. The fix was six lines.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="cdc9"&gt;What’s yours? Drop the worst try-catch you’ve ever shipped in the comments. No judgment we’ve all got one.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="9373"&gt;&lt;strong&gt;Helpful resources&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;

&lt;li id="82a1"&gt;

&lt;a href="https://jakarta.ee/specifications/bean-validation/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Jakarta Bean Validation specification&lt;/strong&gt;&lt;/a&gt; the standard behind &lt;code&gt;@Valid&lt;/code&gt; and constraint annotations&lt;/li&gt;

&lt;li id="c36d"&gt;

&lt;a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ControllerAdvice.html" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Spring @ControllerAdvice documentation&lt;/strong&gt;&lt;/a&gt; official reference for global exception handling&lt;/li&gt;

&lt;li id="ba08"&gt;

&lt;a href="https://www.vavr.io/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Vavr library&lt;/strong&gt;&lt;/a&gt; brings &lt;code&gt;Either&lt;/code&gt;, &lt;code&gt;Try&lt;/code&gt;, and &lt;code&gt;Option&lt;/code&gt; types to Java&lt;/li&gt;

&lt;li id="42e1"&gt;

&lt;a href="https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Rust Book: Recoverable errors with Result&lt;/strong&gt;&lt;/a&gt; the gold standard for how explicit error modeling should work&lt;/li&gt;

&lt;li id="5a8a"&gt;

&lt;a href="https://go.dev/blog/error-handling-and-go" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Error Handling in Go&lt;/strong&gt;&lt;/a&gt; Google’s official take on the (value, error) pattern&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
    <item>
      <title>10 SQL changes. One took 30 seconds. It cut query time by 85%.</title>
      <dc:creator>&lt;devtips/&gt;</dc:creator>
      <pubDate>Tue, 12 May 2026 06:38:34 +0000</pubDate>
      <link>https://dev.to/dev_tips/10-sql-changes-one-took-30-seconds-it-cut-query-time-by-85-4f1f</link>
      <guid>https://dev.to/dev_tips/10-sql-changes-one-took-30-seconds-it-cut-query-time-by-85-4f1f</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="55c3"&gt;&lt;em&gt;A 2015 research paper tested every tip against real data. Most developers have never seen it. The numbers are hard to ignore.&lt;/em&gt;&lt;/h2&gt;
&lt;span&gt;&lt;/span&gt;&lt;blockquote&gt;&lt;p id="cff5"&gt;You wrote a query. It returned the right data. You moved on.&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p id="2977"&gt;That’s the whole story for most developers. The query works, the feature ships, and nobody looks back. Until a senior engineer pulls up the execution plan in a prod review and asks why you’re doing a full table scan on 2 million rows to return twelve records.&lt;/p&gt;
&lt;p id="df59"&gt;That moment has happened to more engineers than will ever admit it publicly.&lt;/p&gt;
&lt;p id="c1a1"&gt;Here’s the thing: most SQL slowness isn’t mysterious. It’s not a missing index, a misconfigured database, or a vendor problem. It’s patterns habits that looked fine when the table had 500 rows and became quietly catastrophic when it hit 5 million.&lt;/p&gt;
&lt;p id="d348"&gt;In 2015, a researcher named Jean Habimana published a paper through IJSTR titled &lt;em&gt;“Query Optimization Techniques: Tips For Writing Efficient And Faster SQL Queries.”&lt;/em&gt; Five pages. Tested against Oracle’s sample Sales database. Every tip benchmarked with actual time reductions. It’s been sitting in academic obscurity ever since, which is a shame, because some of these changes take thirty seconds to make and show query time reductions above 80%.&lt;/p&gt;
&lt;p id="86c4"&gt;&lt;strong&gt;This article is that paper, translated into something you’ll actually read.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="b035"&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; 10 SQL habits ranked by impact. Time reductions range from 11% to 85%. Most fixes take under five minutes. A few will make you retroactively embarrassed about queries you shipped last year.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="df27"&gt;Why your query isn’t just “running”&lt;/h2&gt;
&lt;p id="9242"&gt;Most SQL tutorials skip this part entirely. They go straight to syntax, joins, and GROUP BY, and leave you with a model of SQL that looks roughly like: &lt;em&gt;you write it, the database runs it, data comes back.&lt;/em&gt; Clean. Simple. Wrong.&lt;/p&gt;
&lt;p id="e656"&gt;When you submit a query, it doesn’t execute directly. It goes through a query optimizer first a component that reads your SQL, estimates multiple ways to fetch the result, and picks the one it thinks is cheapest. Cheapest meaning least I/O, least memory, least CPU. The optimizer is making decisions you never see, and those decisions are heavily influenced by how you wrote the query.&lt;/p&gt;
&lt;p id="5e47"&gt;Think of it like a GPS. You give it a destination and it figures out the route. But if you give it bad inputs a vague address, a restricted road it doesn’t know about it picks a worse path. The database optimizer works the same way. Write the query clearly and it finds the fast route. Write it carelessly and it takes the scenic route through every row in your table.&lt;/p&gt;
&lt;p id="95cb"&gt;&lt;strong&gt;The execution path looks roughly like this:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="a225"&gt;Your &lt;span&gt;SQL&lt;/span&gt;&lt;br&gt;    ↓&lt;br&gt;Query Parser — checks syntax&lt;br&gt;    ↓&lt;br&gt;Query Optimizer — estimates cheapest execution path&lt;br&gt;    ↓&lt;br&gt;Execution Plan — the actual instruction &lt;span&gt;set&lt;/span&gt;&lt;br&gt;    ↓&lt;br&gt;Data retrieval → &lt;span&gt;Result&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="b4f8"&gt;Every tip in this article targets one of two things: either helping the optimizer make a better decision, or removing work it shouldn’t have to do in the first place. That’s the whole framework. Keep it in mind as you read the rest.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="427f"&gt;The lazy habits costing you the most&lt;/h2&gt;
&lt;p id="42c2"&gt;Let’s start with the number that stopped me mid-scroll when I first read this paper: &lt;strong&gt;85% query time reduction&lt;/strong&gt;. Not from adding an index. Not from upgrading hardware. From removing one word.&lt;/p&gt;
&lt;h3 id="5acf"&gt;Tip 3 first because 85% deserves the spotlight&lt;/h3&gt;
&lt;p id="455d"&gt;When you write &lt;code&gt;SELECT DISTINCT &lt;em&gt;&lt;/em&gt;&lt;/code&gt; on a join where the primary key is already in the result, duplicates are mathematically impossible. There are no duplicates to remove. But the database doesn't know that so it sorts the entire result set and checks anyway. You're paying full deduplication cost for zero benefit.&lt;/p&gt;
&lt;pre&gt;&lt;span id="e859"&gt;&lt;span&gt;-- Slow: DISTINCT is doing nothing here (primary key present)&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; &lt;span&gt;DISTINCT&lt;/span&gt; &lt;span&gt;&lt;/span&gt; &lt;span&gt;FROM&lt;/span&gt; sales s&lt;br&gt;&lt;span&gt;JOIN&lt;/span&gt; customers c &lt;span&gt;ON&lt;/span&gt; s.cust_id &lt;span&gt;=&lt;/span&gt; c.cust_id;&lt;br&gt;&lt;br&gt;&lt;span&gt;-- Fast: just don't&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; &lt;span&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt; &lt;span&gt;FROM&lt;/span&gt; sales s&lt;br&gt;&lt;span&gt;JOIN&lt;/span&gt; customers c &lt;span&gt;ON&lt;/span&gt; s.cust_id &lt;span&gt;=&lt;/span&gt; c.cust_id;&lt;/span&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p id="4480"&gt;&lt;strong&gt;85% faster. One word removed.&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id="56b3"&gt;Tip 1: SELECT columns, not SELECT * 27% reduction&lt;/h3&gt;
&lt;p id="74d8"&gt;The most universal bad habit in SQL. When you write &lt;code&gt;SELECT *&lt;/code&gt;, the database fetches every column and ships it across the network. If you need two columns from a table with twenty, you're paying for eighteen you'll never use. If any of those columns are large types TEXT, BLOB, JSON you're paying a lot.&lt;/p&gt;
&lt;pre&gt;&lt;span id="182b"&gt;&lt;span&gt;-- Slow&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; &lt;span&gt;&lt;/span&gt; &lt;span&gt;FROM&lt;/span&gt; sales;&lt;br&gt;&lt;br&gt;&lt;span&gt;-- Fast&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; prod_id, cust_id &lt;span&gt;FROM&lt;/span&gt; sales;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="1cc5"&gt;It feels pedantic until your table has 40 columns and half of them are storing documents.&lt;/p&gt;
&lt;h3 id="e9b7"&gt;Tip 2: WHERE before GROUP BY, not HAVING 31% reduction&lt;/h3&gt;
&lt;p id="5be2"&gt;&lt;code&gt;HAVING&lt;/code&gt; filters rows after grouping. &lt;code&gt;WHERE&lt;/code&gt; filters rows before grouping. If your condition doesn't involve an aggregate function, it has no business being in &lt;code&gt;HAVING&lt;/code&gt;. Putting it there means the database groups every row first, then throws away the ones you didn't want work it never needed to do.&lt;/p&gt;
&lt;pre&gt;&lt;span id="98f5"&gt;&lt;span&gt;-- Slow: groups everything, then filters&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; cust_id, &lt;span&gt;COUNT&lt;/span&gt;(cust_id)&lt;br&gt;&lt;span&gt;FROM&lt;/span&gt; sales&lt;br&gt;&lt;span&gt;GROUP&lt;/span&gt; &lt;span&gt;BY&lt;/span&gt; cust_id&lt;br&gt;&lt;span&gt;HAVING&lt;/span&gt; cust_id &lt;span&gt;!=&lt;/span&gt; &lt;span&gt;'1660'&lt;/span&gt;;&lt;br&gt;&lt;br&gt;&lt;span&gt;-- Fast: filters first, groups less&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; cust_id, &lt;span&gt;COUNT&lt;/span&gt;(cust_id)&lt;br&gt;&lt;span&gt;FROM&lt;/span&gt; sales&lt;br&gt;&lt;span&gt;WHERE&lt;/span&gt; cust_id &lt;span&gt;!=&lt;/span&gt; &lt;span&gt;'1660'&lt;/span&gt;&lt;br&gt;&lt;span&gt;GROUP&lt;/span&gt; &lt;span&gt;BY&lt;/span&gt; cust_id;&lt;/span&gt;&lt;/pre&gt;
&lt;h3 id="e500"&gt;Tip 3: Drop unnecessary DISTINCT 85% reduction&lt;/h3&gt;
&lt;p id="5477"&gt;Here it is. The biggest number in the paper.&lt;/p&gt;
&lt;p id="6b32"&gt;When you write &lt;code&gt;SELECT DISTINCT &lt;em&gt;&lt;/em&gt;&lt;/code&gt; on a join where the primary key is already in the result, duplicates are mathematically impossible. There are no duplicates to remove. But the database doesn't know that so it sorts the entire result set and checks anyway. You're paying full deduplication cost for zero benefit.&lt;/p&gt;
&lt;pre&gt;&lt;span id="367f"&gt;&lt;span&gt;-- Slow: DISTINCT is doing nothing here (primary key present)&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; &lt;span&gt;DISTINCT&lt;/span&gt; &lt;span&gt;&lt;/span&gt; &lt;span&gt;FROM&lt;/span&gt; sales s&lt;br&gt;&lt;span&gt;JOIN&lt;/span&gt; customers c &lt;span&gt;ON&lt;/span&gt; s.cust_id &lt;span&gt;=&lt;/span&gt; c.cust_id;&lt;br&gt;&lt;br&gt;&lt;span&gt;-- Fast: just don't&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; &lt;span&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt; &lt;span&gt;FROM&lt;/span&gt; sales s&lt;br&gt;&lt;span&gt;JOIN&lt;/span&gt; customers c &lt;span&gt;ON&lt;/span&gt; s.cust_id &lt;span&gt;=&lt;/span&gt; c.cust_id;&lt;/span&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p id="7409"&gt;&lt;strong&gt;85% faster. One word removed.&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id="4c26"&gt;Tip 4: Un-nest your subqueries 61% reduction&lt;/h3&gt;
&lt;p id="e12f"&gt;Correlated subqueries are the silent performance killer most junior devs don’t recognize until it’s too late. A subquery inside a &lt;code&gt;WHERE&lt;/code&gt; clause that references the outer query runs once per row. Not once total once &lt;em&gt;per row&lt;/em&gt;. On a table with 100,000 rows, that's 100,000 executions of your inner query.&lt;/p&gt;
&lt;p id="ac21"&gt;&lt;strong&gt;A JOIN runs once.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="4fc4"&gt;&lt;span&gt;-- Slow: subquery executes for every row in products&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; &lt;span&gt;&lt;/span&gt; &lt;span&gt;FROM&lt;/span&gt; products p&lt;br&gt;&lt;span&gt;WHERE&lt;/span&gt; p.prod_id &lt;span&gt;=&lt;/span&gt; (&lt;br&gt;    &lt;span&gt;SELECT&lt;/span&gt; s.prod_id &lt;span&gt;FROM&lt;/span&gt; sales s&lt;br&gt;    &lt;span&gt;WHERE&lt;/span&gt; s.cust_id &lt;span&gt;=&lt;/span&gt; &lt;span&gt;100996&lt;/span&gt;&lt;br&gt;);&lt;br&gt;&lt;br&gt;&lt;span&gt;-- Fast: single join operation&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; p.&lt;span&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt; &lt;span&gt;FROM&lt;/span&gt; products p&lt;br&gt;&lt;span&gt;JOIN&lt;/span&gt; sales s &lt;span&gt;ON&lt;/span&gt; p.prod_id &lt;span&gt;=&lt;/span&gt; s.prod_id&lt;br&gt;&lt;span&gt;WHERE&lt;/span&gt; s.cust_id &lt;span&gt;=&lt;/span&gt; &lt;span&gt;100996&lt;/span&gt;;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="670e"&gt;If you’ve ever watched a query go from instant to “still running after 40 seconds” as the table grew, a correlated subquery somewhere is usually the culprit.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="f563"&gt;Four tips. Average time reduction across all four: &lt;strong&gt;51%.&lt;/strong&gt; And none of them required touching your schema, your indexes, or your infrastructure. Just the query.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2A6KSFHB2E8tGb6MSE5REjLA.jpeg"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="6458"&gt;The optimizer killers&lt;/h2&gt;
&lt;p id="b62a"&gt;These four are sneakier. The previous section was mostly about obvious waste fetching columns you don’t need, grouping rows you’re about to throw away. This section is about patterns that look perfectly reasonable but quietly prevent the optimizer from using your indexes. That distinction matters a lot at scale.&lt;/p&gt;
&lt;h3 id="ed68"&gt;Tip 5: IN instead of multiple ORs 73% reduction&lt;/h3&gt;
&lt;p id="32d0"&gt;This one surprises people. Both &lt;code&gt;IN&lt;/code&gt; and &lt;code&gt;OR&lt;/code&gt; feel like they're doing the same thing, and syntactically they kind of are. The difference is what the optimizer can do with them.&lt;/p&gt;
&lt;p id="b0b8"&gt;With an &lt;code&gt;IN&lt;/code&gt; list, the optimizer can sort the values and match them against the index in order. With chained &lt;code&gt;OR&lt;/code&gt; conditions, it can't apply that optimization it evaluates each condition independently, often falling back to a full scan.&lt;/p&gt;
&lt;pre&gt;&lt;span id="890c"&gt;&lt;span&gt;-- Slow&lt;/span&gt;&lt;br&gt;&lt;span&gt;WHERE&lt;/span&gt; prod_id &lt;span&gt;=&lt;/span&gt; &lt;span&gt;14&lt;/span&gt; &lt;span&gt;OR&lt;/span&gt; prod_id &lt;span&gt;=&lt;/span&gt; &lt;span&gt;17&lt;/span&gt;;&lt;br&gt;&lt;br&gt;&lt;span&gt;-- Fast&lt;/span&gt;&lt;br&gt;&lt;span&gt;WHERE&lt;/span&gt; prod_id &lt;span&gt;IN&lt;/span&gt; (&lt;span&gt;14&lt;/span&gt;, &lt;span&gt;17&lt;/span&gt;);&lt;/span&gt;&lt;/pre&gt;
&lt;p id="3154"&gt;Small change. The optimizer sees a completely different instruction. &lt;strong&gt;73% faster.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="ca0d"&gt;Tip 6: EXISTS over DISTINCT on one-to-many joins 61% reduction&lt;/h3&gt;
&lt;p id="4ed5"&gt;When you join a parent table to a child table in a one-to-many relationship and slap &lt;code&gt;DISTINCT&lt;/code&gt; on it to collapse the duplicates, the database fetches every matching row from both tables and then deduplicates the whole thing. That's a lot of rows moved around just to throw most of them away.&lt;/p&gt;
&lt;p id="219c"&gt;&lt;code&gt;EXISTS&lt;/code&gt; short-circuits. It checks whether a match exists and stops the moment it finds one. It never fetches the duplicates in the first place.&lt;/p&gt;
&lt;pre&gt;&lt;span id="4075"&gt;&lt;span&gt;-- Slow: fetches everything, then deduplicates&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; &lt;span&gt;DISTINCT&lt;/span&gt; c.country_id, c.country_name&lt;br&gt;&lt;span&gt;FROM&lt;/span&gt; countries c&lt;br&gt;&lt;span&gt;JOIN&lt;/span&gt; customers e &lt;span&gt;ON&lt;/span&gt; e.country_id &lt;span&gt;=&lt;/span&gt; c.country_id;&lt;br&gt;&lt;br&gt;&lt;span&gt;-- Fast: stops at first match per country&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; c.country_id, c.country_name&lt;br&gt;&lt;span&gt;FROM&lt;/span&gt; countries c&lt;br&gt;&lt;span&gt;WHERE&lt;/span&gt; &lt;span&gt;EXISTS&lt;/span&gt; (&lt;br&gt;    &lt;span&gt;SELECT&lt;/span&gt; &lt;span&gt;1&lt;/span&gt; &lt;span&gt;FROM&lt;/span&gt; customers e&lt;br&gt;    &lt;span&gt;WHERE&lt;/span&gt; e.country_id &lt;span&gt;=&lt;/span&gt; c.country_id&lt;br&gt;);&lt;/span&gt;&lt;/pre&gt;
&lt;p id="43db"&gt;Once you understand the short-circuit behavior, you’ll see &lt;code&gt;DISTINCT&lt;/code&gt; on joins differently. It's not wrong it's just usually the expensive way to ask a simple question.&lt;/p&gt;
&lt;h3 id="c6c8"&gt;Tip 7: UNION ALL over UNION 81% reduction&lt;/h3&gt;
&lt;p id="157f"&gt;&lt;code&gt;UNION&lt;/code&gt; deduplicates. &lt;code&gt;UNION ALL&lt;/code&gt; doesn't. That's the entire difference, and it costs &lt;strong&gt;81%&lt;/strong&gt; of your query time when the data can't have duplicates anyway or when you simply don't care.&lt;/p&gt;
&lt;pre&gt;&lt;span id="9c7b"&gt;&lt;span&gt;-- Slow: scans combined result for duplicates&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; cust_id &lt;span&gt;FROM&lt;/span&gt; sales&lt;br&gt;&lt;span&gt;UNION&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; cust_id &lt;span&gt;FROM&lt;/span&gt; customers;&lt;br&gt;&lt;br&gt;&lt;span&gt;-- Fast: skips deduplication entirely&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; cust_id &lt;span&gt;FROM&lt;/span&gt; sales&lt;br&gt;&lt;span&gt;UNION&lt;/span&gt; &lt;span&gt;ALL&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; cust_id &lt;span&gt;FROM&lt;/span&gt; customers;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="40a1"&gt;The rule is simple: if your data sources can’t produce duplicates by definition, or if downstream logic handles it, &lt;code&gt;UNION ALL&lt;/code&gt; is strictly faster. Using &lt;code&gt;UNION&lt;/code&gt; by default because it feels safer is leaving 81% on the table.&lt;/p&gt;
&lt;h3 id="4250"&gt;Tip 8: Split OR in JOIN conditions into UNION ALL 70% reduction&lt;/h3&gt;
&lt;p id="52cd"&gt;This is the least obvious one in the entire paper, and probably the most common silent killer in production codebases. An &lt;code&gt;OR&lt;/code&gt; condition inside a &lt;code&gt;JOIN&lt;/code&gt; prevents index usage on both sides. The optimizer sees it and gives up on the index entirely, falling back to a full scan of both tables.&lt;/p&gt;
&lt;p id="5107"&gt;The fix is to split it into two separate joins and combine them with &lt;code&gt;UNION ALL&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;span id="1acf"&gt;&lt;span&gt;-- Slow: OR blocks index usage on both columns&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; &lt;span&gt;&lt;/span&gt; &lt;span&gt;FROM&lt;/span&gt; costs c&lt;br&gt;&lt;span&gt;INNER&lt;/span&gt; &lt;span&gt;JOIN&lt;/span&gt; products p&lt;br&gt;&lt;span&gt;ON&lt;/span&gt; c.unit_price &lt;span&gt;=&lt;/span&gt; p.prod_min_price&lt;br&gt;&lt;span&gt;OR&lt;/span&gt; c.unit_price &lt;span&gt;=&lt;/span&gt; p.prod_list_price;&lt;br&gt;&lt;br&gt;&lt;span&gt;-- Fast: two indexed joins, combined&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; &lt;span&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt; &lt;span&gt;FROM&lt;/span&gt; costs c&lt;br&gt;&lt;span&gt;INNER&lt;/span&gt; &lt;span&gt;JOIN&lt;/span&gt; products p &lt;span&gt;ON&lt;/span&gt; c.unit_price &lt;span&gt;=&lt;/span&gt; p.prod_min_price&lt;br&gt;&lt;span&gt;UNION&lt;/span&gt; &lt;span&gt;ALL&lt;/span&gt;&lt;br&gt;&lt;span&gt;SELECT&lt;/span&gt; &lt;span&gt;&lt;/span&gt; &lt;span&gt;FROM&lt;/span&gt; costs c&lt;br&gt;&lt;span&gt;INNER&lt;/span&gt; &lt;span&gt;JOIN&lt;/span&gt; products p &lt;span&gt;ON&lt;/span&gt; c.unit_price &lt;span&gt;=&lt;/span&gt; p.prod_list_price;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="7eb9"&gt;It looks more verbose. It is more verbose. It’s also 70% faster because both joins can now use their indexes cleanly. Verbose and fast beats clean and slow every time a product manager asks why the report takes three minutes to load.&lt;/p&gt;
&lt;p id="9def"&gt;If you want to see exactly what your optimizer is doing with any of these patterns, MySQL’s optimizer trace and PostgreSQL’s &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; will show you the execution plan in detail and make the index abandonment painfully visible.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="a6ce"&gt;&lt;strong&gt;Four tips.&lt;/strong&gt; Reductions of 73%, 61%, 81%, and 70%. All from patterns that look harmless in a code review because the query returns the right data. Correctness and performance are different problems, and SQL will let you solve one while completely ignoring the other.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2AWLL1OxLSiRzr55j5J6SH_Q.jpeg"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="3cc9"&gt;The silent ones&lt;/h2&gt;
&lt;p id="b82e"&gt;These two don’t announce themselves. No error, no warning, no slow query log entry when the table is small. You write them, they work, and then six months later when the data has grown and something is mysteriously sluggish, nobody connects it back to these lines.&lt;/p&gt;
&lt;h3 id="8ca9"&gt;Tip 9: No functions on indexed columns in WHERE 70% reduction&lt;/h3&gt;
&lt;p id="85d8"&gt;This one is responsible for more silent performance regressions than almost anything else on this list. The logic feels completely reasonable when you write it: you need to filter by year, the column stores full dates, so you wrap it in &lt;code&gt;EXTRACT()&lt;/code&gt; and move on.&lt;/p&gt;
&lt;pre&gt;&lt;span id="e0bd"&gt;&lt;span&gt;-- Slow: function call prevents index usage&lt;/span&gt;&lt;br&gt;&lt;span&gt;WHERE&lt;/span&gt; &lt;span&gt;EXTRACT&lt;/span&gt;(&lt;span&gt;YEAR&lt;/span&gt; &lt;span&gt;FROM&lt;/span&gt; time_id) &lt;span&gt;=&lt;/span&gt; &lt;span&gt;2001&lt;/span&gt;;&lt;br&gt;&lt;br&gt;&lt;span&gt;-- Fast: BETWEEN works with the index&lt;/span&gt;&lt;br&gt;&lt;span&gt;WHERE&lt;/span&gt; time_id &lt;span&gt;BETWEEN&lt;/span&gt; &lt;span&gt;'01-JAN-2001'&lt;/span&gt; &lt;span&gt;AND&lt;/span&gt; &lt;span&gt;'31-DEC-2001'&lt;/span&gt;;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="8a3e"&gt;Here’s what actually happens: the moment you wrap a column in a function inside a &lt;code&gt;WHERE&lt;/code&gt; clause, the optimizer can't use the index on that column anymore. The index is organized around the raw column values. Once you transform those values with a function, the index has no idea how to help so it doesn't. The database computes &lt;code&gt;EXTRACT()&lt;/code&gt; for every single row, then filters. Full scan. Every time.&lt;/p&gt;
&lt;p id="62a6"&gt;The fix is to move the transformation to the value side, not the column side. Instead of asking “what year does this date belong to,” ask “does this date fall inside this year.” &lt;code&gt;BETWEEN&lt;/code&gt; two known date boundaries does exactly that, leaves the column untouched, and lets the index do its job.&lt;/p&gt;
&lt;p id="e4e6"&gt;This applies beyond &lt;code&gt;EXTRACT()&lt;/code&gt;. Any function wrapping an indexed column in a &lt;code&gt;WHERE&lt;/code&gt; clause &lt;code&gt;UPPER()&lt;/code&gt;, &lt;code&gt;LOWER()&lt;/code&gt;, &lt;code&gt;CAST()&lt;/code&gt;, &lt;code&gt;DATE_TRUNC()&lt;/code&gt;, &lt;code&gt;TO_CHAR()&lt;/code&gt; breaks index usage the same way. It's a category of mistake, not just one specific function.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="efbc"&gt;&lt;strong&gt;70% reduction. Zero schema changes required.&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id="70f6"&gt;Tip 10: Pre-calculate your math 11% reduction&lt;/h3&gt;
&lt;p id="8ca2"&gt;This one is smaller 11% but it might be the most embarrassing item on the list once you understand what’s happening.&lt;/p&gt;
&lt;pre&gt;&lt;span id="c038"&gt;&lt;span&gt;-- Slow: recalculates for every row&lt;/span&gt;&lt;br&gt;&lt;span&gt;WHERE&lt;/span&gt; cust_id &lt;span&gt;+&lt;/span&gt; &lt;span&gt;10000&lt;/span&gt; &lt;span&gt;&amp;lt;&lt;/span&gt; &lt;span&gt;35000&lt;/span&gt;;&lt;br&gt;&lt;br&gt;&lt;span&gt;-- Fast: constant evaluated once&lt;/span&gt;&lt;br&gt;&lt;span&gt;WHERE&lt;/span&gt; cust_id &lt;span&gt;&amp;lt;&lt;/span&gt; &lt;span&gt;25000&lt;/span&gt;;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="775c"&gt;When your &lt;code&gt;WHERE&lt;/code&gt; clause contains arithmetic on a column, the database evaluates that expression for every row it scans. Not once every row. The value &lt;code&gt;10000&lt;/code&gt; isn't changing between rows. You already know the answer is &lt;code&gt;25000&lt;/code&gt;. But you made the database do the same addition hundreds of thousands of times because you didn't do it yourself first.&lt;/p&gt;
&lt;p id="55e1"&gt;Do the math before the query runs. It costs you nothing and saves the database from doing redundant arithmetic at scale.&lt;/p&gt;
&lt;p id="ad32"&gt;11% might sound modest compared to the numbers earlier in this article. But this is also the change that takes literally ten seconds. There’s no tradeoff to weigh, no refactor to plan. Just move the arithmetic outside the query. If you’re leaving 11% on the table because the fix felt too small to bother with, that’s a habit worth breaking.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="9d00"&gt;Both of these fall into the same category: the optimizer wanted to help, and the way the query was written made that impossible. The database didn’t fail it did exactly what you asked. You just didn’t realize what you were asking.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="5210"&gt;The pre-ship checklist&lt;/h2&gt;
&lt;p id="a50e"&gt;Before you push that query whether it’s going into an ORM, a stored procedure, a data pipeline, or directly into prod run it through this. Seven questions. Takes thirty seconds.&lt;/p&gt;
&lt;pre&gt;&lt;span id="60ff"&gt;SELECT * anywhere?          → Specify only the columns you need&lt;br&gt;Filtering in HAVING?        → Move it to WHERE if no aggregate involved&lt;br&gt;Any DISTINCT?               → Do you actually need it, or is the key already there?&lt;br&gt;Nested subquery?            → Can it be rewritten as a JOIN?&lt;br&gt;OR in WHERE or JOIN?        → Try IN or UNION ALL instead&lt;br&gt;Function wrapping a column? → Move the transformation to the value side&lt;br&gt;Math on a column?           → Pre-calculate it before the query runs&lt;/span&gt;&lt;/pre&gt;
&lt;p id="39fa"&gt;None of these require a DBA, a schema change, or a ticket. They’re query-level habits. The kind that separate code that works from code that scales.&lt;/p&gt;
&lt;p id="1b60"&gt;Bookmark this. Drop it in your team’s wiki. Put it in your &lt;code&gt;CLAUDE.md&lt;/code&gt; if you're using Claude Code. Stick it somewhere you'll actually see it before the slow query alert fires at an inconvenient hour.&lt;/p&gt;
&lt;p id="b060"&gt;For deeper reading on index behavior and execution plans, &lt;a href="https://use-the-index-luke.com" rel="noopener ugc nofollow noreferrer"&gt;Use The Index, Luke&lt;/a&gt; is the best free resource on the internet for this topic. No fluff, just mechanics.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="4d53"&gt;The queries you wrote last year are probably still running&lt;/h2&gt;
&lt;p id="91da"&gt;The paper this article is based on was published in 2015. The database you’re querying today almost certainly has rows that didn’t exist then. The query you wrote last year, maybe last month, is probably still executing exactly as you wrote it habits intact, DISTINCT in place, SELECT * quietly fetching columns nobody asked for.&lt;/p&gt;
&lt;p id="54c8"&gt;That’s the uncomfortable part. These aren’t exotic edge cases. They’re patterns that pass code review because the query returns correct results, and correctness is usually all anyone checks. Performance is someone else’s problem until it becomes everyone’s problem at 11pm on a Tuesday.&lt;/p&gt;
&lt;p id="d63e"&gt;The slightly spicy take: most slow SQL isn’t a database problem. It’s a habits problem. The optimizer is genuinely trying to help you it’s building execution plans, estimating costs, looking for index paths. What it can’t do is save you from instructions that actively prevent it from doing its job. Functions on indexed columns, OR conditions in joins, DISTINCT on results that can’t have duplicates these aren’t bugs the database can route around. They’re the query working exactly as written.&lt;/p&gt;
&lt;p id="e9ce"&gt;The good news is that optimizers are getting smarter. PostgreSQL 16 brought improvements to parallel query execution and partition pruning. AI-assisted query analysis tools are starting to surface execution plan issues automatically. The tooling is moving in the right direction.&lt;/p&gt;
&lt;p id="8202"&gt;But none of it will save you from &lt;code&gt;SELECT *&lt;/code&gt;.&lt;/p&gt;
&lt;p id="4680"&gt;The changes in this article aren’t framework upgrades or infrastructure investments. They’re rewrites. Most take under five minutes. Some show reductions above 80%. The paper tested them on Oracle but the underlying optimizer logic holds across PostgreSQL, MySQL, and SQL Server the principles are the same.&lt;/p&gt;
&lt;p id="34a1"&gt;Go check what your slowest queries are doing. There’s a reasonable chance one of these seven checklist items is in there.&lt;/p&gt;
&lt;p id="87cf"&gt;And if you’ve got a SQL habit that you’re mildly ashamed of a correlated subquery you know you should rewrite, a SELECT * you keep meaning to fix&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="0026"&gt;drop it in the comments. No judgment. We’ve all shipped something we’d rather not explain.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="1cd4"&gt;Resources&lt;/h2&gt;
&lt;ul&gt;

&lt;li id="86ab"&gt;

&lt;strong&gt;Original paper:&lt;/strong&gt; Jean Habimana, &lt;em&gt;“Query Optimization Techniques: Tips For Writing Efficient And Faster SQL Queries”&lt;/em&gt;, IJSTR Vol. 4, Issue 10, October 2015&lt;/li&gt;

&lt;li id="78ec"&gt;

&lt;a href="https://use-the-index-luke.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Use The Index, Luke&lt;/strong&gt;&lt;/a&gt; index mechanics explained without the academic fog&lt;/li&gt;

&lt;li id="68df"&gt;

&lt;a href="https://www.postgresql.org/docs/current/query-path.html" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;PostgreSQL query planning docs&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;how PG’s optimizer actually works&lt;/li&gt;

&lt;li id="be07"&gt;

&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/optimizer-tracing.html" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;MySQL optimizer trace&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;see exactly what MySQL is doing with your query&lt;/li&gt;

&lt;li id="0814"&gt;

&lt;a href="https://explain.dalibo.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;explain.dalibo.com&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;visual EXPLAIN ANALYZE for Postgres, free and genuinely useful&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>sql</category>
    </item>
    <item>
      <title>I built an AI agent in 50 lines of Python. Here’s what everyone gets wrong about them.</title>
      <dc:creator>&lt;devtips/&gt;</dc:creator>
      <pubDate>Mon, 11 May 2026 01:42:36 +0000</pubDate>
      <link>https://dev.to/dev_tips/i-built-an-ai-agent-in-50-lines-of-python-heres-what-everyone-gets-wrong-about-them-2l1d</link>
      <guid>https://dev.to/dev_tips/i-built-an-ai-agent-in-50-lines-of-python-heres-what-everyone-gets-wrong-about-them-2l1d</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h1 id="790c"&gt;&lt;/h1&gt;
&lt;h2 id="fb09"&gt;&lt;em&gt;You’ve been calling things agents for months. Most of them are just chatbots with extra steps.&lt;/em&gt;&lt;/h2&gt;
&lt;span&gt;&lt;/span&gt;&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p id="c78c"&gt;Every senior developer I know has done this at least once: you’re mid-review, someone asks how the agent loop actually works under the hood, and your brain quietly blue-screens. You’ve been using Claude, Cursor, Copilot, and Junie every single day. You’ve shipped features on top of them. You’ve nodded confidently in sprint planning while someone called the autocomplete a “multi-agent orchestration layer.” And yet, if someone put a gun to your head and asked you to draw the architecture the actual mechanics of what makes something an &lt;em&gt;agent&lt;/em&gt; instead of a really fast autocomplete you’d stall.&lt;/p&gt;
&lt;p id="a532"&gt;That was me about six weeks ago.&lt;/p&gt;
&lt;p id="b690"&gt;I got fed up with not knowing, so I did what I always do when a concept won’t click: I built the stupidest possible version of it from scratch. No LangChain. No LangGraph. No CrewAI. No framework scaffolding to hide behind. Just Python, an API key, and a while loop. Fifty lines. And somewhere around line thirty-two, the thing that’s been fuzzy for a year finally came into focus.&lt;/p&gt;
&lt;p id="677f"&gt;Turns out an agent isn’t magic. It’s not even particularly clever. It’s a deterministic loop with a short memory and a decision point. The model doesn’t “think.” It reads a conversation history, decides whether it has enough to answer or needs to call a tool, and either exits or loops. That’s it. Everything else the retry logic, the subagents, the memory persistence, the human-in-the-loop approvals is infrastructure layered on top of that core loop.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="899c"&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; This article walks through building a real AI agent from first principle cloud API brain, then local models via Ollama, then mixed-mode orchestration, then MCP tool sharing, then a straight talk about where frameworks actually earn their keep. By the end, you’ll be able to explain exactly what’s happening when Claude Code spins up a subagent, and you’ll have the code to prove you understand it.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="bc9c"&gt;&lt;strong&gt;What an agent actually is (and why most “agents” aren’t)&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="d16d"&gt;Here’s the real definition, stripped of the marketing: a regular LLM call is a one-shot operation. You send a prompt, you get a response, done. The model has no memory of what just happened. It doesn’t know if it got it right. It doesn’t retry. It just answers and exits.&lt;/p&gt;
&lt;p id="813e"&gt;An agent is different because it &lt;em&gt;loops&lt;/em&gt;. It takes a high-level task, reasons about what to do next, takes an action, observes the result, and keeps going until it decides it’s finished. That repeating cycle think, act, observe, decide is the thing that turns a language model into something that behaves like an autonomous system. Remove the loop and you just have a chatbot that called a function once.&lt;/p&gt;
&lt;p id="c6a5"&gt;Most things being marketed as “agents” right now are closer to the chatbot end of that spectrum. A RAG pipeline that retrieves documents and summarizes them isn’t an agent. A Slack bot that calls an API when you type a command isn’t an agent. They’re useful tools, but they don’t loop, they don’t observe results and adjust, and they don’t decide when to stop. Calling them agents is like calling a calculator a mathematician because it can add numbers.&lt;/p&gt;
&lt;p id="3c67"&gt;The pattern that most real agents follow is called ReAct Reasoning and Acting, introduced by Yao et al. in &lt;a href="https://arxiv.org/abs/2210.03629" rel="noopener ugc nofollow noreferrer"&gt;a 2022 paper&lt;/a&gt; that’s worth at least skimming. The idea is simple: the model doesn’t jump straight to a final answer. It produces a &lt;em&gt;thought&lt;/em&gt; about what to do, then an &lt;em&gt;action&lt;/em&gt; (a tool call), then waits to &lt;em&gt;observe&lt;/em&gt; the result before deciding what to do next. The loop continues until the model has enough context to answer directly without calling any more tools.&lt;/p&gt;
&lt;p id="4d13"&gt;Think of it like a developer assigned a Jira ticket. They don’t read it once and immediately output the solution. They read it, try something, hit an error, read the error, Google something, try again, check if the tests pass, then mark it done. The agent does the same thing it’s just that the conversation history is the Jira thread, the tools are the shell commands, and the LLM is the developer who never sleeps and never complains about the sprint velocity.&lt;/p&gt;
&lt;p id="7953"&gt;&lt;strong&gt;Here’s what every single cycle of the loop looks like under the hood:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;

&lt;li id="906d"&gt;Send the current conversation to the LLM system prompt, user message, any prior tool results&lt;/li&gt;

&lt;li id="7fda"&gt;The LLM returns either a final answer or a list of tool calls it wants to make&lt;/li&gt;

&lt;li id="9b53"&gt;If it’s a final answer, you’re done&lt;/li&gt;

&lt;li id="2161"&gt;If it’s tool calls, execute them, append the results to the conversation, go back to step 1&lt;/li&gt;

&lt;/ol&gt;
&lt;p id="6296"&gt;That’s the entire architecture. The model has no consciousness and no genuine self-reflection what it has is the full conversation history sitting in its context window, and a system prompt telling it what tools exist and when to stop. The ReAct pattern turns that into something that &lt;em&gt;looks&lt;/em&gt; like self-correction. And it works surprisingly well.&lt;/p&gt;
&lt;p id="87dd"&gt;The part people consistently underestimate is the system prompt. It’s not a formality. It’s the steering wheel. It tells the model when to use tools, when a task is complete, what a final answer should look like, and what it should never do. When leaked system prompts from production agents at Anthropic and Apple showed up online, the striking thing wasn’t the model sophistication it was how long and careful the system prompts were. Hundreds of lines of plain English, constraining behavior with precision. The loop is simple. The steering is where the craft lives.&lt;/p&gt;
&lt;h3 id="6658"&gt;&lt;strong&gt;Points:&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;

&lt;li id="5af5"&gt;Agent = a loop with a decision point, not a single LLM call with extra marketing&lt;/li&gt;

&lt;li id="c6cd"&gt;ReAct (Reason + Act) is the foundational pattern think, act, observe, repeat until done&lt;/li&gt;

&lt;li id="cbab"&gt;The model “self-corrects” because the conversation history accumulates not because it’s actually reflecting&lt;/li&gt;

&lt;li id="138f"&gt;The system prompt is the real logic layer underestimate it and your agent does whatever it wants&lt;/li&gt;

&lt;/ul&gt;
&lt;blockquote&gt;&lt;p id="fd4d"&gt;“The model doesn’t think. It reads a very long chat history and decides whether to answer or loop. That’s it.”&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="5472"&gt;&lt;strong&gt;The 50-line implementation cloud brain, zero framework&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="1580"&gt;Let’s build the thing. The core of every agent ever written is a function that looks roughly like this:&lt;/p&gt;
&lt;pre&gt;&lt;span id="b231"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;run_agent&lt;/span&gt;(&lt;span&gt;task: &lt;span&gt;str&lt;/span&gt;, client: OpenAI, model: &lt;span&gt;str&lt;/span&gt; = &lt;span&gt;"gpt-4o-mini"&lt;/span&gt;&lt;/span&gt;) -&amp;gt; &lt;span&gt;str&lt;/span&gt;:&lt;br&gt;    messages = [&lt;br&gt;        {&lt;br&gt;            &lt;span&gt;"role"&lt;/span&gt;: &lt;span&gt;"system"&lt;/span&gt;,&lt;br&gt;            &lt;span&gt;"content"&lt;/span&gt;: (&lt;br&gt;                &lt;span&gt;"You are a helpful assistant. Use tools when needed. "&lt;/span&gt;&lt;br&gt;                &lt;span&gt;"When you have a final answer, respond without calling any tools."&lt;/span&gt;&lt;br&gt;            ),&lt;br&gt;        },&lt;br&gt;        {&lt;span&gt;"role"&lt;/span&gt;: &lt;span&gt;"user"&lt;/span&gt;, &lt;span&gt;"content"&lt;/span&gt;: task},&lt;br&gt;    ]&lt;br&gt;&lt;br&gt;&lt;span&gt;while&lt;/span&gt; &lt;span&gt;True&lt;/span&gt;:&lt;br&gt;        response = client.chat.completions.create(&lt;br&gt;            model=model,&lt;br&gt;            messages=messages,&lt;br&gt;            tools=TOOLS,&lt;br&gt;            tool_choice=&lt;span&gt;"auto"&lt;/span&gt;,&lt;br&gt;        )&lt;br&gt;&lt;br&gt;        message = response.choices[&lt;span&gt;0&lt;/span&gt;].message&lt;br&gt;        messages.append(message)&lt;br&gt;&lt;br&gt;        &lt;span&gt;if&lt;/span&gt; &lt;span&gt;not&lt;/span&gt; message.tool_calls:&lt;br&gt;            &lt;span&gt;return&lt;/span&gt; message.content&lt;br&gt;&lt;br&gt;        &lt;span&gt;for&lt;/span&gt; tool_call &lt;span&gt;in&lt;/span&gt; message.tool_calls:&lt;br&gt;            name = tool_call.function.name&lt;br&gt;            args = json.loads(tool_call.function.arguments)&lt;br&gt;            fn = TOOL_FUNCTIONS.get(name)&lt;br&gt;            result = fn(**args) &lt;span&gt;if&lt;/span&gt; fn &lt;span&gt;else&lt;/span&gt; &lt;span&gt;f"Unknown tool: &lt;span&gt;{name}&lt;/span&gt;"&lt;/span&gt;&lt;br&gt;            messages.append({&lt;br&gt;                &lt;span&gt;"role"&lt;/span&gt;: &lt;span&gt;"tool"&lt;/span&gt;,&lt;br&gt;                &lt;span&gt;"tool_call_id"&lt;/span&gt;: tool_call.&lt;span&gt;id&lt;/span&gt;,&lt;br&gt;                &lt;span&gt;"content"&lt;/span&gt;: result,&lt;br&gt;        })&lt;/span&gt;&lt;/pre&gt;
&lt;p id="63d5"&gt;That’s the whole architecture. Everything else LangGraph, CrewAI, AutoGen, the Claude Agents SDK is infrastructure layered on top of that pattern. Strip any of them down far enough and you’ll find a while loop, a messages list, and an &lt;code&gt;if not tool_calls&lt;/code&gt; check hiding somewhere inside.&lt;/p&gt;
&lt;p id="2412"&gt;The critical line is &lt;code&gt;if not message.tool_calls&lt;/code&gt;. If the model returns text without requesting any tools, it's signaling it has everything it needs to answer. The agent exits and returns that response. If the model requests tools, the agent executes them, appends the results to &lt;code&gt;messages&lt;/code&gt;, and sends the whole conversation back for another round. The &lt;code&gt;messages&lt;/code&gt; list is the agent's short-term memory every tool call and every result gets appended to it, so by the time the LLM decides it's done, it has seen everything it did and everything it learned from doing it.&lt;/p&gt;
&lt;p id="c7f2"&gt;To make this concrete, three simple tools: current date/time, a calculator, and a weather stub you’d replace with a real API call in production.&lt;/p&gt;
&lt;pre&gt;&lt;span id="fd37"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;get_current_date&lt;/span&gt;() -&amp;gt; &lt;span&gt;str&lt;/span&gt;:&lt;br&gt;    &lt;span&gt;return&lt;/span&gt; datetime.now().strftime(&lt;span&gt;"%Y-%m-%d %H:%M:%S"&lt;/span&gt;)&lt;br&gt;&lt;br&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;calculate&lt;/span&gt;(&lt;span&gt;expression: &lt;span&gt;str&lt;/span&gt;&lt;/span&gt;) -&amp;gt; &lt;span&gt;str&lt;/span&gt;:&lt;br&gt;    &lt;span&gt;try&lt;/span&gt;:&lt;br&gt;        result = &lt;span&gt;eval&lt;/span&gt;(expression, {&lt;span&gt;"&lt;strong&gt;builtins&lt;/strong&gt;"&lt;/span&gt;: {}}, {})&lt;br&gt;        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;str&lt;/span&gt;(result)&lt;br&gt;    &lt;span&gt;except&lt;/span&gt; Exception &lt;span&gt;as&lt;/span&gt; e:&lt;br&gt;        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;f"Error: &lt;span&gt;{e}&lt;/span&gt;"&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;get_weather&lt;/span&gt;(&lt;span&gt;city: &lt;span&gt;str&lt;/span&gt;&lt;/span&gt;) -&amp;gt; &lt;span&gt;str&lt;/span&gt;:&lt;br&gt;    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;f"Weather in &lt;span&gt;{city}&lt;/span&gt;: 72°F, partly cloudy"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="1aeb"&gt;Each tool also needs a JSON schema that tells the LLM what’s available, what arguments it takes, and what they mean. This is what the model actually reads when it decides which tool to call. Sloppy descriptions here = wrong tool calls later. The schema is documentation the model acts on, not just metadata you write once and forget.&lt;/p&gt;
&lt;p id="edf7"&gt;&lt;strong&gt;Run it against a task that needs all three tools at once:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="a3bc"&gt;Task: What's today's date? Also, what is 15% of 847? And what's the weather in Tokyo?&lt;br&gt;&lt;span&gt;&lt;br&gt;&amp;gt; &lt;/span&gt;calling get_current_date({})&lt;br&gt;&lt;span&gt;  &amp;gt; &lt;/span&gt;calling calculate({'expression': '847 * 0.15'})&lt;br&gt;&lt;span&gt;  &amp;gt; &lt;/span&gt;calling get_weather({'city': 'Tokyo'})&lt;br&gt;&lt;br&gt;Answer: Today is 2026-05-11 09:14:22. 15% of 847 is 127.05.&lt;br&gt;The weather in Tokyo is 72°F and partly cloudy.&lt;/span&gt;&lt;/pre&gt;
&lt;p id="d3bb"&gt;On the first turn the LLM identified all three tools, called them, got the results, and assembled the final answer. No orchestration layer. No dependency to install. The model just read the tool schemas, figured out it needed all three, executed them in parallel, and exited cleanly.&lt;/p&gt;
&lt;p id="4aac"&gt;What you’re looking at is a complete mental model for every production agent you’ll ever use. Claude Code starting a subagent is this loop calling another loop. Cursor retrying a failed file write is this loop with error handling bolted on. GitHub Copilot Workspace planning a multi-step refactor is this loop running longer with a more complex system prompt. The shape is always the same.&lt;/p&gt;
&lt;p id="929c"&gt;One thing worth calling out: I used OpenAI here because it has the cleanest tool-calling interface for a tutorial, but this works with any OpenAI-compatible API Anthropic, Gemini, and most local inference servers all support the same pattern. The agent code has no opinion about which model is on the other end, as long as it speaks the protocol.&lt;/p&gt;
&lt;p id="8814"&gt;The full working version with all tool schemas is in &lt;a href="https://github.com/sergenes/mini_agent" rel="noopener ugc nofollow noreferrer"&gt;sergenes/mini_agent&lt;/a&gt; on GitHub if you want to run it directly. Worth pulling down and stepping through with a debugger once watching the messages list grow in real time makes the memory model click in a way that reading about it doesn’t.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="307" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2A0wWKlHJtoefmlm67mMhKyg.jpeg"&gt;The ReAct loop the actual architecture underneath every agent you’ve used&lt;h3 id="04be"&gt;&lt;strong&gt;Points:&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;

&lt;li id="ea55"&gt;The agent is a while loop, a messages list, and one if-statement everything else is scaffolding&lt;/li&gt;

&lt;li id="1eeb"&gt;

&lt;code&gt;messages&lt;/code&gt; is the memory every tool result gets appended before the next LLM call&lt;/li&gt;

&lt;li id="1f4e"&gt;Tool schemas are documentation the model acts on write them carefully&lt;/li&gt;

&lt;li id="98fc"&gt;Every production agent you’ve used is this pattern with error handling, retry logic, and a longer system prompt&lt;/li&gt;

&lt;/ul&gt;
&lt;blockquote&gt;&lt;p id="05f2"&gt;“LangGraph, CrewAI, AutoGen strip any of them down far enough and you’ll find a while loop and an if not tool_calls check hiding inside.”&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="879b"&gt;&lt;strong&gt;The local model trap why Mistral 7B silently broke everything&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="bffe"&gt;Ollama exposes an OpenAI-compatible API, which means the agent code runs on a local model with exactly one change:&lt;/p&gt;
&lt;pre&gt;&lt;span id="0f46"&gt;ollama_client = OpenAI(&lt;br&gt;    base_url=&lt;span&gt;"&lt;a href="http://localhost:11434/v1" rel="noopener noreferrer"&gt;http://localhost:11434/v1&lt;/a&gt;"&lt;/span&gt;,&lt;br&gt;    api_key=&lt;span&gt;"ollama"&lt;/span&gt;,&lt;br&gt;)&lt;br&gt;&lt;br&gt;answer = run_agent(task, ollama_client, model=&lt;span&gt;"qwen2.5"&lt;/span&gt;)&lt;/span&gt;&lt;/pre&gt;
&lt;p id="76db"&gt;That’s it. The agent has no idea whether it’s talking to OpenAI’s servers or a model running on your laptop. Same loop, same tool schemas, same exit condition. To get Ollama running: install from &lt;a href="https://ollama.com" rel="noopener ugc nofollow noreferrer"&gt;ollama.com&lt;/a&gt;, then &lt;code&gt;ollama pull qwen2.5&lt;/code&gt; and &lt;code&gt;ollama serve&lt;/code&gt;. After that, the whole thing runs offline. No API costs, no data leaving your machine, no rate limits at two in the morning when you're debugging something you broke before bed.&lt;/p&gt;
&lt;p id="f451"&gt;Except here’s what actually happened when I tried it the first time&lt;strong&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p id="c71d"&gt;I pulled Mistral 7B. Widely recommended, solid benchmark numbers, comes up in every “best local models” thread on Hacker News. Ran the same three-tool task. No errors. Clean output. And then I read it:&lt;/p&gt;
&lt;pre&gt;&lt;span id="b7bf"&gt;Answer: I need to call get_current_date() to find today's date.&lt;br&gt;Let me use the calculate tool: calculate(expression="847 * 0.15")...&lt;br&gt;The weather in Tokyo is probably warm this time of year.&lt;/span&gt;&lt;/pre&gt;
&lt;p id="b2bc"&gt;&lt;strong&gt;Plain text&lt;/strong&gt;. Describing tool calls in prose. Guessing the weather. &lt;code&gt;response.tool_calls&lt;/code&gt; was empty on every turn, so the agent hit the &lt;code&gt;if not message.tool_calls&lt;/code&gt; check, found nothing, and exited immediately with whatever the model had written.&lt;/p&gt;
&lt;p id="6d35"&gt;My first instinct was that I’d broken the agent code. Spent a solid chunk of time staring at the while loop, checking the tool schemas, wondering if Ollama’s API response format was slightly different. It wasn’t. The code worked exactly as written. The problem was the model.&lt;/p&gt;
&lt;p id="eadd"&gt;Mistral 7B doesn’t support OpenAI-style structured function calling. It was trained to describe actions in prose, not emit them as structured JSON. When it saw the tool schemas in the request, it understood conceptually what tools were available so it started narrating what it &lt;em&gt;would&lt;/em&gt; call. But it never actually emitted the structured &lt;code&gt;tool_calls&lt;/code&gt; object the agent was waiting for. The model hallucinated the syntax it thought I expected, and the agent politely returned that hallucination as a final answer.&lt;/p&gt;
&lt;p id="cb49"&gt;This is the trap: the code gives you no error. No exception. No warning. The agent just exits on the first turn and you get back a paragraph of the model describing what it wishes it could do. If you’re not watching closely, you might not even notice the tools never fired.&lt;/p&gt;
&lt;p id="6b74"&gt;The fix is model selection. Not all local models support structured function calling through Ollama, and the ones that don’t fail silently in exactly this way. &lt;br&gt;&lt;strong&gt;The models that reliably work:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="0fe0"&gt;qwen2.5          ✓  Strong tool calling, good reasoning&lt;br&gt;llama3.1         ✓  Solid across most tool schemas&lt;br&gt;mistral-nemo     ✓  Works, occasionally verbose in reasoning&lt;br&gt;phi4             ✓  Surprisingly capable for its size&lt;br&gt;mistral 7B       ✗  Prose descriptions, no structured calls&lt;br&gt;gemma2           ✗  Inconsistent — sometimes works, often doesn't&lt;/span&gt;&lt;/pre&gt;
&lt;p id="c37d"&gt;If your agent returns immediately without calling any tools, suspect the model before the code. Swap to &lt;code&gt;qwen2.5&lt;/code&gt; and see if the behavior changes. Nine times out of ten that's it.&lt;/p&gt;
&lt;p id="562b"&gt;There’s a deeper lesson here about the gap between benchmark performance and production behavior. Mistral 7B scores well on reasoning benchmarks. It generates coherent, useful text. But “coherent text” and “emits structured JSON tool calls” are different capabilities, and the benchmarks that get cited in model announcements don’t always test the latter. Before you commit to a local model for anything agentic, run the simplest possible tool-calling task first. Date, calculator, weather stub. If those three work, you’re probably fine. If the model narrates them instead of calling them, move on.&lt;/p&gt;
&lt;p id="c2a6"&gt;The mixed-mode pattern handles this more elegantly run the loop locally, delegate to a cloud model when the task genuinely needs it but that’s the next section.&lt;/p&gt;
&lt;h3 id="1bcf"&gt;&lt;strong&gt;Points:&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;

&lt;li id="116c"&gt;Ollama’s OpenAI-compatible API means zero code changes to run locally one base_url swap&lt;/li&gt;

&lt;li id="6d20"&gt;Not all local models support structured function calling silent failure is the default behavior&lt;/li&gt;

&lt;li id="0abb"&gt;Mistral 7B describes tool calls in prose instead of emitting JSON; the agent exits cleanly on turn one&lt;/li&gt;

&lt;li id="5624"&gt;Test with a simple three-tool task before committing to any local model for agentic work&lt;/li&gt;

&lt;li id="85bb"&gt;

&lt;code&gt;qwen2.5&lt;/code&gt; and &lt;code&gt;llama3.1&lt;/code&gt; are the reliable starting points as of mid-2026&lt;/li&gt;

&lt;/ul&gt;
&lt;blockquote&gt;&lt;p id="0cd6"&gt;“The code gives you no error. No exception. No warning. The agent just exits and returns a paragraph of the model describing what it wishes it could do.”&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="ee90"&gt;&lt;strong&gt;MCP the protocol that finally fixes hardcoded tool hell&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="fb00"&gt;Here’s the problem nobody talks about when they show you the 50-line agent: every tool is hardcoded into the script. &lt;code&gt;get_current_date&lt;/code&gt;, &lt;code&gt;calculate&lt;/code&gt;, &lt;code&gt;get_weather&lt;/code&gt; all defined in the same file, all manually added to the &lt;code&gt;TOOLS&lt;/code&gt; list, all maintained by you. If you want another agent to use the same tools, you copy and paste. If you want to use tools someone else built, you rewrite their implementation into your format. If you update a tool, you update every script that hardcoded it.&lt;/p&gt;
&lt;p id="9b98"&gt;This is fine for a tutorial. It becomes genuinely painful around the time you have three agents across two projects and you’re maintaining six copies of the same file read utility with slightly different signatures.&lt;/p&gt;
&lt;p id="8c24"&gt;MCP Model Context Protocol is the standard Anthropic shipped in November 2024 specifically to fix this. The spec lives at &lt;a href="https://modelcontextprotocol.io" rel="noopener ugc nofollow noreferrer"&gt;modelcontextprotocol.io&lt;/a&gt; and the idea is straightforward: instead of hardcoding tool definitions into your agent, you point the agent at a server. The server advertises what tools it has. The agent discovers them, gets their schemas, and calls them exactly the same way it calls local functions. The agent doesn’t care whether a tool is a Python function in the same file or a service running on the other side of the internetas long as it speaks MCP, it works.&lt;/p&gt;
&lt;p id="40bc"&gt;Think USB-C. Before USB-C, every device had its own connector and you maintained a drawer full of incompatible cables. MCP is the USB-C for AI tools one protocol, anything plugs in. GitHub has an MCP server. Slack has one. Postgres has one. Google Drive has one. You point your agent at any of them and the tools just appear, already described, ready to use. Someone else writes the implementation once; everyone else benefits.&lt;/p&gt;
&lt;p id="a850"&gt;&lt;strong&gt;Building an MCP server is almost comically simple with FastMCP:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="0105"&gt;&lt;span&gt;from&lt;/span&gt; mcp.server.fastmcp &lt;span&gt;import&lt;/span&gt; FastMCP&lt;br&gt;&lt;br&gt;mcp = FastMCP(&lt;span&gt;"mini-tools"&lt;/span&gt;)&lt;br&gt;&lt;br&gt;&lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/mcp"&gt;@mcp&lt;/a&gt;.tool()&lt;/span&gt;&lt;br&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;to_uppercase&lt;/span&gt;(&lt;span&gt;text: &lt;span&gt;str&lt;/span&gt;&lt;/span&gt;) -&amp;gt; &lt;span&gt;str&lt;/span&gt;:&lt;br&gt;    &lt;span&gt;"""Convert text to uppercase."""&lt;/span&gt;&lt;br&gt;    &lt;span&gt;return&lt;/span&gt; text.upper()&lt;br&gt;&lt;br&gt;&lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/mcp"&gt;@mcp&lt;/a&gt;.tool()&lt;/span&gt;&lt;br&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;count_words&lt;/span&gt;(&lt;span&gt;text: &lt;span&gt;str&lt;/span&gt;&lt;/span&gt;) -&amp;gt; &lt;span&gt;int&lt;/span&gt;:&lt;br&gt;    &lt;span&gt;"""Count the number of words in a string."""&lt;/span&gt;&lt;br&gt;    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;len&lt;/span&gt;(text.split())&lt;br&gt;&lt;br&gt;&lt;span&gt;if&lt;/span&gt; &lt;strong&gt;name&lt;/strong&gt; == &lt;span&gt;"&lt;strong&gt;main&lt;/strong&gt;"&lt;/span&gt;:&lt;br&gt;    mcp.run()&lt;/span&gt;&lt;/pre&gt;
&lt;p id="c333"&gt;That’s a complete, working MCP server. Ten lines. The decorator handles schema generation from the type hints and docstring no manual JSON. Run it as a subprocess and any MCP-compatible client can discover those tools via a JSON-RPC handshake. Claude Desktop, Cursor, your DIY agent, anyone. Publish the server, point a config at it, and it just works.&lt;/p&gt;
&lt;p id="843d"&gt;From the agent’s side, the MCP client starts the server as a subprocess and calls tools via JSON-RPC. The agent receives the tool list exactly the same way it would receive hardcoded definitions they show up in &lt;code&gt;TOOLS&lt;/code&gt;, get passed to the LLM, get called the same way, return results the same way. The companion repo at &lt;a href="https://github.com/sergenes/mini_agent" rel="noopener ugc nofollow noreferrer"&gt;github.com/sergenes/mini_agent&lt;/a&gt; includes a working &lt;code&gt;mcp_client.py&lt;/code&gt; that demonstrates the full handshake if you want to see the plumbing.&lt;/p&gt;
&lt;p id="8ef5"&gt;The architectural shift here is bigger than it sounds. Before MCP, every team building agents was maintaining their own tool library, reinventing the same integrations “call GitHub API”, “query Postgres”, “read a Slack channel” across dozens of repos with incompatible interfaces. MCP turns those into shared infrastructure. One well-maintained server replaces hundreds of slightly-wrong copies.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="339" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2AXywguCqcwfW-VnpqnNPlFQ.jpeg"&gt;&lt;p id="4897"&gt;It also changes what “building an agent” means in practice. The interesting work stops being “how do I wire up this API” and starts being “which servers do I need and how do I write a system prompt that uses them well.” The loop stays exactly the same. What changes is that the tool ecosystem is now the internet instead of whatever you’ve personally had time to implement.&lt;/p&gt;
&lt;p id="5627"&gt;The &lt;a href="https://github.com/jlowin/fastmcp" rel="noopener ugc nofollow noreferrer"&gt;FastMCP library&lt;/a&gt; is worth bookmarking it’s the fastest way to expose your own tools to any agent in the ecosystem, and the decorator pattern makes the schema definition essentially free. Write the function, add the decorator, document the arguments, done.&lt;/p&gt;
&lt;p id="d82d"&gt;&lt;strong&gt;Points:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li id="c525"&gt;Hardcoded tools don’t scale you end up maintaining copies across every project and agent&lt;/li&gt;

&lt;li id="95ca"&gt;MCP is the standard protocol for tool discovery and calling agent points at server, server advertises tools, agent uses them&lt;/li&gt;

&lt;li id="0c9e"&gt;Building an MCP server with FastMCP is ten lines decorator handles schema generation from type hints&lt;/li&gt;

&lt;li id="dc68"&gt;The ecosystem already has MCP servers for GitHub, Slack, Postgres, Google Drive, and hundreds more&lt;/li&gt;

&lt;li id="91ae"&gt;MCP shifts the interesting work from “wiring APIs” to “writing system prompts that use shared tools well”&lt;/li&gt;

&lt;/ul&gt;
&lt;blockquote&gt;&lt;p id="384f"&gt;“Before MCP I was copy-pasting tool definitions across three different repos. Not great. Not sustainable. Definitely the kind of thing you notice at code review.”&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="cc76"&gt;&lt;strong&gt;Frameworks aren’t magic they’re load-bearing walls&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="e20a"&gt;Let’s be honest about what the 50-line agent is missing, because pretending it’s production-ready would be doing you a disservice.&lt;/p&gt;
&lt;p id="ff47"&gt;No error handling. If a tool throws an exception, the agent crashes. No retry logic a flaky API call takes down the whole run. No way to pause for human approval before the agent does something destructive like deleting files or sending emails. No memory beyond the current conversation restart the process and it has no idea what it did ten minutes ago. No ability to spawn parallel subagents when a task is large enough to benefit from splitting. No context compression when the messages list grows long enough to hit the model’s limit. No observability into what the agent decided at each step and why.&lt;/p&gt;
&lt;p id="8faa"&gt;That list is not a criticism of the 50-line version. It’s a description of what frameworks exist to solve.&lt;/p&gt;
&lt;p id="025e"&gt;LangGraph models the agent as a state machine with explicit nodes and edges. You define what happens at each step and what conditions trigger the next one. More setup upfront, but you get checkpointing the agent can pause mid-run and resume from exactly where it stopped. You get structured error handling at each node. You get human-in-the-loop steps where execution blocks until a human approves the next action. You get full observability into the graph traversal. If you’re building something where a failed tool call means a corrupted database or a mistaken API call means actual money spent, LangGraph is where you want to be. The &lt;a href="https://langchain-ai.github.io/langgraph" rel="noopener ugc nofollow noreferrer"&gt;docs&lt;/a&gt; are dense but the core concept clicks fast once you’ve built the naive loop yourself.&lt;/p&gt;
&lt;p id="4e72"&gt;CrewAI and AutoGen go a layer higher multi-agent coordination. Instead of one agent with many tools, you define multiple agents with specialized roles: a researcher, a writer, a critic. Each has its own system prompt, its own tool access, its own model if you want. The orchestrator decides who talks to whom and in what order. Useful for complex tasks where different phases genuinely need different prompts a research phase that needs web search and summarization, followed by a writing phase that needs a totally different voice, followed by a review phase that needs to be adversarial. Trying to do all of that with one system prompt and one tool set gets messy fast. &lt;a href="https://crewai.com" rel="noopener ugc nofollow noreferrer"&gt;CrewAI&lt;/a&gt; and &lt;a href="https://microsoft.github.io/autogen" rel="noopener ugc nofollow noreferrer"&gt;AutoGen&lt;/a&gt; are the frameworks worth reaching for when you hit that wall.&lt;/p&gt;
&lt;p id="2a4e"&gt;The managed runtimes Claude Agents SDK and OpenAI Assistants API trade control for speed. You hand off state management, tool routing, threading, and context compression to the platform. Less visibility into what’s happening, but dramatically less code to write and maintain. Worth it when you need to ship something in a week and the task doesn’t require custom orchestration logic. The Claude Agents SDK docs in particular are worth reading even if you don’t use the SDK the mental model they describe maps directly onto everything we’ve covered here.&lt;/p&gt;
&lt;p id="fcac"&gt;Claude Code is the honest comparison point for the DIY version. It does things the 50-line agent can’t touch: starts subagents with isolated context windows when a task is too large for one loop, prompts for confirmation before running destructive shell commands, maintains persistent memory across sessions, retries failed tool calls with adjusted parameters, compresses prior messages when approaching context limits. My agent has six tools, one messages list, and no safety net. Claude Code bills per message. My agent costs nothing until I tell it to hit GPT-4.&lt;/p&gt;
&lt;p id="fdae"&gt;If I need to ship something reliable and I don’t want to think about orchestration, I use Claude Code. If I’m prototyping something I’d spend a week fighting a framework to implement, I start from the loop. That tension is actually healthy. Knowing what frameworks abstract means you can make the decision deliberately instead of defaulting to LangGraph because it’s what the tutorial used.&lt;/p&gt;
&lt;p id="8be1"&gt;The 50-line version is a sketch. LangGraph is that sketch turned into a building with proper load-bearing walls. You need to understand the sketch before you can reason about the building. Now you do.&lt;/p&gt;
&lt;h3 id="9329"&gt;&lt;strong&gt;Points:&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;

&lt;li id="57de"&gt;The naive loop is missing error handling, retries, human-in-the-loop, persistent memory, parallel subagents, and context compression frameworks exist to solve exactly these problems&lt;/li&gt;

&lt;li id="9126"&gt;LangGraph: state machine model, checkpointing, structured error handling, best for production reliability&lt;/li&gt;

&lt;li id="d36b"&gt;CrewAI / AutoGen: multi-agent coordination, specialized roles, best when different phases need genuinely different prompts&lt;/li&gt;

&lt;li id="0b56"&gt;Managed runtimes (Claude Agents SDK, OpenAI Assistants): fastest to ship, least control, worth it for tight deadlines&lt;/li&gt;

&lt;li id="e6b0"&gt;Use frameworks when you’ve hit the problem they solve not before&lt;/li&gt;

&lt;/ul&gt;
&lt;blockquote&gt;&lt;p id="0799"&gt;“The 50-line loop is a sketch. LangGraph is that sketch turned into a building with proper load-bearing walls. Understand the sketch first.”&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="d27e"&gt;&lt;strong&gt;What building this actually taught me&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="65a7"&gt;I started this because I couldn’t answer a question in a code review. I ended up with something more useful than the answer a complete mental model I couldn’t get from using production tools, no matter how much I used them.&lt;/p&gt;
&lt;p id="4217"&gt;That’s the thing about abstractions. They’re designed to hide complexity, and they’re good at it. Cursor doesn’t show you the while loop. Claude Code doesn’t surface the messages list growing with each tool call. LangGraph definitely doesn’t make you think about the &lt;code&gt;if not tool_calls&lt;/code&gt; check at the center of everything. The abstraction works, which means you can ship fast without understanding the foundation. Until you can't. Until something breaks in a way the framework doesn't have a handler for, or you need behavior the abstraction actively fights against, or someone asks you in a review to explain what's actually happening under the hood.&lt;/p&gt;
&lt;p id="7651"&gt;Building the naive version closed that gap for me. I can see now exactly where an agent gets stuck when the system prompt is underspecified and the model doesn’t know when to stop. I can see why it picks one tool over another because the schema description was more precise. I can see when adding more tools actually makes things worse because the model starts hallucinating tool calls for tools that don’t quite fit the task, instead of admitting it can’t do it. That visibility is worth more than any framework feature.&lt;/p&gt;
&lt;p id="5a90"&gt;Some of my projects will use LangGraph or the Claude Agents SDK going forward. Those frameworks solve real problems I genuinely don’t want to reimplement retry logic, checkpointing, human-in-the-loop approvals. But some will start from this 50-line loop, because I know exactly what it does and I can modify it without fighting abstractions I don’t fully understand. That optionality is the real output of the exercise.&lt;/p&gt;
&lt;p id="7ab4"&gt;Here’s the slightly uncomfortable opinion to end on: too many developers are reaching for agent frameworks before they understand what’s being abstracted. The ecosystem moved fast LangChain appeared, then LangGraph, then CrewAI, then a dozen managed runtimes, all within about eighteen months. The tutorials went straight to the framework. A lot of people never saw the loop. That’s going to become a problem as agents get more embedded in production systems and the failure modes get more consequential. You can’t debug what you don’t understand, and “the framework handled it” stops being a satisfying answer when the thing the framework handled was your customer’s data.&lt;/p&gt;
&lt;p id="b663"&gt;Build the naive version first. Then decide what infrastructure you actually need. The loop is twenty minutes of work. The clarity it gives you is permanent.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="6ccb"&gt;What did you build to understand something you were already using? Drop it in the comments I read every one.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="efcf"&gt;&lt;strong&gt;Helpful resources&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;

&lt;li id="4462"&gt;&lt;a href="https://arxiv.org/abs/2210.03629" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;ReAct: Synergizing Reasoning and Acting in Language Models&lt;/strong&gt; Yao et al., 2022&lt;/a&gt;&lt;/li&gt;

&lt;li id="15c8"&gt;&lt;a href="https://github.com/sergenes/mini_agent" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;mini_agent companion repo&lt;/strong&gt; full working code&lt;/a&gt;&lt;/li&gt;

&lt;li id="b10c"&gt;&lt;a href="https://platform.openai.com/docs/guides/function-calling" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;OpenAI function calling docs&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;

&lt;li id="fe7e"&gt;&lt;a href="https://github.com/ollama/ollama/blob/main/docs/api.md" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Ollama API docs&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;

&lt;li id="fd91"&gt;&lt;a href="https://modelcontextprotocol.io" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Model Context Protocol&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;

&lt;li id="2447"&gt;&lt;a href="https://github.com/jlowin/fastmcp" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;FastMCP&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;

&lt;li id="4182"&gt;&lt;a href="https://langchain-ai.github.io/langgraph" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;LangGraph&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>GitHub is dead to the Netherlands and they built something better</title>
      <dc:creator>&lt;devtips/&gt;</dc:creator>
      <pubDate>Sun, 10 May 2026 07:31:28 +0000</pubDate>
      <link>https://dev.to/dev_tips/github-is-dead-to-the-netherlands-and-they-built-something-better-438k</link>
      <guid>https://dev.to/dev_tips/github-is-dead-to-the-netherlands-and-they-built-something-better-438k</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="733c"&gt;The Dutch government quietly replaced GitHub with a self-hosted FOSS forge. No press release. Just commits.&lt;/h2&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="446" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2AndOthOfiOT3xrFiH_vhOBA.jpeg"&gt;&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p id="1698"&gt;Most developers have had the self-hosting conversation at least once. Someone on the team floats it, everyone nods, someone opens a Hetzner tab, and then a Jira ticket appears and the whole thing dies quietly on a Friday afternoon. We talk about owning our stack the way people talk about going to the gym constantly, convincingly, and without follow-through.&lt;/p&gt;
&lt;p id="2be8"&gt;The Netherlands apparently skipped that part.&lt;/p&gt;
&lt;p id="ff0b"&gt;In November 2025, a software engineer named Jan Vlug published a detailed evaluation on the Dutch government’s developer portal asking one very reasonable question: which Git forge should the Netherlands actually use for its governmental source code? Not which one is most popular. Not which one has the slickest UI. Which one is &lt;em&gt;appropriate&lt;/em&gt; for a government that has an actual policy about open source software.&lt;/p&gt;
&lt;p id="256a"&gt;That blog post wasn’t a thought experiment. The Ministry of the Interior was already spinning up a dedicated Git instance. The platform decision was live and open. And the Dutch government’s code at that point was scattered across GitHub and GitLab neither of which is under any form of government oversight or control.&lt;/p&gt;
&lt;p id="3a8a"&gt;So they actually did it. On April 24, 2026, &lt;a href="https://code.overheid.nl" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;code.overheid.nl&lt;/strong&gt;&lt;/a&gt; went live quietly, without fanfare, in pilot phase as a fully self-hosted, FOSS-only government code forge. No press release. No launch party. Just a blog post from developer advocate Tom Ootes saying, essentially,&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="5a03"&gt;“we’re building this together, come help.”&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p id="650f"&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; The Dutch government evaluated GitHub, evaluated GitLab, rejected both on legitimate technical and policy grounds, and shipped a self-hosted &lt;a href="https://forgejo.org" rel="noopener ugc nofollow noreferrer"&gt;Forgejo&lt;/a&gt; instance with actual ministries and municipalities already committing code to it. This is what digital sovereignty looks like when someone actually does the work instead of just writing about it.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="6c89"&gt;Why GitHub got killed first&lt;/h2&gt;
&lt;p id="244b"&gt;Let’s be honest GitHub losing a government contract isn’t exactly a scandal. It’s more like getting eliminated from a cooking show because you showed up with a microwave. The outcome was obvious the moment someone read the rules.&lt;/p&gt;
&lt;p id="d050"&gt;The Dutch government operates under a policy that says: prefer open source software when options are equally suitable. That’s not a vague suggestion buried in a footnote. It’s the kind of thing that shows up in procurement checklists and makes lawyers nervous when ignored. GitHub is proprietary software. Full stop. It didn’t matter how good the UI is, how many integrations exist, or how many developers already live there. Proprietary meant disqualified, and that was the end of that conversation.&lt;/p&gt;
&lt;p id="0b75"&gt;What’s interesting though isn’t that GitHub lost it’s &lt;em&gt;why&lt;/em&gt; that reasoning is actually correct. When your government’s source code lives on someone else’s SaaS platform, you don’t control it. You control a subscription. There’s a difference between storing your nation’s infrastructure code on a platform and &lt;em&gt;owning&lt;/em&gt; where that code lives. One of those is a business arrangement. The other is sovereignty.&lt;/p&gt;
&lt;p id="012b"&gt;The Microsoft acquisition in 2018 didn’t help GitHub’s case either. That’s not FUD it’s a real procurement consideration for any government evaluating long-term vendor risk. When a $26 billion acquisition happens, the platform’s priorities shift. Governments move slowly enough that they’re still feeling that shift when everyone else has moved on.&lt;/p&gt;
&lt;p id="721b"&gt;GitLab made it further, which is fair. GitLab’s Community Edition is genuinely solid software it’s what a lot of self-hosting teams reach for when they want the GitHub experience without the GitHub dependency. But GitLab runs an &lt;a href="https://en.wikipedia.org/wiki/Open-core_model" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;open-core model&lt;/strong&gt;&lt;/a&gt;, and that model has a very specific problem: the free version is real, but the version with everything your team eventually needs is not.&lt;/p&gt;
&lt;p id="800a"&gt;Every team I’ve been on has hit this wall. You start on GitLab CE, everything’s fine, and then six months later someone needs a feature advanced CI analytics, security dashboards, dependency scanning at scale and it’s sitting right there in the UI with a little lock icon on it. The open-core model is free-to-play with a battle pass. The base game works. The content you actually want is in the enterprise tier.&lt;/p&gt;
&lt;p id="f134"&gt;For a government platform that needs to stay fully open and auditable indefinitely, “free until it isn’t” isn’t good enough. GitLab CE is free software. GitLab EE is not. That split was enough to end the evaluation.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="3e34"&gt;Enter Forgejo: the underdog with a squirrel&lt;/h2&gt;
&lt;p id="0062"&gt;If you haven’t heard of Forgejo, you’re not alone. It doesn’t have a $7 billion valuation, a Super Bowl ad, or a developer relations team sending you swag. What it has is a GPLv3+ license, a democratic nonprofit governing it, and a squirrel mascot that somehow makes the whole thing feel more trustworthy than it has any right to.&lt;/p&gt;
&lt;p id="9621"&gt;Forgejo is a fork of &lt;a href="https://gitea.com" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Gitea&lt;/strong&gt;&lt;/a&gt;, which itself was a fork of Gogs, which means it has the kind of lineage that open source projects accumulate when the community keeps deciding the current maintainers aren’t moving fast enough. The fork happened in 2022 when a subset of the Gitea community felt the project was drifting toward commercial interests. They took the code, stood up &lt;a href="https://codeberg.org" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Codeberg e.V.&lt;/strong&gt;&lt;/a&gt; as a governing body, and built something with an explicit commitment to staying fully free no enterprise edition, no proprietary upsell, no acquisition exit ramp.&lt;/p&gt;
&lt;p id="d5e3"&gt;Codeberg e.V. is a registered democratic nonprofit based in Germany. That structure matters more than it sounds. There’s no board of investors with a liquidation preference waiting for a strategic exit. No sales team trying to land an enterprise deal that quietly shapes the product roadmap. The governance model is the moat, and for a government platform that needs to be auditable and independent indefinitely, that moat is exactly what the evaluation was looking for.&lt;/p&gt;
&lt;p id="d25e"&gt;Feature-wise, Forgejo covers the 90% of what most teams actually use on GitHub. Pull requests, issue tracking, code review, CI integration hooks, webhooks, org management it’s all there. It’s not trying to be GitHub. It’s trying to be a solid, self-hostable Git forge that doesn’t ask you to trust a vendor with your data, your uptime, or your roadmap.&lt;/p&gt;
&lt;p id="da10"&gt;The analogy that keeps coming to mind is Nextcloud. Nextcloud isn’t sexier than Dropbox. The setup takes longer, the UI isn’t as polished, and you have to think about your own backups. But it’s &lt;em&gt;yours&lt;/em&gt;. When Dropbox changes its pricing, raises its limits, or gets acquired, your data isn’t caught in the crossfire. Forgejo is that, but for code hosting. Less glamorous than the SaaS alternative. Infinitely more yours.&lt;/p&gt;
&lt;p id="a276"&gt;Jan Vlug’s &lt;a href="https://developer.overheid.nl/blog/2025/11/11/git-forge-overheid" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;evaluation post&lt;/strong&gt;&lt;/a&gt; laid all of this out methodically license model, governance structure, hosting requirements, community health. It wasn’t a hot take. It was the kind of calm, thorough technical writeup that procurement decisions should be based on and almost never are. Forgejo cleared every bar. So Forgejo won.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="369" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2ApeKV-jWmovyl6PlkHv2k5g.jpeg"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="c656"&gt;What’s already live and why it matters more than you think&lt;/h2&gt;
&lt;p id="0189"&gt;Here’s where it stops being a policy story and starts being an engineering story.&lt;/p&gt;
&lt;p id="eb4c"&gt;&lt;a href="https://code.overheid.nl" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;code.overheid.nl&lt;/strong&gt;&lt;/a&gt; had its soft launch on April 24, 2026. Developer advocate Tom Ootes wrote about it on the government’s developer portal, framing it not as a finished product being handed down but as a collective build early adopters were explicitly encouraged to file issues and open pull requests on the platform itself. Eat your own dog food, government edition.&lt;/p&gt;
&lt;p id="9e5c"&gt;The platform is a self-hosted Forgejo instance running on Dutch government infrastructure managed by SSC-ICT, the government’s shared IT services organization. It’s free for all government organizations to join. No licensing fees, no per-seat pricing, no surprise invoice when you add your fifteenth ministry.&lt;/p&gt;
&lt;p id="f2f3"&gt;Now here’s the part that made me stop scrolling.&lt;/p&gt;
&lt;p id="6b2b"&gt;The most prominent presence on the platform right now is &lt;a href="https://english.kiesraad.nl" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Kiesraad&lt;/strong&gt;&lt;/a&gt; the Dutch Electoral Council. They’ve pushed several repositories including &lt;a href="https://code.overheid.nl/Kiesraad/Abacus" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Abacus&lt;/strong&gt;&lt;/a&gt;, the software used for vote counting and seat distribution in Dutch elections, and e-KS, an electronic candidate nomination system.&lt;/p&gt;
&lt;p id="24e0"&gt;Let that sit for a second. The software that counts votes in a national election is publicly hosted, publicly auditable, open source, and sitting on a government-controlled server. No black box. No “trust us.” Anyone can read the code, file an issue, or submit a pull request. That’s not just good open source practice that’s a fundamentally different relationship between a government and the people it serves.&lt;/p&gt;
&lt;p id="f529"&gt;Think about how many countries have their election-adjacent infrastructure running on proprietary systems or third-party SaaS platforms where independent audit is somewhere between difficult and impossible. The bar for public trust in election software is already low. Making it open and auditable doesn’t fix every problem, but it’s a meaningful step that most governments haven’t taken.&lt;/p&gt;
&lt;p id="0514"&gt;The Ministry of the Interior has the DAWO project on there their digital autonomous workplace initiative along with a DigiD source code release that was published under a freedom of information ruling. That last one is particularly interesting. The code went public not because the government volunteered it, but because a legal ruling required it. And now it lives on infrastructure the government actually controls.&lt;/p&gt;
&lt;p id="6e03"&gt;On the organization side, the roster for a platform still in pilot is genuinely surprising. National ministries already signed up include Finance, Foreign Affairs, Agriculture, and Interior. Municipalities include The Hague, Utrecht, Leiden, and Arnhem. That’s not a proof of concept anymore. That’s adoption.&lt;/p&gt;
&lt;p id="84cd"&gt;Tom Ootes framed the whole thing as building alongside the people who will use it rather than shipping something finished. That framing is quietly smart. Government software projects fail constantly because they get designed in isolation and handed to users who had no input. Starting in public, in pilot, with issues open that’s the right instinct even if it’s a slower path.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="486c"&gt;Europe is waking up slowly, quietly, correctly&lt;/h2&gt;
&lt;p id="ed4e"&gt;The Netherlands didn’t invent this idea. They just executed it better than most.&lt;/p&gt;
&lt;p id="9db3"&gt;France has been running &lt;a href="https://code.gouv.fr" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;code.gouv.fr&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;for a while now. It’s a public inventory of open source software published by French public agencies. Germany has &lt;a href="https://opencode.de" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;opencode.de&lt;/strong&gt;&lt;/a&gt;, a similar initiative pushing government code into the open on self-hosted infrastructure. The pattern is consistent across every country doing this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="3a4e"&gt;US cloud platform → data sovereignty concern → FOSS forge.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="914f"&gt;It’s not a coincidence. It’s GDPR doing its job.&lt;/p&gt;
&lt;p id="6fa4"&gt;When your government code lives on servers in Virginia, you have a problem. Not a theoretical one. A legal one. GDPR has real teeth, and “we use GitHub” is not a compliance strategy when the data in question belongs to citizens of a member state. The move toward self-hosted, EU-controlled infrastructure isn’t ideological it’s a legal and operational requirement that some governments are finally taking seriously.&lt;/p&gt;
&lt;p id="e238"&gt;Here’s the thing though. Nobody’s making noise about this.&lt;/p&gt;
&lt;p id="bb2e"&gt;If a VC-backed startup pulled off what the Netherlands just did evaluated the market, rejected the dominant players on principled technical grounds, shipped a self-hosted alternative with real adoption in under six months it would be a Product Hunt #1, a TechCrunch piece, and seventeen LinkedIn posts about “disruption.”&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="35c8"&gt;Instead it’s a quiet blog post from a developer advocate and a Forgejo instance that most devs outside the Netherlands have never heard of.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="15e9"&gt;That asymmetry says something about where the dev community puts its attention. We celebrate funded startups shipping fast. We ignore governments shipping slowly and correctly. But slow and correct compounds. The Netherlands isn’t going to wake up one day and discover their code platform got acquired, deprecated, or pivoted into an enterprise product. They own it.&lt;/p&gt;
&lt;p id="5a49"&gt;The &lt;em&gt;“Europe buying a NAS instead of paying for iCloud”&lt;/em&gt; energy is real here. Yes, the NAS takes longer to set up. Yes, you have to think about your own redundancy. But when Apple changes its storage tiers, your files aren’t hostage to a pricing decision made in Cupertino.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="e8f8"&gt;Same logic. Different scale. Higher stakes.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="6677"&gt;GitHub won the developer market on network effects, not on being the objectively correct choice for every use case. For personal projects, open source collaboration, and public repos GitHub is great. For government infrastructure that needs to stay auditable, sovereign, and free from vendor risk indefinitely? The Netherlands just proved there’s a better answer.&lt;/p&gt;
&lt;p id="1363"&gt;And it has a squirrel mascot.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2AM85z_p7KNF1ZLlKMrP4R4g.jpeg"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="089e"&gt;The quiet ones ship&lt;/h2&gt;
&lt;p id="c05b"&gt;There’s a version of this story where the Netherlands holds a press conference, brings in a minister to cut a ribbon on a server rack, and publishes a forty-page digital sovereignty strategy document that nobody reads.&lt;/p&gt;
&lt;p id="d355"&gt;They didn’t do that.&lt;/p&gt;
&lt;p id="6e10"&gt;They hired an engineer to write an honest evaluation. They picked the right tool. They stood up the infrastructure. They opened it in pilot with real organizations committing real code. And then they wrote a blog post about it.&lt;/p&gt;
&lt;p id="61dd"&gt;That’s it. That’s the whole move.&lt;/p&gt;
&lt;p id="1c34"&gt;There’s something genuinely refreshing about watching an institution do the unglamorous work without performing it. No roadmap deck. No keynote. No waitlist with a referral bonus. Just a Forgejo instance, a squirrel mascot, and government ministries quietly pushing commits.&lt;/p&gt;
&lt;p id="ff63"&gt;The part that sticks with me is Abacus. Election software. Publicly hosted. Openly auditable. In a world where public trust in institutions is running low and the black-box nature of critical infrastructure is a legitimate concern making your vote-counting code readable by anyone is a statement. Not a loud one. But a real one.&lt;/p&gt;
&lt;p id="ac28"&gt;GitHub isn’t going anywhere. It’s still where most of the world’s open source code lives and where most developers spend their days. But the assumption that GitHub is the default, obvious, unquestioned choice for &lt;em&gt;every&lt;/em&gt; organization is starting to crack. Slowly. One government forge at a time.&lt;/p&gt;
&lt;p id="e41e"&gt;The Netherlands killed that assumption for themselves. France and Germany are doing the same. More will follow not because FOSS forges are trendy, but because the alternative is permanent dependency on platforms you don’t control, running on infrastructure outside your jurisdiction, governed by priorities that aren’t yours.&lt;/p&gt;
&lt;p id="5edb"&gt;If you’ve been meaning to look into self-hosting your team’s code, this is a decent nudge. And if you want to see what a government actually shipping digital sovereignty looks like, &lt;a href="https://code.overheid.nl" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;code.overheid.nl&lt;/strong&gt;&lt;/a&gt; is live right now.&lt;/p&gt;
&lt;p id="974c"&gt;Go look at it. The squirrel is worth it.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="be3b"&gt;&lt;strong&gt;Are you self-hosting your team’s Git infrastructure? Is your organization still on GitHub by default or by choice? Drop your take in the comments.&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="97b3"&gt;Helpful resources&lt;/h2&gt;
&lt;ul&gt;

&lt;li id="4c57"&gt;

&lt;a href="https://code.overheid.nl" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;code.overheid.nl&lt;/strong&gt;&lt;/a&gt; the live platform&lt;/li&gt;

&lt;li id="04b1"&gt;

&lt;a href="https://forgejo.org" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Forgejo official site&lt;/strong&gt;&lt;/a&gt; what it is and how to self-host&lt;/li&gt;

&lt;li id="764b"&gt;

&lt;a href="https://codeberg.org" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Codeberg e.V.&lt;/strong&gt;&lt;/a&gt; the nonprofit governing Forgejo&lt;/li&gt;

&lt;li id="1a46"&gt;

&lt;a href="https://developer.overheid.nl/blog/2025/11/11/git-forge-overheid" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Jan Vlug’s Git forge evaluation&lt;/strong&gt;&lt;/a&gt; the blog post that started it&lt;/li&gt;

&lt;li id="53b0"&gt;

&lt;a href="https://developer.overheid.nl/blog/2026/04/24/we-gaan-samen-code-overheid-bouwen" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Tom Ootes soft launch post&lt;/strong&gt;&lt;/a&gt; the April 24 announcement&lt;/li&gt;

&lt;li id="d706"&gt;

&lt;a href="https://code.gouv.fr" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;code.gouv.fr&lt;/strong&gt;&lt;/a&gt; France’s government FOSS platform&lt;/li&gt;

&lt;li id="1eb2"&gt;

&lt;a href="https://opencode.de" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;opencode.de&lt;/strong&gt;&lt;/a&gt; Germany’s equivalent&lt;/li&gt;

&lt;li id="229a"&gt;

&lt;a href="https://forgejo.org/faq/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Forgejo FAQ and governance&lt;/strong&gt;&lt;/a&gt; how the project is actually run&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>programming</category>
      <category>github</category>
    </item>
    <item>
      <title>30+ Linux environment variables hackers memorize on day one (and most devs never bother with</title>
      <dc:creator>&lt;devtips/&gt;</dc:creator>
      <pubDate>Thu, 07 May 2026 14:08:40 +0000</pubDate>
      <link>https://dev.to/dev_tips/30-linux-environment-variables-hackers-memorize-on-day-one-and-most-devs-never-bother-with-ak</link>
      <guid>https://dev.to/dev_tips/30-linux-environment-variables-hackers-memorize-on-day-one-and-most-devs-never-bother-with-ak</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="8aaa"&gt;
&lt;br&gt;
&lt;em&gt;Alt:&lt;/em&gt; “Your Linux setup is leaking. These 30+ environment variables are why.”&lt;/h2&gt;
&lt;span&gt;&lt;/span&gt;&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p id="2fd7"&gt;You’ve been writing code on Linux for years. Maybe you run it in Docker, on a VPS, on your actual machine like a person with good taste. You know your way around a terminal. You feel comfortable there.&lt;/p&gt;
&lt;p id="0d2e"&gt;And yet.&lt;/p&gt;
&lt;p id="60fb"&gt;Somewhere between your first &lt;code&gt;apt install&lt;/code&gt; and your current career, you quietly decided that environment variables were a "good to know someday" topic. You learned PATH. You learned HOME. You maybe, on a heroic day, looked up what SHELL does. Then you moved on.&lt;/p&gt;
&lt;p id="f6e2"&gt;That’s fine. Until you’re debugging a production issue at an hour you’d rather not name, and the answer is buried in a variable you’ve never heard of. Or worse until someone who has done their homework walks into a system and does things you can’t explain because you never learned the control plane sitting right under your nose.&lt;/p&gt;
&lt;p id="f0c8"&gt;Here’s the uncomfortable truth: environment variables aren’t a Linux curiosity. They’re the configuration layer for every process, every shell session, every tool you run. Hackers the ethical, CTF-grinding, red-team-report-writing kind treat them like first-class knowledge. Most developers treat them like a footnote.&lt;/p&gt;
&lt;p id="4d3e"&gt;This article fixes that. We’re going through 30+ of them across three tiers: the essentials you should already know cold, the power-user variables most people skip entirely, and the ones that show up in every serious security engagement. By the end, you’ll have a mental model for this stuff that actually sticks.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="618a"&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; There are dozens of Linux environment variables that shape how your system behaves. Most developers know three. This guide covers 30+, split into essentials, power-user config, and security-critical variables with real examples and the context to actually use them.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="4823"&gt;Tier 1: The essentials the 10 you should know cold&lt;/h2&gt;
&lt;p id="d24f"&gt;Most developers exist in a comfortable relationship with about three environment variables. PATH gets them where they need to go. HOME tells their tools where to store things. USER shows up occasionally in a script. That’s the whole map.&lt;/p&gt;
&lt;p id="cf5e"&gt;The problem is that “knowing” PATH and actually understanding what it does are two very different things. And when something breaks a command not found, a tool opening the wrong editor, logs full of encoding garbage the answer is almost always in one of these ten variables. You just didn’t know to look there.&lt;/p&gt;
&lt;p id="83c6"&gt;Let’s close that gap.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="0cba"&gt;&lt;strong&gt;PATH&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="25fb"&gt;The most consequential variable on your system. When you type any command, Linux doesn’t search every directory it walks through PATH left to right and stops at the first match.&lt;/p&gt;
&lt;pre&gt;&lt;span id="ddb9"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$PATH&lt;/span&gt;&lt;br&gt;&lt;span&gt;# /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;export&lt;/span&gt; PATH=/usr/local/bin:&lt;span&gt;$PATH&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="628e"&gt;Order matters more than most people realize. If you’ve ever installed a tool and gotten an older version back, PATH ordering is your suspect. The directory that appears first wins, every time.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="3505"&gt;&lt;strong&gt;HOME&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="2549"&gt;The absolute path to your current user’s home directory. Nearly every application uses this to figure out where to read config, write cache, and store data.&lt;/p&gt;
&lt;pre&gt;&lt;span id="c312"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$HOME&lt;/span&gt;&lt;br&gt;&lt;span&gt;# /home/devtips&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="d76b"&gt;Change HOME and you change where everything lands. Useful to know when you’re scripting something that needs to behave differently across users.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="446" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2AV_ZBMy37qOd1F1lvIWie0w.png"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="b376"&gt;&lt;strong&gt;USER&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="a0f4"&gt;The username of whoever is running the current session. Simple on the surface, essential in any script that needs to branch based on who’s executing it.&lt;/p&gt;
&lt;pre&gt;&lt;span id="2e09"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$USER&lt;/span&gt;&lt;br&gt;&lt;span&gt;# devtips&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="f4b6"&gt;Don’t hardcode usernames in scripts. Read USER instead. Your future self will thank you when someone else runs it.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="810e"&gt;&lt;strong&gt;SHELL&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="b664"&gt;The path to your active shell binary. This determines your scripting behavior, your tab completion, and your default prompt.&lt;/p&gt;
&lt;pre&gt;&lt;span id="cb56"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$SHELL&lt;/span&gt;&lt;br&gt;&lt;span&gt;# /bin/bash&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="31f5"&gt;Never assume bash. In a lot of modern setups especially containers and minimal server installs you’re on dash, sh, or zsh. SHELL tells you what you’re actually dealing with.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="37bb"&gt;&lt;strong&gt;PWD&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="3f58"&gt;Your current working directory, updated automatically every time you &lt;code&gt;cd&lt;/code&gt;. More useful in scripts than in interactive use.&lt;/p&gt;
&lt;pre&gt;&lt;span id="5be5"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$PWD&lt;/span&gt;&lt;br&gt;&lt;span&gt;# /home/devtips/projects&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="1207"&gt;Use it to build absolute paths inside scripts instead of relying on relative paths that break the moment someone runs the script from a different directory.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="60ba"&gt;&lt;strong&gt;HOSTNAME&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="2008"&gt;The network name of the machine you’re on. The one you actually want in scripts that need to behave differently across dev, staging, and prod.&lt;/p&gt;
&lt;pre&gt;&lt;span id="902d"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$HOSTNAME&lt;/span&gt;&lt;br&gt;&lt;span&gt;# ubuntu-server&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="1220"&gt;I’ve seen scripts that hardcode machine names. I’ve seen what happens when those machines get renamed. Set HOSTNAME checks in your scripts and stop praying.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="8d46"&gt;&lt;strong&gt;LANG&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="b10e"&gt;Controls language, character encoding, and locale behavior across the entire system.&lt;/p&gt;
&lt;pre&gt;&lt;span id="a9a0"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$LANG&lt;/span&gt;&lt;br&gt;&lt;span&gt;# en_US.UTF-8&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;export&lt;/span&gt; LANG=en_US.UTF-8&lt;/span&gt;&lt;/pre&gt;
&lt;p id="e36a"&gt;Mismatched locales are responsible for some of the most baffling, hard-to-reproduce bugs in production. Logs showing up as question marks. Sorting behaving wrong. String comparisons failing in ways that make no sense. Always set this explicitly in production scripts don’t inherit whatever the system happens to have.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="75c0"&gt;&lt;strong&gt;TERM&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="9055"&gt;Tells applications what kind of terminal they’re dealing with, which determines what escape codes they use for colors, cursor movement, and formatting.&lt;/p&gt;
&lt;pre&gt;&lt;span id="4763"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$TERM&lt;/span&gt;&lt;br&gt;&lt;span&gt;# xterm-256color&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="feda"&gt;If your terminal output looks garbled, colors aren’t rendering, or a tool is behaving like it’s running blind TERM is your first check. A lot of SSH sessions inherit a wrong TERM value and everything downstream breaks quietly.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="1248"&gt;&lt;strong&gt;EDITOR&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="8be7"&gt;Defines which text editor programs open when they need you to write something git commit messages, crontab entries, anything that spawns an interactive editor.&lt;/p&gt;
&lt;pre&gt;&lt;span id="2a09"&gt;&lt;span&gt;export&lt;/span&gt; EDITOR=vim&lt;/span&gt;&lt;/pre&gt;
&lt;p id="949c"&gt;Set this once in your &lt;code&gt;~/.bashrc&lt;/code&gt; and every tool that respects it falls in line. Not setting it means you're at the mercy of whatever the system default is. On a lot of servers, that's ed. You do not want to meet ed unprepared.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="fab0"&gt;&lt;strong&gt;TZ&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="d3d1"&gt;Sets the timezone for the current session and any process that inherits the environment.&lt;/p&gt;
&lt;pre&gt;&lt;span id="d2c1"&gt;&lt;span&gt;export&lt;/span&gt; TZ=America/New_York&lt;br&gt;&lt;span&gt;# or&lt;/span&gt;&lt;br&gt;&lt;span&gt;export&lt;/span&gt; TZ=Asia/Karachi&lt;/span&gt;&lt;/pre&gt;
&lt;p id="8e08"&gt;This one bites teams constantly. Two servers, same codebase, logs showing timestamps an hour apart someone forgot to standardize TZ across environments. In containerized systems especially, never assume the timezone is what you think it is. Set it explicitly and move on.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="d617"&gt;Those are your ten. If you can explain what each one does without looking it up, you’re already ahead of most people running Linux daily. If two or three were new good, now they’re not.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="4a93"&gt;Tier 2: Power user variables most devs sleep on&lt;/h2&gt;
&lt;p id="5543"&gt;Here’s where the gap actually opens up. Tier 1 variables are the ones you stumble into eventually they show up in error messages, Stack Overflow answers, and “getting started with Linux” guides. You learn them by accident.&lt;/p&gt;
&lt;p id="b2ab"&gt;Tier 2 doesn’t work that way. Nobody’s going to hand you these. They live in the corners of documentation pages, in experienced engineers’ dotfiles, in the kind of config that makes you think “wait, you can just &lt;em&gt;do&lt;/em&gt; that?” the first time you see it. Most developers go years without touching them. Power users configure them on day one.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="b724"&gt;&lt;strong&gt;PS1&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="4c77"&gt;Your bash prompt is programmable. PS1 is the variable that controls exactly what it displays.&lt;/p&gt;
&lt;pre&gt;&lt;span id="04b6"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$PS1&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;export&lt;/span&gt; PS1=&lt;span&gt;"[\u@\h \W]\$ "&lt;/span&gt;&lt;br&gt;&lt;span&gt;# Output: [devtips@ubuntu projects]$&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="5a3e"&gt;&lt;code&gt;\u&lt;/code&gt; is your username, &lt;code&gt;\h&lt;/code&gt; is hostname, &lt;code&gt;\W&lt;/code&gt; is the current directory. You can layer in colors, git branch names, exit codes from the last command, timestamps basically anything. Think of the default prompt like a stock game HUD. PS1 is where you remap everything to actually make sense for how you work. If you're still on the default, you're leaving information on the table every time you open a terminal.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;p id="6c55"&gt;&lt;strong&gt;HISTSIZE&lt;/strong&gt;&lt;/p&gt;
&lt;p id="0eed"&gt;Controls how many commands are kept in memory during your active session.&lt;/p&gt;
&lt;p id="1327"&gt;bash&lt;/p&gt;
&lt;pre&gt;&lt;span id="b936"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$HISTSIZE&lt;/span&gt;&lt;br&gt;&lt;span&gt;# 1000&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;export&lt;/span&gt; HISTSIZE=10000&lt;/span&gt;&lt;/pre&gt;
&lt;p id="1abc"&gt;The default on most systems is 1000. That sounds like a lot until you’re trying to find a command you ran three days ago and it’s gone. Power users set this to something large ten thousand, fifty thousand and treat their history like a searchable log of everything they’ve done. Pair it with &lt;code&gt;Ctrl+R&lt;/code&gt; for reverse history search and you've got a lightweight audit trail of your own work.&lt;/p&gt;
&lt;p id="ed00"&gt;We’ll revisit HISTSIZE in Tier 3 for very different reasons.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="15f1"&gt;&lt;strong&gt;HISTFILESIZE&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="78b9"&gt;The on-disk companion to HISTSIZE. Controls how many commands get saved to &lt;code&gt;~/.bash_history&lt;/code&gt; when your session ends.&lt;/p&gt;
&lt;p id="328e"&gt;bash&lt;/p&gt;
&lt;pre&gt;&lt;span id="cbf3"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$HISTFILESIZE&lt;/span&gt;&lt;br&gt;&lt;span&gt;# 2000&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;export&lt;/span&gt; HISTFILESIZE=20000&lt;/span&gt;&lt;/pre&gt;
&lt;p id="cb91"&gt;These two variables are separate knobs and that trips people up. HISTSIZE is what’s held in memory during your session. HISTFILESIZE is what gets written to disk afterward. You can have a large in-session history and a small file, or vice versa. Set both deliberately instead of inheriting whatever your distro shipped with.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="446" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2AJ_9-lX8zKdxZEg-jDRqs8Q.png"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="1e1a"&gt;&lt;strong&gt;MANPATH&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="a514"&gt;Defines where the &lt;code&gt;man&lt;/code&gt; command looks for manual pages.&lt;/p&gt;
&lt;pre&gt;&lt;span id="d375"&gt;&lt;span&gt;export&lt;/span&gt; MANPATH=/usr/local/share/man:&lt;span&gt;$MANPATH&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="c604"&gt;This one matters the moment you start installing tools to non-standard locations custom builds, things in &lt;code&gt;/opt&lt;/code&gt;, tools you've compiled yourself. If &lt;code&gt;man yourtool&lt;/code&gt; returns nothing, the manual exists somewhere your system doesn't know to look. Add the right path to MANPATH and it works immediately. Most people just Google the man page instead. Setting MANPATH is faster and works offline.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="1595"&gt;&lt;strong&gt;DISPLAY&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="a467"&gt;Used by the X Window System to specify which display server to connect to.&lt;/p&gt;
&lt;pre&gt;&lt;span id="65d3"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$DISPLAY&lt;/span&gt;&lt;br&gt;&lt;span&gt;# :0.0&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;export&lt;/span&gt; DISPLAY=:0.0&lt;/span&gt;&lt;/pre&gt;
&lt;p id="ff59"&gt;You won’t think about this variable until the exact moment you need it. That moment is usually: you SSH into a remote machine, try to open something with a GUI, and get a cryptic error about not being able to connect to a display. DISPLAY is what tells the application where to render. Get it right and the GUI opens on your local screen. Get it wrong and you’re reading X11 error messages at a time of day that tests your patience. Enable X11 forwarding in your SSH config and set DISPLAY accordingly.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="d466"&gt;&lt;strong&gt;MAIL&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="7d0e"&gt;Points to the location of the current user’s mail spool where local system mail lands.&lt;/p&gt;
&lt;pre&gt;&lt;span id="42c6"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$MAIL&lt;/span&gt;&lt;br&gt;&lt;span&gt;# /var/mail/devlink&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="dcaa"&gt;Nobody thinks about this one. Cron jobs do. When a scheduled task fails or produces output, it doesn’t throw an error into the void it sends local mail. If MAIL isn’t set or nobody’s checking it, those failure messages have been quietly piling up unseen. This is genuinely how some cron jobs silently die for months before anyone notices. Check your mail spool occasionally. You might find a graveyard.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="5ea2"&gt;&lt;strong&gt;OSTYPE&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="d96c"&gt;Tells you what operating system you’re running on at the shell level.&lt;/p&gt;
&lt;pre&gt;&lt;span id="3313"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$OSTYPE&lt;/span&gt;&lt;br&gt;&lt;span&gt;# linux-gnu&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="555e"&gt;The place this earns its keep is cross-platform shell scripting. If you’re writing a script that needs to run on both Linux and macOS and eventually you will be OSTYPE lets you branch cleanly without spawning a &lt;code&gt;uname&lt;/code&gt; subprocess. Linux returns &lt;code&gt;linux-gnu&lt;/code&gt;, macOS returns &lt;code&gt;darwin&lt;/code&gt;, BSD variants have their own values. One variable, clean conditional logic, no forks.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="8611"&gt;&lt;strong&gt;COLORTERM&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="04bd"&gt;Signals to applications that your terminal supports true color full 24-bit RGB rather than the 256-color or 8-color fallback modes.&lt;/p&gt;
&lt;pre&gt;&lt;span id="3d64"&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;$COLORTERM&lt;/span&gt;&lt;br&gt;&lt;span&gt;# truecolor&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;export&lt;/span&gt; COLORTERM=truecolor&lt;/span&gt;&lt;/pre&gt;
&lt;p id="226c"&gt;This is one of those variables where not setting it correctly causes problems that look like something completely different. Your terminal supports full color. Your tool supports full color. But COLORTERM isn’t set, so the tool falls back to 256 colors and everything looks slightly off in a way you can’t quite name. Set it explicitly in your dotfiles and stop wondering why your color scheme looks duller on one machine than another.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="dd4a"&gt;Those eight variables separate the people who use Linux from the people who actually configure it. None of them are secrets they’re all in the documentation. But documentation doesn’t tell you &lt;em&gt;why&lt;/em&gt; they matter or when you’ll need them. That’s what experience does, and now you’ve got a head start.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="d0eb"&gt;Tier 3: The hacker toolkit&lt;/h2&gt;
&lt;blockquote&gt;&lt;p id="da52"&gt;&lt;strong&gt;&lt;em&gt;Disclaimer:&lt;/em&gt;&lt;/strong&gt;&lt;em&gt; Everything in this section is for ethical hacking, penetration testing, CTF challenges, and defensive security awareness. Understanding what attackers use is how defenders build better detection. Use these only on systems you own or have explicit written permission to test.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p id="a7b2"&gt;This is where the article gets interesting.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="e674"&gt;Tiers 1 and 2 are about configuration. Tier 3 is about control. These variables don’t just shape how your shell looks or which directories get searched they touch process loading, traffic routing, credential handling, library injection, and session forensics. They’re the reason experienced security engineers audit environment variables on any box they’re responsible for, and why attackers learn them before almost anything else.&lt;/p&gt;
&lt;p id="a0c3"&gt;None of this is exotic. It’s all documented. It’s all standard Linux. That’s precisely what makes it dangerous.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="446" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2ACZYYvWbxuBVtptM8qN0kug.png"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="1d51"&gt;&lt;strong&gt;HISTSIZE=0 and HISTFILESIZE=0&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;span id="9f92"&gt;&lt;span&gt;export&lt;/span&gt; HISTSIZE=0&lt;br&gt;&lt;span&gt;export&lt;/span&gt; HISTFILESIZE=0&lt;/span&gt;&lt;/pre&gt;
&lt;p id="8d16"&gt;Set both to zero at the start of a session and you get complete command history suppression. Nothing gets stored in memory, nothing gets written to disk when the session ends. Standard OPSEC in any authorized red team engagement.&lt;/p&gt;
&lt;p id="0fbe"&gt;Why defenders care: if you’re doing incident response on a compromised machine and find these set early in a session especially in &lt;code&gt;.bashrc&lt;/code&gt; or &lt;code&gt;/etc/profile&lt;/code&gt; someone was thinking about logging before they started working. That's not accidental configuration. That's a signal.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="2e9b"&gt;&lt;strong&gt;http_proxy and https_proxy&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;span id="f88c"&gt;&lt;span&gt;export&lt;/span&gt; http_proxy=&lt;span&gt;"&lt;a href="http://10.10.10.10:8080" rel="noopener noreferrer"&gt;http://10.10.10.10:8080&lt;/a&gt;"&lt;/span&gt;&lt;br&gt;&lt;span&gt;export&lt;/span&gt; https_proxy=&lt;span&gt;"&lt;a href="http://10.10.10.10:8080" rel="noopener noreferrer"&gt;http://10.10.10.10:8080&lt;/a&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="a2f6"&gt;Any process that respects these variables routes its traffic through the specified proxy. This is how security testers intercept application traffic through tools like &lt;a href="https://portswigger.net/burp" rel="noopener ugc nofollow noreferrer"&gt;Burp Suite&lt;/a&gt; during an engagement without touching application code, without modifying config files, without restarting services. Set the variable, run the process, read the traffic.&lt;/p&gt;
&lt;p id="d630"&gt;The lowercase versions (&lt;code&gt;http_proxy&lt;/code&gt;) are respected by most command-line tools. Some applications only check the uppercase versions. In practice, set both.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="9128"&gt;&lt;strong&gt;SSL_CERT_FILE and SSL_CERT_DIR&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;span id="1e57"&gt;&lt;span&gt;export&lt;/span&gt; SSL_CERT_FILE=/path/to/ca-bundle.pem&lt;br&gt;&lt;span&gt;export&lt;/span&gt; SSL_CERT_DIR=/path/to/ca-certificates&lt;/span&gt;&lt;/pre&gt;
&lt;p id="e45f"&gt;Processes that read these variables trust the certificates you point them at. In an authorized testing context, this is how you get an application to trust your proxy’s self-signed certificate so you can inspect encrypted HTTPS traffic without the tool throwing certificate errors and refusing to connect.&lt;/p&gt;
&lt;p id="a792"&gt;Combined with &lt;code&gt;http_proxy&lt;/code&gt;, these two variables give you a complete traffic interception setup without touching a single config file inside the application.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="13b2"&gt;&lt;strong&gt;LD_PRELOAD&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;span id="127a"&gt;&lt;span&gt;export&lt;/span&gt; LD_PRELOAD=/tmp/custom.so&lt;/span&gt;&lt;/pre&gt;
&lt;p id="a8f4"&gt;This is the most powerful variable on this list. When set, the dynamic linker loads your specified shared library &lt;em&gt;before anything else&lt;/em&gt; before the C standard library, before every other dependency the binary has. Functions in your library override functions in any other library the process loads.&lt;/p&gt;
&lt;p id="47fd"&gt;The implication: you can intercept system calls, hook functions, and fundamentally alter the behavior of a running process entirely from outside its source code. No recompilation. No patching. Just an environment variable.&lt;/p&gt;
&lt;p id="0e31"&gt;In CTF challenges and authorized penetration tests, LD_PRELOAD shows up in privilege escalation paths, sandbox bypasses, and function hooking scenarios. It’s also widely used legitimately memory profilers, debugging tools, and performance analyzers all use this same mechanism.&lt;/p&gt;
&lt;p id="a46b"&gt;Why defenders care: monitor for LD_PRELOAD pointing to paths in &lt;code&gt;/tmp&lt;/code&gt; or world-writable directories. Legitimate tools don't load libraries from there. Something else does.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="b1d5"&gt;&lt;strong&gt;LD_LIBRARY_PATH&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;span id="c584"&gt;&lt;span&gt;export&lt;/span&gt; LD_LIBRARY_PATH=/tmp/mylib:&lt;span&gt;$LD_LIBRARY_PATH&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="f097"&gt;Defines the directories the dynamic linker searches for shared libraries. By prepending a custom directory, you can substitute your own version of any library a binary depends on the binary loads your library instead of the real one and never knows the difference.&lt;/p&gt;
&lt;p id="2586"&gt;This is the environment variable equivalent of DLL hijacking on Windows. Same concept, same impact, different operating system. The attack works because most binaries trust that the libraries they load are the ones they expect.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="60bd"&gt;&lt;strong&gt;SUDO_ASKPASS&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;span id="3b69"&gt;&lt;span&gt;export&lt;/span&gt; SUDO_ASKPASS=/tmp/fake-prompt&lt;br&gt;sudo -A &lt;span&gt;whoami&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="6288"&gt;When &lt;code&gt;sudo&lt;/code&gt; is called with the &lt;code&gt;-A&lt;/code&gt; flag, instead of prompting for a password in the terminal, it executes whatever program is set in SUDO_ASKPASS and uses that program's output as the password. In authorized social engineering simulations, this technique demonstrates how applications and GUI wrappers around sudo can be leveraged to intercept credential input without the user realizing the prompt they're responding to isn't the real one.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="ef3d"&gt;&lt;strong&gt;LD_DEBUG&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;span id="4e43"&gt;&lt;span&gt;export&lt;/span&gt; LD_DEBUG=libs&lt;br&gt;./somebinary&lt;/span&gt;&lt;/pre&gt;
&lt;p id="e642"&gt;A reconnaissance variable. Setting it makes the dynamic linker print detailed output about every library being loaded the full path, the search order, which directories were checked, which version was found. No special permissions required. No tools to install.&lt;/p&gt;
&lt;p id="12df"&gt;In authorized engagements, this is how you identify which libraries a binary depends on and whether any of them are loaded from locations that could be hijacked with LD_LIBRARY_PATH. It turns library loading from a black box into a visible, auditable process.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="446" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2APk1zxjt62iCMCnFFAUGXbQ.png"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="6bc5"&gt;&lt;strong&gt;IFS&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;span id="2c8b"&gt;&lt;span&gt;export&lt;/span&gt; IFS=$&lt;span&gt;'\n'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="5fb7"&gt;IFS Internal Field Separator tells bash how to split strings into tokens. The default is space, tab, and newline. Changing it changes how every command and script in your session parses its inputs.&lt;/p&gt;
&lt;p id="9742"&gt;In exploit development and CTF challenges, subtle IFS manipulation breaks input validation in scripts that weren’t written with this in mind. A script that sanitizes space-separated input suddenly behaves differently when the separator changes. It’s a small variable with outsized consequences in poorly written shell code which, in production environments, is not rare.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="6d0f"&gt;&lt;strong&gt;GDBINIT&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;span id="dd46"&gt;&lt;span&gt;export&lt;/span&gt; GDBINIT=/tmp/custom-gdbinit&lt;/span&gt;&lt;/pre&gt;
&lt;p id="8572"&gt;GDB the GNU Debugger reads initialization commands from the file specified here on startup. In authorized assessments, this demonstrates how developer tooling itself can become an execution vector when environment variables aren’t controlled. CI pipelines, developer workstations, build servers anything that invokes GDB in an environment where GDBINIT can be influenced is a potential target.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="c928"&gt;&lt;strong&gt;TMOUT&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;span id="5deb"&gt;&lt;span&gt;export&lt;/span&gt; TMOUT=1&lt;/span&gt;&lt;/pre&gt;
&lt;p id="60f3"&gt;Sets bash to automatically terminate after the specified number of seconds of inactivity. Setting it to 1 closes a shell almost immediately after it goes idle. In an authorized engagement context, this is how you ensure a session closes cleanly without leaving an open shell exposed on a system you’re no longer actively using.&lt;/p&gt;
&lt;p id="8fd2"&gt;For defenders, it’s also worth setting in &lt;code&gt;/etc/profile&lt;/code&gt; on any server where unattended sessions are a risk. Idle shells with elevated privileges sitting open are an opportunity nobody needs to create.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;p id="789e"&gt;&lt;strong&gt;XDG_CONFIG_HOME&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="a525"&gt;&lt;span&gt;export&lt;/span&gt; XDG_CONFIG_HOME=/tmp/custom-config&lt;/span&gt;&lt;/pre&gt;
&lt;p id="3394"&gt;Applications that follow the &lt;a href="https://specifications.freedesktop.org/basedir-spec/latest/" rel="noopener ugc nofollow noreferrer"&gt;XDG Base Directory Specification&lt;/a&gt; read their config from this path. Redirect it to a controlled directory and you supply a completely custom configuration to any XDG-compliant application without modifying a single file on the real filesystem. Clean, contained, reversible.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="5dc9"&gt;Thirteen variables. Every one of them is in the man pages, in the official documentation, available to anyone who reads carefully enough. The difference isn’t access to secret knowledge it’s whether you took the time to understand what was already there.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="73a9"&gt;How to audit and manage your environment properly&lt;/h2&gt;
&lt;blockquote&gt;&lt;p id="3093"&gt;You’ve now got 30+ variables in your head. The natural next question is: what’s actually set on my system right now, and how do I control it properly?&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="5208"&gt;Most people interact with environment variables reactively they set something when a tool breaks, forget where they set it, and spend twenty minutes debugging the wrong file six months later. The fix is understanding the three distinct layers your environment is built from, and having a handful of commands you can reach for without thinking.&lt;/p&gt;
&lt;h3 id="5429"&gt;&lt;strong&gt;Viewing what’s currently set&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="6e68"&gt;&lt;strong&gt;Four commands, four slightly different outputs:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="14bd"&gt;&lt;span&gt;# Everything exported to the current environment&lt;/span&gt;&lt;br&gt;&lt;span&gt;printenv&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;# A specific variable&lt;/span&gt;&lt;br&gt;&lt;span&gt;printenv&lt;/span&gt; PATH&lt;br&gt;&lt;br&gt;&lt;span&gt;# All exported variables (similar to printenv)&lt;/span&gt;&lt;br&gt;&lt;span&gt;env&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;# All shell variables including unexported ones - verbose&lt;/span&gt;&lt;br&gt;&lt;span&gt;set&lt;/span&gt; | less&lt;/span&gt;&lt;/pre&gt;
&lt;p id="261e"&gt;&lt;code&gt;printenv&lt;/code&gt; and &lt;code&gt;env&lt;/code&gt; show you what's exported — what child processes will inherit. &lt;code&gt;set&lt;/code&gt; shows everything including shell-local variables that don't get passed down. For most auditing purposes, &lt;code&gt;printenv&lt;/code&gt; is what you want. For thoroughness, &lt;code&gt;set | less&lt;/code&gt; and scroll.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="b33d"&gt;&lt;strong&gt;Setting variables know your layers&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="6224"&gt;This is where most confusion lives. There are three distinct scopes and they don’t interact the way people assume:&lt;/p&gt;
&lt;pre&gt;&lt;span id="0185"&gt;&lt;span&gt;# Current session only — gone when you close the terminal&lt;/span&gt;&lt;br&gt;&lt;span&gt;export&lt;/span&gt; MY_VAR=&lt;span&gt;"value"&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;# Permanent for your user - survives reboots, applies to new sessions&lt;/span&gt;&lt;br&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;'export MY_VAR="value"'&lt;/span&gt; &amp;gt;&amp;gt; ~/.bashrc&lt;br&gt;&lt;span&gt;source&lt;/span&gt; ~/.bashrc&lt;br&gt;&lt;span&gt;# System-wide for all users - requires root&lt;/span&gt;&lt;br&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;'MY_VAR="value"'&lt;/span&gt; &amp;gt;&amp;gt; /etc/environment&lt;/span&gt;&lt;/pre&gt;
&lt;p id="c330"&gt;The hierarchy goes: system (&lt;code&gt;/etc/environment&lt;/code&gt;) → user shell config (&lt;code&gt;~/.bashrc&lt;/code&gt;, &lt;code&gt;~/.bash_profile&lt;/code&gt;) → current session (&lt;code&gt;export&lt;/code&gt;). Each layer can override the one above it. When a variable is behaving unexpectedly, you're almost always looking at a conflict between two of these layers something set in &lt;code&gt;/etc/environment&lt;/code&gt; getting overridden in &lt;code&gt;.bashrc&lt;/code&gt;, or a session export shadowing both.&lt;/p&gt;
&lt;p id="4644"&gt;I’ve personally spent an embarrassing amount of time debugging a “why is this variable always wrong” issue that turned out to be set correctly in &lt;code&gt;.bashrc&lt;/code&gt;, overridden in &lt;code&gt;.bash_profile&lt;/code&gt;, and then overridden again by a script that exported it fresh on every run. Check all three layers before assuming the system is broken.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;p id="dc26"&gt;&lt;strong&gt;Removing and locking variables&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="041d"&gt;&lt;span&gt;# Remove a variable from the current session&lt;/span&gt;&lt;br&gt;&lt;span&gt;unset&lt;/span&gt; MY_VAR&lt;br&gt;&lt;br&gt;&lt;span&gt;# Lock a variable so it can't be modified in the current session&lt;/span&gt;&lt;br&gt;&lt;span&gt;readonly&lt;/span&gt; SECURE_VAR=&lt;span&gt;"value"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="aa25"&gt;&lt;code&gt;unset&lt;/code&gt; is straightforward. &lt;code&gt;readonly&lt;/code&gt; is underused once set, any attempt to modify or unset that variable in the current session returns an error. Useful for variables that should never change after initialization, like paths to critical binaries in a script.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="a20d"&gt;&lt;strong&gt;Passing variables to a single command without exporting&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;span id="4afb"&gt;MY_VAR=&lt;span&gt;"value"&lt;/span&gt; some-command&lt;/span&gt;&lt;/pre&gt;
&lt;p id="3f01"&gt;This sets the variable in the environment of &lt;code&gt;some-command&lt;/code&gt; only it doesn't persist to your shell, doesn't affect other processes, disappears immediately after the command finishes. Useful for one-off overrides without polluting your session. A lot of developers don't know this syntax exists and reach for &lt;code&gt;export&lt;/code&gt; when they don't actually need it.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h3 id="6c1a"&gt;&lt;strong&gt;Auditing for suspicious variables&lt;/strong&gt;&lt;/h3&gt;
&lt;p id="2632"&gt;On any machine you’re responsible for especially one you’ve inherited or that’s been flagged for investigation:&lt;/p&gt;
&lt;pre&gt;&lt;span id="ef31"&gt;&lt;span&gt;# Check startup files for variables that shouldn't be there&lt;/span&gt;&lt;br&gt;grep -r &lt;span&gt;"LD_PRELOAD|HISTSIZE=0|SUDO_ASKPASS"&lt;/span&gt; &amp;lt;br&amp;gt;  /etc/profile.d/ ~/.bashrc ~/.bash_profile ~/.profile&lt;/span&gt;&lt;/pre&gt;
&lt;p id="caee"&gt;If any of those turn up unexpectedly, don’t assume it’s a misconfiguration. Investigate. Their presence in startup files especially &lt;code&gt;HISTSIZE=0&lt;/code&gt; and &lt;code&gt;LD_PRELOAD&lt;/code&gt; pointing to &lt;code&gt;/tmp&lt;/code&gt; is a meaningful signal, not noise.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="4e8d"&gt;That’s the full management toolkit. View, set, scope, lock, audit. Five operations that cover everything you’ll need in day-to-day work and in the more interesting situations this knowledge puts you in reach of.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="0619"&gt;Security rules most hardening guides forget&lt;/h2&gt;
&lt;p id="0042"&gt;Configuration guides cover firewalls. They cover SSH keys. They cover &lt;code&gt;fail2ban&lt;/code&gt; and port knocking and a dozen other things that are genuinely important. Environment variables don't make the list often, which is exactly why this attack surface stays underappreciated.&lt;/p&gt;
&lt;p id="7091"&gt;&lt;strong&gt;A few rules that actually matter:&lt;/strong&gt;&lt;/p&gt;
&lt;p id="7333"&gt;&lt;strong&gt;Never store secrets in plain environment variables.&lt;/strong&gt; They show up in &lt;code&gt;printenv&lt;/code&gt;. They show up in &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/environ&lt;/code&gt; readable by any process running as the same user. They show up in crash dumps, in CI logs when a build fails mid-run, and in container inspection output if your orchestration config is even slightly misconfigured. Use a secrets manager. Pass secrets through files with tight permissions, not shell exports.&lt;/p&gt;
&lt;p id="a101"&gt;&lt;strong&gt;Lock down critical variables in scripts:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="63b4"&gt;&lt;span&gt;readonly&lt;/span&gt; PATH=&lt;span&gt;"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"&lt;/span&gt;&lt;br&gt;&lt;span&gt;readonly&lt;/span&gt; SECURE_VAR=&lt;span&gt;"value"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="0bb5"&gt;&lt;strong&gt;Harden your history file:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="02d5"&gt;&lt;span&gt;chmod&lt;/span&gt; 600 ~/.bash_history&lt;/span&gt;&lt;/pre&gt;
&lt;p id="0fc3"&gt;The default permissions on bash_history are often more permissive than they should be. Other users on a shared system can read your command history. One command does the job.&lt;/p&gt;
&lt;p id="faff"&gt;&lt;strong&gt;Audit environment files on every box you manage:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;span id="b995"&gt;grep -r &lt;span&gt;"LD_PRELOAD|HISTSIZE=0|SUDO_ASKPASS"&lt;/span&gt; &amp;lt;br&amp;gt;  /etc/profile.d/ ~/.bashrc ~/.profile ~/.bash_profile&lt;/span&gt;&lt;/pre&gt;
&lt;p id="15f9"&gt;Run this on any system you’ve inherited, any container base image you didn’t build yourself, any machine that’s been flagged in an incident. Unexpected results here aren’t curiosities they’re findings.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="4a6b"&gt;Conclusion&lt;/h2&gt;
&lt;p id="4444"&gt;Here’s a slightly uncomfortable opinion: most Linux security hardening guides are written for people who already know this stuff. They assume you understand the environment variable layer, so they skip it entirely and go straight to network rules and access controls. That assumption leaves a real gap one that shows up repeatedly in CTF writeups, in post-incident reports, and in the gap between developers who use Linux and engineers who understand it.&lt;/p&gt;
&lt;p id="fecf"&gt;Environment variables are not a trivia topic. They’re the control plane for every process running on your system. The 30+ variables in this article aren’t exhaustive they’re the ones that matter most, the ones that explain the most behavior, and the ones that show up when things go wrong or when someone with bad intentions goes right.&lt;/p&gt;
&lt;p id="434f"&gt;The terminal is the most honest interface on any system. It hides nothing if you know what to ask. Start asking better questions.&lt;/p&gt;
&lt;p id="c438"&gt;As containers and srverless architectures continue to take over infrastructure, environment variables are increasingly &lt;em&gt;the&lt;/em&gt; primary configuration layer injected at runtime, scoped per service, and almost never audited properly. That makes this knowledge more relevant every year, not less.&lt;/p&gt;
&lt;p id="dbe9"&gt;Go through this list on a test machine. Run each export. Watch what changes. The best way to internalize this is to break something deliberately in a safe environment and understand exactly why it broke.&lt;/p&gt;
&lt;p id="e518"&gt;And if you’ve got a cursed environment variable story a HISTSIZE=0 you found where it shouldn’t be, an LD_PRELOAD incident, a production outage that traced back to TZ being unset drop it in the comments. I genuinely want to hear it.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="bc95"&gt;Helpful resources&lt;/h2&gt;
&lt;ul&gt;

&lt;li id="ffec"&gt;

&lt;a href="https://man7.org/linux/man-pages/man8/ld.so.8.html" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Linux man page: ld.so&lt;/strong&gt;&lt;/a&gt; full documentation on LD_PRELOAD, LD_LIBRARY_PATH, LD_DEBUG behavior&lt;/li&gt;

&lt;li id="65ad"&gt;

&lt;a href="https://gtfobins.github.io" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;GTFOBins&lt;/strong&gt;&lt;/a&gt; practical reference for how environment variables feature in privilege escalation paths&lt;/li&gt;

&lt;li id="83c1"&gt;

&lt;a href="https://wiki.archlinux.org/title/Environment_variables" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Arch Wiki: Environment Variables&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;one of the clearest practical guides on scoping and management&lt;/li&gt;

&lt;li id="dfdd"&gt;

&lt;a href="https://specifications.freedesktop.org/basedir-spec/latest/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;XDG Base Directory Specification&lt;/strong&gt;&lt;/a&gt; official spec for XDG_CONFIG_HOME and related variables&lt;/li&gt;

&lt;li id="750f"&gt;

&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;OWASP Secrets Management Cheat Sheet&lt;/strong&gt;&lt;/a&gt; why plain environment variables are the wrong place for credentials&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>linux</category>
    </item>
    <item>
      <title>Software development is having a second chance. Nobody saw this coming.</title>
      <dc:creator>&lt;devtips/&gt;</dc:creator>
      <pubDate>Thu, 07 May 2026 14:01:55 +0000</pubDate>
      <link>https://dev.to/dev_tips/software-development-is-having-a-second-chance-nobody-saw-this-coming-2eih</link>
      <guid>https://dev.to/dev_tips/software-development-is-having-a-second-chance-nobody-saw-this-coming-2eih</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h1 id="e3e5"&gt;&lt;/h1&gt;
&lt;h2 id="587e"&gt;&lt;em&gt;Everyone spent two years arguing about whether AI would kill the craft. Turns out it might be the thing that saves it.&lt;/em&gt;&lt;/h2&gt;
&lt;span&gt;&lt;/span&gt;&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p id="2666"&gt;Here’s a thing nobody wants to admit out loud: while half the dev community was busy writing “AI is going to take our jobs” threads, the other half was quietly shipping more software than ever before. Solo. Faster. With fewer standups.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="45f1"&gt;There’s something deeply ironic about that.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="7f9b"&gt;For the past two years, the loudest conversation in tech has been about what AI is &lt;em&gt;taking away&lt;/em&gt; from software development. The junior roles. The craft. The thinking. And look some of that is real. Nobody’s going to pretend the job market looks the same as it did in 2022. But here’s the angle that keeps getting skipped in all those LinkedIn hot takes: software development as a &lt;em&gt;practice&lt;/em&gt; the act of building systems, shipping products, solving real problems with code is quietly having a moment.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="3158"&gt;Not a crisis. A comeback.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="70eb"&gt;GitHub recently had to redesign its entire infrastructure to handle &lt;strong&gt;30x its previous scale&lt;/strong&gt; not because of more developers, but because agentic workflows and AI-assisted development exploded so fast the old architecture couldn’t keep up. That’s not a dying field. That’s a field that just found a second gear.&lt;/p&gt;
&lt;p id="2032"&gt;This article is about that second gear.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="ea1b"&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; AI didn’t kill software development. It stress-tested it, stripped out the tedious parts, and handed the keys back to engineers who know what to do with them. Here’s what that actually looks like.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="128d"&gt;&lt;strong&gt;The myth of the dying craft&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="3743"&gt;Let’s get something straight. “AI is killing software development” is doing a lot of heavy lifting as a take, and most of the people repeating it are confusing two very different things: the &lt;em&gt;job market&lt;/em&gt; and the &lt;em&gt;craft&lt;/em&gt;.&lt;/p&gt;
&lt;p id="e8b0"&gt;The job market? Yeah, it’s shifting. That’s real and worth a separate conversation. But the craft the actual act of designing systems, writing code, shipping things that work is not dying. It’s redistributing. And there’s a massive difference.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="51ff"&gt;&lt;strong&gt;Here’s the tell:&lt;/strong&gt; if software development were actually collapsing, you wouldn’t see GitHub scrambling to redesign its infrastructure for 30x capacity because agentic development workflows accelerated faster than anyone predicted. You wouldn’t see repository creation, pull request activity, and API usage all trending sharply upward at the same time. Those aren’t the numbers of a dying discipline. Those are the numbers of a discipline that just removed a ceiling.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="7ca7"&gt;Think about what the day-to-day used to look like. I remember spending a full afternoon building a pagination component. Not because it was intellectually interesting. Not because it required deep thought. Because there was no faster way. Someone had to write it, so someone did. That was the job a mix of genuinely hard problems and an enormous amount of mechanical, repetitive work that just had to get done.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="9282"&gt;AI ate the second category. Almost entirely.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="c144"&gt;And this is the part people keep misreading as loss. Calculators didn’t kill mathematicians. They killed &lt;em&gt;arithmetic drudgery&lt;/em&gt;, which freed mathematicians to do more actual mathematics. Same play. Different stack. The pagination component was never the craft it was the toll booth before the craft.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2Afy10GiLvGg-agYq5C_rKKg.png"&gt;&lt;p id="be1d"&gt;The craft is still there. It just finally got a fast lane.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="1a14"&gt;&lt;strong&gt;The 50-year debt AI is finally paying off&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="60f9"&gt;Software development has been dragging a 50-year backpack of accumulated bad decisions. Bad abstractions that got copy-pasted into frameworks. Over-engineered patterns that became industry standards before anyone could stop them. Wheels reinvented so many times they stopped being round.&lt;/p&gt;
&lt;p id="dad1"&gt;Every senior dev has a version of this story. Mine involves a codebase where three different teams had independently written three different date formatting utilities none of which talked to each other, all of which were “the right way” according to whoever wrote them. Nobody meant for it to happen. It just did. Because software moves fast, documentation is always someone else’s problem, and the cost of fixing old decisions is always higher than the cost of living with them.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="f35f"&gt;For decades, that debt just… compounded. Quietly. In the walls.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="3b1a"&gt;Here’s what’s actually interesting about AI: it’s the first thing that moves fast enough to surface &lt;em&gt;and&lt;/em&gt; bypass that debt at the same time. Auth systems that used to take a week to spec out and another two to implement correctly? Afternoon work now. CI/CD pipelines that felt like a rite of passage the kind where you hadn’t really earned your DevOps stripes until you’d genuinely cried over a YAML indent? Scaffolded in minutes, debugged in context.&lt;/p&gt;
&lt;p id="152c"&gt;It’s not just that things are faster. It’s that old patterns are getting stress-tested at a pace humans alone never could have managed. The bad abstractions are getting caught earlier. The reinvented wheels are getting spotted before they ship.&lt;/p&gt;
&lt;p id="447e"&gt;Think of it like inheriting a house with thirty years of questionable DIY plumbing except now you have a contractor who can see every pipe in the wall before touching a single one.&lt;/p&gt;
&lt;p id="b32d"&gt;The debt isn’t gone. But for the first time, we have a tool that doesn’t just add to it. Sometimes it actually pays some of it back.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2A1JkXV4dA2W0XnJ9TxMij_A.png"&gt;&lt;p id="c54b"&gt;That’s not a small thing. That’s kind of a big deal.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="9e00"&gt;&lt;strong&gt;One dev, infinite surface area&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="7db9"&gt;Here’s the shift that doesn’t get talked about enough and it’s the one that actually changes everything.&lt;/p&gt;
&lt;p id="31f6"&gt;The real unlock from AI-assisted development isn’t that code gets written faster. It’s that one engineer can now own and reason about dramatically more surface area than before. Not a little more. A lot more. Backend, DevOps, frontend, QA, monitoring the full stack of concerns that used to require four different people with four different specializations can now live inside one person’s working context.&lt;/p&gt;
&lt;p id="6bd3"&gt;I watched a friend ship a SaaS product in six weeks. Solo. Full auth layer, billing integration, CI/CD pipeline, a halfway decent UI. In 2020, that same scope took his startup four months with a team of three. Same person. Same skills. Wildly different output. The only meaningful variable was the tooling.&lt;/p&gt;
&lt;p id="a49c"&gt;Tools like &lt;a href="https://www.cursor.com/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Cursor&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;, &lt;/em&gt;&lt;/strong&gt;&lt;a href="https://aider.chat/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Aider&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;,&lt;/em&gt; &lt;/strong&gt;and &lt;strong&gt;&lt;em&gt;Claude Code&lt;/em&gt;&lt;/strong&gt; aren’t just autocomplete on steroids. They’re surface-area expanders. They let a single engineer hold more of the system in their head or at least in their context window without dropping pieces of it on the floor.&lt;/p&gt;
&lt;p id="b96e"&gt;This is the second chance for an archetype that never quite fit the enterprise era: the solo builder. The person who could see the whole product but never had the hours to execute all of it alone. That person now has a legitimate shot.&lt;/p&gt;
&lt;p id="87b2"&gt;It’s not that one player got dramatically better overnight. It’s that the map got smaller and the spawn points moved. A solo dev in 2025 starts the game with resources that a small team in 2019 would’ve spent months unlocking.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2AqI3Z_wY0Oxxdy4t5c8gIuA.png"&gt;&lt;p id="e86b"&gt;The surface area didn’t shrink. The headcount required to cover it did.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="1d98"&gt;&lt;strong&gt;What “good engineering” means now&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="4f33"&gt;Something quiet is happening to the definition of a good engineer. It’s not being announced anywhere. There’s no RFC, no industry memo, no Stack Overflow post with 4,000 upvotes explaining it. It’s just… shifting. And if you’re not paying attention, you’ll miss the transition entirely.&lt;/p&gt;
&lt;p id="de87"&gt;For most of software’s history, “good engineer” meant someone who could write correct, efficient code across a broad range of problems. The person who knew the right data structure without Googling it. Who could debug a memory leak at midnight without losing their mind. Who had enough language fluency to move fast without making a mess.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="bac3"&gt;That still matters. But it’s no longer the whole game.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="840d"&gt;A colleague of mine spent three hours chasing a bug that an AI tool introduced into a service. Genuinely frustrating, genuinely subtle. Then he spent thirty minutes giving the same tool the right context the actual constraints, the edge cases, the downstream dependencies and watched it fix the bug cleanly. The skill wasn’t in writing the fix. It was in knowing exactly what information to hand over and when.&lt;/p&gt;
&lt;p id="b31a"&gt;That’s the shift. Good engineering now has a new layer on top: judgment, taste, and the ability to reason about systems at a level where you’re directing work rather than just executing it.&lt;/p&gt;
&lt;p id="1a44"&gt;Think less “master chef who cooks every dish” and more “executive chef who knows exactly what to order, from where, and what to do when it arrives wrong.”&lt;/p&gt;
&lt;p id="a405"&gt;AI is a very fast, occasionally overconfident intern. It needs direction. It needs someone who can spot when it’s confidently wrong which, if you’ve used any of these tools seriously, you know happens more than the demos suggest.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2AWOqviPEGCgXwY_IRNQRcsA.png"&gt;&lt;p id="f86d"&gt;The engineers who’ll thrive aren’t the ones who out-type AI. They’re the ones who out-think it.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="b91f"&gt;&lt;strong&gt;The uncomfortable truth about the second chance&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="8583"&gt;Second chances come with conditions. This one’s no different, and it would be dishonest to skip past that part.&lt;/p&gt;
&lt;p id="a2d6"&gt;Junior developers have a harder path right now. The entry-level roles that used to absorb new grads the ones that were repetitive enough to be learnable and scoped enough to be survivable a lot of those look different now. Some are gone. That’s worth saying clearly, without dressing it up as “the market is evolving” or some other phrase that means the same thing while feeling less uncomfortable.&lt;/p&gt;
&lt;p id="3a16"&gt;But here’s where it’s worth zooming out.&lt;/p&gt;
&lt;p id="6bbd"&gt;Every major shift in this industry looked like the end from inside it. Cloud computing arrived and everyone asked why anyone would ever manage their own servers again. Docker showed up and the “but it works on my machine” era started dying. Each time, the engineers who leaned in early who treated the new thing as infrastructure to understand rather than a threat to outlast came out with more leverage, not less.&lt;/p&gt;
&lt;p id="d858"&gt;This moment is opt-in. That’s the uncomfortable part. The second chance doesn’t land in your inbox automatically. It goes to the people who decide to pick it up.&lt;/p&gt;
&lt;p id="f024"&gt;The craft is still here. The problems are still real. The systems still need someone to design them, reason about them, and take responsibility when they fall over at the worst possible moment.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="dc92"&gt;AI doesn’t do that last part. Not yet. Probably not for a while.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="1849"&gt;That gap is the opportunity.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="23e3"&gt;&lt;strong&gt;The craft didn’t die. It leveled up.&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="0f87"&gt;Here’s where I land on this, for whatever it’s worth from someone who’s been watching this industry long enough to have strong opinions about tab versus space debates that nobody asked for.&lt;/p&gt;
&lt;p id="3e37"&gt;Software development isn’t being replaced. It’s getting a difficulty reset. The grind that used to live in the boilerplate, the scaffolding, the “I’ve written this exact thing four times across three jobs” work that part is going away. And honestly, good riddance. Nobody got into this field because they loved writing the fifteenth variation of a user authentication flow.&lt;/p&gt;
&lt;p id="4dd0"&gt;The second chance is real. But it belongs to engineers who are willing to see AI as infrastructure the same way we eventually stopped arguing about whether to use the cloud and just started building on it.&lt;/p&gt;
&lt;p id="f4ef"&gt;The next decade of software will be built by fewer people, moving faster, covering more ground. The quality ceiling on everything they ship will depend almost entirely on the judgment, taste, and systems thinking of the humans steering it. That’s not a demotion. That’s actually a more interesting job than the one that came before it.&lt;/p&gt;
&lt;blockquote&gt;

&lt;p id="4401"&gt;So the question worth sitting with isn’t “is AI taking over software development?”&lt;/p&gt;

&lt;p id="8cc3"&gt;It’s: are you picking up the controller or not?&lt;/p&gt;


&lt;/blockquote&gt;
&lt;p id="4332"&gt;Because the game didn’t end. The respawn screen just looked a lot like a warning, and most people stopped reading before the countdown finished.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="8b73"&gt;Drop your take in the comments second chance or last chance? I want to know where you land.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="7e7c"&gt;Helpful resources&lt;/h2&gt;
&lt;ul&gt;

&lt;li id="2304"&gt;&lt;a href="https://github.blog/news-insights/company-news/an-update-on-github-availability/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;GitHub Blog:&lt;/strong&gt; Why GitHub had to redesign for 30x scale&lt;/a&gt;&lt;/li&gt;

&lt;li id="4688"&gt;&lt;a href="https://www.cursor.com/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Cursor&lt;/strong&gt; AI-first code editor&lt;/a&gt;&lt;/li&gt;

&lt;li id="b997"&gt;&lt;a href="https://aider.chat/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Aider&lt;/strong&gt; AI pair programming in your terminal&lt;/a&gt;&lt;/li&gt;

&lt;li id="16a3"&gt;&lt;a href="https://docs.anthropic.com/en/docs/claude-code/overview" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Claude Code &lt;/strong&gt;agentic coding from the terminal&lt;/a&gt;&lt;/li&gt;

&lt;li id="72cb"&gt;&lt;a href="https://dora.dev/" rel="noopener ugc nofollow noreferrer"&gt;DORA 2025 State of DevOps Report&lt;/a&gt;&lt;/li&gt;

&lt;li id="6187"&gt;&lt;a href="https://github.blog/news-insights/octoverse/" rel="noopener ugc nofollow noreferrer"&gt;&lt;strong&gt;Octoverse 2025&lt;/strong&gt; GitHub’s annual developer report&lt;/a&gt;&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Kubernetes 1.36 killed your webhooks. Here are 10 other things it quietly changed.</title>
      <dc:creator>&lt;devtips/&gt;</dc:creator>
      <pubDate>Thu, 07 May 2026 13:56:43 +0000</pubDate>
      <link>https://dev.to/dev_tips/kubernetes-136-killed-your-webhooks-here-are-10-other-things-it-quietly-changed-171</link>
      <guid>https://dev.to/dev_tips/kubernetes-136-killed-your-webhooks-here-are-10-other-things-it-quietly-changed-171</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="c303"&gt;Haru dropped with a Hokusai painting and a calligraphy inscription. Buried underneath all that poetry is a release that rearranged how your cluster actually works.&lt;/h2&gt;
&lt;span&gt;&lt;/span&gt;&lt;p id="d443"&gt;There’s a tradition in Kubernetes releases where the changelog reads like a corporate memo dry, dense, and written for people who already know what they’re looking for. Kubernetes 1.36 broke that tradition in the most unexpected way. The release is named &lt;em&gt;Haru&lt;/em&gt; a Japanese word that carries three meanings at once: spring, clear skies, far-off horizons. The logo is a reimagining of Hokusai’s &lt;em&gt;Red Fuji&lt;/em&gt;, with the Kubernetes helm floating in the sky above the mountain. The calligraphy brushed across it translates to&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="daa1"&gt;&lt;em&gt;“soar into clear skies; toward tomorrow’s sunrise.”&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p id="95c8"&gt;And I’m sitting here like okay, K8s. We’re doing this now.&lt;/p&gt;
&lt;p id="f705"&gt;The poetry is earned though, because underneath it, 1.36 is one of the more consequential releases in recent memory. Not because it introduced a dozen shiny new alpha features (it did that too), but because it finally graduated things that have been in progress for years, killed things that should’ve died ages ago, and gave platform engineers actual tools to stop duct-taping their clusters together.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="7025"&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Mutating Admission Policies are GA and webhooks are on notice. User Namespaces finally hit stable after four years in alpha. Ingress NGINX is officially retired not deprecated, &lt;em&gt;retired&lt;/em&gt;. DRA grew up for real GPU scheduling. OCI volumes made ML model distribution less embarrassing. HPA scale-to-zero is now a thing. And the gitRepo volume type is gone, eight years after everyone was told it was going away.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="ea0a"&gt;&lt;strong&gt;Webhooks: cooked&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="5081"&gt;If you’ve run admission webhooks in production, you already know the drill. Every API request hits your webhook server on the way in. That server lives outside the cluster, needs its own deployment, its own TLS, its own on-call rotation. And when it goes down and it will go down pod scheduling stops. Not degrades. Stops.&lt;/p&gt;
&lt;p id="b269"&gt;I once spent a week chasing a stalled CI/CD pipeline. Deployments failing with no clear pattern, logs noisy but useless. Root cause: an OPA Gatekeeper webhook silently dropping pod CREATE requests under load. A week. For a dropped request.&lt;/p&gt;
&lt;p id="f378"&gt;That entire class of problem disappears with &lt;a href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-api-machinery/3962-mutating-admission-policies/README.md" rel="noopener ugc nofollow noreferrer"&gt;MutatingAdmissionPolicies&lt;/a&gt;, which hit GA in 1.36. Instead of an external service, you write mutation logic as CEL expressions evaluated inline inside the API server. No external server. No TLS certs to rotate. No 3am pages because the webhook pod got evicted. Define it as a Kubernetes object, version-control it in Git, ship it through your normal GitOps flow.&lt;/p&gt;
&lt;p id="2655"&gt;&lt;strong&gt;The asterisk:&lt;/strong&gt; if your mutation logic needs to call an external service, you still need a webhook. But that’s maybe 20% of real-world use cases. Label injection, sidecar prepending, field defaulting all of that is now native and in-process.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2AQ6Jrcgbg4w6pXzGfz_hGuQ.jpeg"&gt;&lt;p id="53a8"&gt;Webhooks aren’t gone. They’re just no longer the only option. And for most teams, that’s a massive operational relief.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="ac6a"&gt;&lt;strong&gt;Root inside a container was always fiction&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="a57d"&gt;Here’s something nobody likes to say out loud: when your container runs as root, it’s running as root on the host too. The container boundary exists, sure but if something escapes it, it lands on your node with full administrative power. That’s not a hypothetical. Container breakout vulnerabilities are real, documented, and have CVEs attached to them.&lt;/p&gt;
&lt;p id="640f"&gt;The fix has existed in Linux for years. User Namespaces let you map UID 0 inside the container to an unprivileged user on the host. Your process thinks it’s root. The kernel knows it isn’t. If it escapes, it lands with essentially nothing.&lt;/p&gt;
&lt;p id="9d59"&gt;Kubernetes has been working toward this since v1.25 in August 2022. Four years of alpha, beta, validation on production workloads, and edge case hunting. In 1.36 it’s finally &lt;a href="https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/127-user-namespaces" rel="noopener ugc nofollow noreferrer"&gt;Stable&lt;/a&gt;. You enable it with one field:&lt;/p&gt;
&lt;pre&gt;&lt;span id="bf89"&gt;&lt;span&gt;spec:&lt;/span&gt;&lt;br&gt;  &lt;span&gt;hostUsers:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="73b6"&gt;That’s it. That’s the fence with a lock.&lt;/p&gt;
&lt;p id="1f95"&gt;Before this, running genuinely rootless containers in Kubernetes meant layering on gVisor, Kata Containers, or some combination of third-party tooling and crossed fingers. Now it’s native, stable, and production-ready with no extra dependencies.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="1433" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2AhlKRN7L6FCE0T2mabdzVtg.jpeg"&gt;&lt;p id="e20c"&gt;&lt;strong&gt;One watch-out:&lt;/strong&gt; Images built assuming real root privileges may behave unexpectedly under UID remapping. Test on non-critical workloads first before rolling it cluster-wide.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="8547"&gt;&lt;strong&gt;Ingress NGINX is dead. Not deprecated. Dead.&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="8953"&gt;On March 24, 2026, Kubernetes SIG Network and the Security Response Committee officially retired Ingress NGINX. No more releases. No bug fixes. No security patches. If you’re running it today, you’re running unsupported software in production and the maintainers have left the building.&lt;/p&gt;
&lt;p id="b946"&gt;This isn’t a soft deprecation with a two-release runway. There’s no “we recommend migrating by v1.38.” It’s done. The flaws were too deep, the maintainer bandwidth wasn’t there, and the Security Response Committee signed off on pulling the plug.&lt;/p&gt;
&lt;p id="231f"&gt;The uncomfortable part is how many production clusters are still running it. Ingress NGINX became the default answer to “how do I route traffic in Kubernetes” for years. It was good enough, it was everywhere, and nobody had a strong reason to migrate until now.&lt;/p&gt;
&lt;p id="d5ac"&gt;The migration path is &lt;a href="https://gateway-api.sigs.k8s.io/" rel="noopener ugc nofollow noreferrer"&gt;Gateway API v1.5&lt;/a&gt;. It gives you structured routing, cross-namespace references, and a proper separation between infrastructure concerns and developer concerns things Ingress never cleanly handled. The &lt;a href="https://github.com/kubernetes-sigs/ingress2gateway" rel="noopener ugc nofollow noreferrer"&gt;Ingress2Gateway project&lt;/a&gt; hit 1.0 in March 2026 specifically to help with this transition. The tooling exists. The excuses don’t.&lt;/p&gt;
&lt;p id="e6d2"&gt;If you’re on Ingress NGINX, this is the conversation to have with your team this sprint, not next quarter.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="3019"&gt;&lt;strong&gt;DRA grew a brain. GPU scheduling makes sense now.&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="49dd"&gt;If you’ve ever tried to schedule GPU workloads on Kubernetes before Dynamic Resource Allocation existed, you know what it felt like. Node selectors, custom labels, resource limits that didn’t reflect actual hardware topology, vendor-specific device plugins that all invented their own interfaces. It worked, technically. The way duct tape works, technically.&lt;/p&gt;
&lt;p id="e22d"&gt;DRA has been Kubernetes’ answer to this for a few releases now a proper framework for scheduling specialized hardware like GPUs, accelerators, and custom silicon. 1.36 is the release where it stops feeling experimental.&lt;/p&gt;
&lt;p id="aa77"&gt;A few things landed here that matter. Taints and tolerations for hardware devices: you can now take a specific GPU offline for maintenance without touching the rest of the cluster. Same mental model you already use for nodes, applied to individual devices. Resource Health Status is now surfaced through standard Kubernetes tooling if a GPU is unhealthy, it shows up like any unhealthy pod or node. No custom monitoring stack per vendor. No guessing. Just a status field that’s either green or it isn’t.&lt;/p&gt;
&lt;p id="183e"&gt;Per-pod DRA resource visibility is also locked to GA, meaning monitoring tools, billing systems, and operators can reliably query exactly what hardware each pod has been allocated. That matters a lot when your GPU cluster costs more per hour than most engineers’ daily rate.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2Ah-nrt_DgQTGEQA3TSoUXcA.jpeg"&gt;&lt;p id="51e3"&gt;The AI/ML infra angle here is obvious. Training jobs are expensive. Scheduling a 12-hour run onto a degraded GPU because your health checks lived in a vendor sidecar that missed a signal is the kind of thing that ends sprints. 1.36 starts fixing the foundation.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="1930"&gt;&lt;strong&gt;Shipping ML models in init containers was embarrassing&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="adf1"&gt;Getting a large model into a container has always been an exercise in picking the least bad option. Bake it into the image and watch your pull times become a running joke. Use an init container to download it at runtime and accept the complexity and failure modes that come with that. Fight ConfigMap size limits trying to get config artifacts in. Build a custom distribution pipeline and maintain that forever.&lt;/p&gt;
&lt;p id="39ea"&gt;None of these are good. All of them are common.&lt;/p&gt;
&lt;p id="285b"&gt;&lt;a href="https://www.perfectscale.io/blog/kubernetes-v1-36-sneak-peek" rel="noopener ugc nofollow noreferrer"&gt;OCI VolumeSource&lt;/a&gt; hits GA in 1.36 and it’s the answer that should’ve existed earlier. Reference any OCI image as a volume. Kubernetes pulls it and mounts the contents into your pod exactly like a regular volume. Your 40GB model lives in its own OCI artifact, versioned and distributed through the same registry infrastructure you already use. Your app container stays lean. Updates to the model don’t require rebuilding the app image.&lt;/p&gt;
&lt;pre&gt;&lt;span id="52bb"&gt;&lt;span&gt;volumes:&lt;/span&gt;&lt;br&gt;  &lt;span&gt;-&lt;/span&gt; &lt;span&gt;name:&lt;/span&gt; &lt;span&gt;model-weights&lt;/span&gt;&lt;br&gt;    &lt;span&gt;image:&lt;/span&gt;&lt;br&gt;      &lt;span&gt;reference:&lt;/span&gt; &lt;span&gt;registry.example.com/models/llama-weights:v3&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2AT2WL5eenMjZczj29x4O1qA.jpeg"&gt;&lt;p id="ab13"&gt;For AI/ML workloads specifically this is a meaningful quality-of-life change. Model and code have different update cadences, different owners, and different size profiles. Treating them as separate artifacts that get composed at runtime is just the correct architecture. OCI VolumeSource makes that native instead of something you have to engineer around Kubernetes to achieve.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="5902"&gt;&lt;strong&gt;HPA scale-to-zero: serverless K8s without the serverless tax&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="0f29"&gt;Every team has them. Pods sitting at 0.3% CPU at midnight, burning compute budget, waiting for a queue message that won’t arrive until morning. You know you should scale them down. You also know that wiring up a custom scaler, a KEDA setup, or a managed FaaS layer just to handle idle workloads is its own operational surface area to maintain.&lt;/p&gt;
&lt;p id="ba7f"&gt;1.36 introduces alpha support for HPA scale-to-zero for Object and External metrics. &lt;code&gt;minReplicas: 0&lt;/code&gt; is now a real configuration, not a validation error.&lt;/p&gt;
&lt;pre&gt;&lt;span id="7fe9"&gt;&lt;span&gt;spec:&lt;/span&gt;&lt;br&gt;  &lt;span&gt;minReplicas:&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;br&gt;  &lt;span&gt;maxReplicas:&lt;/span&gt; &lt;span&gt;50&lt;/span&gt;&lt;br&gt;  &lt;span&gt;metrics:&lt;/span&gt;&lt;br&gt;    &lt;span&gt;-&lt;/span&gt; &lt;span&gt;type:&lt;/span&gt; &lt;span&gt;External&lt;/span&gt;&lt;br&gt;      &lt;span&gt;external:&lt;/span&gt;&lt;br&gt;        &lt;span&gt;metric:&lt;/span&gt;&lt;br&gt;          &lt;span&gt;name:&lt;/span&gt; &lt;span&gt;sqs_queue_length&lt;/span&gt;&lt;br&gt;        &lt;span&gt;target:&lt;/span&gt;&lt;br&gt;          &lt;span&gt;type:&lt;/span&gt; &lt;span&gt;AverageValue&lt;/span&gt;&lt;br&gt;          &lt;span&gt;value:&lt;/span&gt; &lt;span&gt;10&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="2a16"&gt;Queue hits zero, pods go to zero. Message arrives, HPA spins them back up. Combine with Karpenter and the node drains too. That’s native scale-to-zero serverless architecture without handing control to a managed FaaS platform or bolting on a separate autoscaling tool.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2AFX8jnbwBuoDgIuJqf_VoeA.jpeg"&gt;&lt;p id="7843"&gt;It’s alpha, so you need to enable the &lt;code&gt;HPAScaleToZero&lt;/code&gt; feature gate explicitly. More importantly audit your existing HPAs. If you're not explicitly setting &lt;code&gt;minReplicas: 1&lt;/code&gt; somewhere, your workloads may now behave differently than you expect. That's the kind of silent change that shows up in a production incident, not a code review.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="2e70"&gt;&lt;strong&gt;SSH-ing into nodes to check logs was always shameful&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="9500"&gt;You know the sequence. Something’s wrong on a node. You find the bastion host. You SSH in. You navigate to the worker node. You run &lt;code&gt;journalctl -u kubelet&lt;/code&gt; and scroll through walls of output trying to find the one line that explains why your pods aren't scheduling. Meanwhile the incident is live and your team is waiting.&lt;/p&gt;
&lt;p id="a00c"&gt;Every engineer who’s run Kubernetes in production has done this at least once. Most have done it more times than they’d like to admit.&lt;/p&gt;
&lt;p id="d6b6"&gt;Node log queries hit GA in 1.36. With &lt;code&gt;NodeLogQuery&lt;/code&gt; enabled on the kubelet and &lt;code&gt;enableSystemLogQuery&lt;/code&gt; set in your kubelet config, you can query node-level logs kubelet logs, system service logs directly through kubectl. No SSH. No bastion. No explaining to your security team why you needed direct node access during an incident.&lt;/p&gt;
&lt;pre&gt;&lt;span id="8f63"&gt;kubectl get --raw &lt;span&gt;"/api/v1/nodes/&amp;lt;node-name&amp;gt;/proxy/logs/kubelet"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="f794"&gt;It’s not glamorous. It’s not the kind of feature that gets a conference talk. But the number of minutes lost per engineer per year to that SSH chain in production is genuinely non-trivial, and now it’s gone.&lt;/p&gt;
&lt;p id="11a7"&gt;This was SIG Windows work through &lt;a href="https://github.com/kubernetes/enhancements/issues/2258" rel="noopener ugc nofollow noreferrer"&gt;KEP-2258&lt;/a&gt;, which also means Windows nodes get full parity here something that’s historically lagged behind Linux in the observability tooling space.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="4a14"&gt;&lt;strong&gt;The kubelet was handing out too much access. Fixed.&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="4688"&gt;The kubelet exposes a gRPC API that monitoring agents, device plugins, and observability tools use to query what’s running on a node which pods are scheduled, what hardware resources they’ve been allocated, container states. Useful stuff. Stuff that a lot of tools legitimately need.&lt;/p&gt;
&lt;p id="22d4"&gt;The problem was access granularity. Getting a monitoring tool the access it needed often meant granting broad kubelet permissions. In regulated environments PCI-DSS, FedRAMP, SOC 2 that’s not a risk you can quietly accept. It’s a finding waiting to happen.&lt;/p&gt;
&lt;p id="27ce"&gt;Fine-grained kubelet API authorization hits GA in 1.36. Tools get exactly the permissions they need, scoped to the specific API surfaces they actually call. Nothing broader. The least-privilege model that should’ve been there from the start is now the stable, production-ready default.&lt;/p&gt;
&lt;p id="a89c"&gt;External ServiceAccount token signing also graduates to GA in this release. If your compliance framework requires key management outside Kubernetes’ default signing setup or you’re running in an environment where the control plane’s signing keys need to live in an external KMS this gives you a native, stable path to that without third-party workarounds.&lt;/p&gt;
&lt;p id="c425"&gt;Neither of these features will show up in a demo. Nobody’s going to tweet about kubelet auth granularity. But for teams running Kubernetes under any kind of compliance requirement, these two graduating to stable quietly removes two items from the audit findings list that have been sitting there for a while.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="9808"&gt;&lt;strong&gt;Your PVCs now tell you when they were last used&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="b888"&gt;Orphaned PersistentVolumeClaims are one of those slow, invisible cost drivers that nobody notices until someone pulls up the storage bill and starts asking uncomfortable questions. PVC gets created, workload gets deleted or redeployed, claim sits there bound and unused, and the underlying disk keeps billing. Multiply that across a busy cluster over a few months and it adds up faster than you’d think.&lt;/p&gt;
&lt;p id="40e7"&gt;Before 1.36, identifying idle PVCs meant either building a custom controller, running a third-party cleanup tool, or doing periodic manual audits none of which are things anyone actually wants to own.&lt;/p&gt;
&lt;p id="03f0"&gt;1.36 adds an &lt;code&gt;unusedSince&lt;/code&gt; timestamp field to &lt;code&gt;PersistentVolumeClaimStatus&lt;/code&gt;. The PVC protection controller now stamps it when the last pod referencing that claim is deleted or hits a terminal state. When a new pod mounts it again, the field clears back to nil.&lt;/p&gt;
&lt;pre&gt;&lt;span id="d72f"&gt;&lt;span&gt;status:&lt;/span&gt;&lt;br&gt;  &lt;span&gt;phase:&lt;/span&gt; &lt;span&gt;Bound&lt;/span&gt;&lt;br&gt;  &lt;span&gt;unusedSince:&lt;/span&gt; &lt;span&gt;"2026-03-10T14:22:00Z"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p id="9d0b"&gt;Two states. Either it has a timestamp idle since that moment or it’s nil, meaning it’s currently mounted or has never been used. Simple, native, queryable through standard kubectl and the API.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;img alt="" width="800" height="436" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A945%2F1%2AZKoZD3O3wwY1r-tnVR8XUw.jpeg"&gt;&lt;p id="91bd"&gt;It’s alpha, so it’s not on by default yet. But the pattern it enables list all PVCs where &lt;code&gt;unusedSince&lt;/code&gt; is older than 30 days, review, clean up is something teams have been building custom tooling to approximate for years. Now it's just a field.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="46b6"&gt;&lt;strong&gt;The gitRepo volume: eight years after deprecation, finally gone&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="9566"&gt;Deprecated in v1.11. June 2018. That’s not a typo.&lt;/p&gt;
&lt;p id="75b2"&gt;For eight years the gitRepo volume type lived in the Kubernetes codebase like a haunted house nobody wanted to deal with. It let you populate a volume directly from a Git repository at pod startup which sounds convenient until you think about what that actually means. Arbitrary git clone operations running with pod-level permissions, no real sandboxing, a well-documented attack surface. It was deprecated almost immediately after people understood the implications.&lt;/p&gt;
&lt;p id="1a53"&gt;And yet there it sat. Release after release. Deprecation notice in place, removal perpetually deferred.&lt;/p&gt;
&lt;p id="4526"&gt;1.36 finally pulls it out. If you’re somehow still using gitRepo volumes in 2026, this is the migration you should have done in 2019. Init containers with a proper git clone step, or a CI/CD pipeline that bakes artifacts into the image, are both better in every dimension.&lt;/p&gt;
&lt;p id="a858"&gt;Also worth flagging in the same breath: &lt;code&gt;externalIPs&lt;/code&gt; in the Service spec is deprecated in 1.36, with full removal planned for v1.43. It's been a known attack vector since &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2020-8554" rel="noopener ugc nofollow noreferrer"&gt;CVE-2020-8554&lt;/a&gt;. If it's in your configs, start the conversation now rather than doing it in a panic seven releases from now.&lt;/p&gt;
&lt;p id="8147"&gt;The broader signal here is worth noting. Kubernetes is enforcing its own deprecation timeline now, not just suggesting it. That’s good for the project’s long-term hygiene and a sign the maintainers are serious about not carrying dead weight forever.&lt;/p&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="222d"&gt;&lt;strong&gt;Haru didn’t come to play&lt;/strong&gt;&lt;/h2&gt;
&lt;p id="b707"&gt;There’s a version of this release that gets written off as a maintenance drop. No dramatic new directions, no paradigm-shifting alpha features with a flashy demo. Just a lot of things graduating, a lot of debt getting cleared, and a handful of quiet improvements that compound over time.&lt;/p&gt;
&lt;p id="cc75"&gt;That reading misses the point entirely.&lt;/p&gt;
&lt;p id="7017"&gt;1.36 is the release where Kubernetes starts filling its own gaps instead of expecting you to build scaffolding around them. Admission webhooks were a workaround that became an institution MAPs replace the institution. Container root isolation required third-party tooling for years User Namespaces makes it native. GPU scheduling was a patchwork of vendor plugins and custom controllers DRA gives it a real foundation. ML model distribution was genuinely embarrassing OCI volumes fix the architecture.&lt;/p&gt;
&lt;p id="e1a4"&gt;None of these are new ideas. All of them are ideas that finally graduated from “you can kind of do this if you squint” to “this is stable and production-ready.”&lt;/p&gt;
&lt;p id="4906"&gt;The Ingress NGINX retirement and the gitRepo removal are the most underrated signals in the release. They tell you the project is serious about what Kubernetes should and shouldn’t be and serious about not carrying CVE-adjacent code forever because migration is inconvenient.&lt;/p&gt;
&lt;p id="4645"&gt;DRA’s trajectory over the next two or three releases will define how Kubernetes handles the AI infrastructure wave. The bones being laid in 1.36 matter more than they look.&lt;/p&gt;
&lt;p id="ce68"&gt;Haru means spring. New season, cleared skies, distant horizon. The release name was earned.&lt;/p&gt;
&lt;blockquote&gt;&lt;p id="7dfd"&gt;Which of these 10 changes hits closest to home for your cluster? Drop it in the comments.&lt;/p&gt;&lt;/blockquote&gt;
&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;h2 id="c854"&gt;&lt;strong&gt;Helpful resources&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;

&lt;li id="05ed"&gt;&lt;a href="https://kubernetes.io/blog/2026/04/22/kubernetes-v1-36-release/" rel="noopener ugc nofollow noreferrer"&gt;Kubernetes v1.36 official release blog&lt;/a&gt;&lt;/li&gt;

&lt;li id="5ca5"&gt;&lt;a href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-api-machinery/3962-mutating-admission-policies/README.md" rel="noopener ugc nofollow noreferrer"&gt;KEP-3962: MutatingAdmissionPolicies&lt;/a&gt;&lt;/li&gt;

&lt;li id="c86d"&gt;&lt;a href="https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/127-user-namespaces" rel="noopener ugc nofollow noreferrer"&gt;KEP-127: User Namespaces&lt;/a&gt;&lt;/li&gt;

&lt;li id="f02e"&gt;&lt;a href="https://gateway-api.sigs.k8s.io/" rel="noopener ugc nofollow noreferrer"&gt;Gateway API v1.5 docs&lt;/a&gt;&lt;/li&gt;

&lt;li id="de04"&gt;&lt;a href="https://github.com/kubernetes-sigs/ingress2gateway" rel="noopener ugc nofollow noreferrer"&gt;Ingress2Gateway migration tool&lt;/a&gt;&lt;/li&gt;

&lt;li id="110c"&gt;&lt;a href="https://www.cloudraft.io/blog/kubernetes-v1-36-haru-features-upgrade-guide" rel="noopener ugc nofollow noreferrer"&gt;Cloudraft v1.36 upgrade guide&lt;/a&gt;&lt;/li&gt;

&lt;li id="35b9"&gt;&lt;a href="https://palark.com/blog/kubernetes-1-36-release-features/" rel="noopener ugc nofollow noreferrer"&gt;Palark DRA deep dive&lt;/a&gt;&lt;/li&gt;

&lt;li id="f8e5"&gt;&lt;a href="https://diginomica.com/kubernetes-v136-haru-security-gpus-and-observability-grow" rel="noopener ugc nofollow noreferrer"&gt;Diginomica interview with Release Lead Ryota Sawada&lt;/a&gt;&lt;/li&gt;

&lt;li id="5e1b"&gt;&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2020-8554" rel="noopener ugc nofollow noreferrer"&gt;CVE-2020–8554 — externalIPs attack vector&lt;/a&gt;&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>kubernetes</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
