<?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: Lars Winstand</title>
    <description>The latest articles on DEV Community by Lars Winstand (@lars_winstand).</description>
    <link>https://dev.to/lars_winstand</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3908932%2Feb8bc1ff-405f-4ef0-8204-ba1ed7caa59f.jpeg</url>
      <title>DEV Community: Lars Winstand</title>
      <link>https://dev.to/lars_winstand</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lars_winstand"/>
    <language>en</language>
    <item>
      <title>A Claude Max billing error is what finally made me stop running agents on chat subscriptions</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Thu, 15 Oct 2026 06:10:48 +0000</pubDate>
      <link>https://dev.to/lars_winstand/a-claude-max-billing-error-is-what-finally-made-me-stop-running-agents-on-chat-subscriptions-e1</link>
      <guid>https://dev.to/lars_winstand/a-claude-max-billing-error-is-what-finally-made-me-stop-running-agents-on-chat-subscriptions-e1</guid>
      <description>&lt;p&gt;I didn’t stop using Claude because Claude was bad.&lt;/p&gt;

&lt;p&gt;I stopped putting agent workloads on consumer chat plans because I got tired of pretending a chat subscription was infrastructure.&lt;/p&gt;

&lt;p&gt;That distinction matters a lot more than people admit.&lt;/p&gt;

&lt;p&gt;For interactive work, Claude Pro and Claude Max feel great. You pay a monthly fee, stop obsessing over token counts, open Claude.ai, and get work done. For solo thinking, writing, debugging, and one-off coding sessions, that experience is genuinely good.&lt;/p&gt;

&lt;p&gt;But once you move from "help me think" to "do this job while I sleep," the whole setup starts to feel shaky.&lt;/p&gt;

&lt;p&gt;A billing edge case was the thing that finally made me admit it.&lt;/p&gt;

&lt;p&gt;Not a dramatic outage. Not a benchmark. Not a model-quality debate.&lt;/p&gt;

&lt;p&gt;A billing problem.&lt;/p&gt;

&lt;p&gt;And that was enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem: annoyances become operational risks when you automate them
&lt;/h2&gt;

&lt;p&gt;A weird usage pause in a browser tab is annoying.&lt;/p&gt;

&lt;p&gt;A weird usage pause in an unattended workflow is a production problem.&lt;/p&gt;

&lt;p&gt;A vague limit message is survivable when you can click retry.&lt;/p&gt;

&lt;p&gt;It’s not survivable when an n8n workflow is halfway through 600 records and nobody is watching.&lt;/p&gt;

&lt;p&gt;Some account-policy weirdness is tolerable when you’re brainstorming.&lt;/p&gt;

&lt;p&gt;It’s unacceptable when OpenClaw or a Python worker is chaining tasks overnight.&lt;/p&gt;

&lt;p&gt;That’s the whole argument:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;consumer chat plans are optimized for human tolerance; agent systems need machine-readable failure modes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once I looked at it that way, the rest got obvious fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anthropic already tells you where serious workloads belong
&lt;/h2&gt;

&lt;p&gt;This part is hiding in plain sight.&lt;/p&gt;

&lt;p&gt;Anthropic splits its products into different buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude.ai / Claude Pro / Claude Max&lt;/strong&gt; under consumer-style usage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic API / Claude Console&lt;/strong&gt; under commercial developer usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That product split is the tell.&lt;/p&gt;

&lt;p&gt;You can argue about the philosophy, but the implementation is clear: if you are powering software, automations, workflows, or user-facing systems, the API side is the real substrate.&lt;/p&gt;

&lt;p&gt;The consumer side is for humans using a chat app.&lt;/p&gt;

&lt;p&gt;A lot of builders blur that line because modern chat apps are very, very good. Claude.ai is polished. ChatGPT is polished. They make it easy to forget that convenience and operational fitness are not the same thing.&lt;/p&gt;

&lt;p&gt;They are not interchangeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Claude Max actually gives you
&lt;/h2&gt;

&lt;p&gt;Claude Max is attractive because it feels flat-rate.&lt;/p&gt;

&lt;p&gt;That’s the hook.&lt;/p&gt;

&lt;p&gt;But the language matters. It’s sold as giving you something like &lt;strong&gt;5x&lt;/strong&gt; or &lt;strong&gt;20x&lt;/strong&gt; more usage than Pro, not a deterministic compute budget.&lt;/p&gt;

&lt;p&gt;For an interactive human, that can be fine.&lt;/p&gt;

&lt;p&gt;For an agent, that’s a red flag.&lt;/p&gt;

&lt;p&gt;Anthropic’s own docs say availability can vary based on things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;message length&lt;/li&gt;
&lt;li&gt;file size&lt;/li&gt;
&lt;li&gt;conversation length&lt;/li&gt;
&lt;li&gt;tool use&lt;/li&gt;
&lt;li&gt;model choice&lt;/li&gt;
&lt;li&gt;effort level&lt;/li&gt;
&lt;li&gt;artifact creation&lt;/li&gt;
&lt;li&gt;multi-step tasks like browsing or code execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That list is basically a description of what happens as agents become more useful.&lt;/p&gt;

&lt;p&gt;The more agentic your system gets, the less predictable your effective capacity becomes.&lt;/p&gt;

&lt;p&gt;That is backwards for operations.&lt;/p&gt;

&lt;p&gt;Operators want boring systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit limits&lt;/li&gt;
&lt;li&gt;known caps&lt;/li&gt;
&lt;li&gt;documented retries&lt;/li&gt;
&lt;li&gt;clear failure modes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A human can work around fuzzy capacity. A workflow cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why API errors are better than chat weirdness
&lt;/h2&gt;

&lt;p&gt;This was the thing I resisted for too long.&lt;/p&gt;

&lt;p&gt;Metered APIs feel scarier at first because the costs are explicit.&lt;/p&gt;

&lt;p&gt;But for unattended systems, explicit beats ambiguous every time.&lt;/p&gt;

&lt;p&gt;APIs fail in ways machines can understand.&lt;/p&gt;

&lt;p&gt;That means you can build real control loops.&lt;/p&gt;

&lt;p&gt;Typical API-side failures look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;402&lt;/strong&gt; for billing issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;429&lt;/strong&gt; for rate limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5xx / overload&lt;/strong&gt; for transient service problems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not nuisances. They are features.&lt;/p&gt;

&lt;p&gt;If n8n gets a 429, you back off and retry.&lt;/p&gt;

&lt;p&gt;If your worker gets a 5xx, you queue and retry.&lt;/p&gt;

&lt;p&gt;If you hit a spend cap, you alert, fail over, or intentionally pause.&lt;/p&gt;

&lt;p&gt;That is actual operations.&lt;/p&gt;

&lt;p&gt;Here’s a minimal Python example with retries around a chat-completions-style API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_with_retry&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a precise coding assistant.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize this deployment log and flag failures.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That kind of code is boring.&lt;/p&gt;

&lt;p&gt;Boring is good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Billing systems are part of your production stack
&lt;/h2&gt;

&lt;p&gt;This is the lesson I think most AI builders learn late.&lt;/p&gt;

&lt;p&gt;People obsess over model quality and ignore billing behavior.&lt;/p&gt;

&lt;p&gt;That’s a mistake.&lt;/p&gt;

&lt;p&gt;If billing can stop requests, billing is part of your runtime.&lt;/p&gt;

&lt;p&gt;If usage enforcement can pause your workload, usage enforcement is part of your runtime.&lt;/p&gt;

&lt;p&gt;If account state can break automation, account state is part of your runtime.&lt;/p&gt;

&lt;p&gt;Once you accept that, consumer chat plans look a lot less appealing for agent execution.&lt;/p&gt;

&lt;p&gt;Because they’re not designed to be operated like infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost story gets weird as soon as agents get busy
&lt;/h2&gt;

&lt;p&gt;A lot of people move to chat subscriptions because they hate token anxiety.&lt;/p&gt;

&lt;p&gt;Fair. I hate it too.&lt;/p&gt;

&lt;p&gt;But hiding the meter is not the same as solving the economics.&lt;/p&gt;

&lt;p&gt;Once your workflows get heavier, the complexity comes back somewhere else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;long context windows&lt;/li&gt;
&lt;li&gt;tool calls&lt;/li&gt;
&lt;li&gt;multi-step chains&lt;/li&gt;
&lt;li&gt;output-heavy transforms&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;fan-out jobs&lt;/li&gt;
&lt;li&gt;overnight runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, “flat-feeling” usage doesn’t remove operational complexity. It just makes it harder to reason about.&lt;/p&gt;

&lt;p&gt;Here’s the practical comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What you’re really buying&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Max&lt;/td&gt;
&lt;td&gt;A premium consumer chat experience with variable effective capacity, great for interactive use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic API&lt;/td&gt;
&lt;td&gt;Metered programmable access with documented limits, spend controls, and machine-readable errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI-compatible flat-rate API layer&lt;/td&gt;
&lt;td&gt;Predictable monthly pricing for agents and automations, with API semantics developers can actually build around&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That third category is where this got interesting for me.&lt;/p&gt;

&lt;p&gt;Because the real problem wasn’t "Claude vs OpenAI" or "which model is smartest."&lt;/p&gt;

&lt;p&gt;The real problem was: &lt;strong&gt;how do I run agents without babysitting token spend and without trusting a browser-first product to behave like infrastructure?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration path is easier than people think
&lt;/h2&gt;

&lt;p&gt;If you’re still doing most of your work in chat apps, moving to API-driven workflows is less painful than it sounds.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. If you already use the OpenAI SDK
&lt;/h3&gt;

&lt;p&gt;You can usually keep your client code shape and swap the base URL.&lt;/p&gt;

&lt;p&gt;That’s the easiest off-ramp from browser habits.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STANDARD_COMPUTE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.standardcompute.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a senior SRE.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Classify these incidents by severity and probable root cause.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the appeal of an OpenAI-compatible API layer: minimal rewiring.&lt;/p&gt;

&lt;p&gt;If your stack already speaks OpenAI, you don’t want a migration project. You want a config change.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. If you live in n8n
&lt;/h3&gt;

&lt;p&gt;This is where chat subscriptions really fall apart.&lt;/p&gt;

&lt;p&gt;n8n wants explicit request/response behavior. It wants credentials, retries, branching, and observability.&lt;/p&gt;

&lt;p&gt;That maps naturally to APIs.&lt;/p&gt;

&lt;p&gt;A practical pattern in n8n looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Trigger on new records&lt;/li&gt;
&lt;li&gt;Batch items&lt;/li&gt;
&lt;li&gt;Call an OpenAI-compatible chat endpoint&lt;/li&gt;
&lt;li&gt;On 429/5xx, retry with backoff&lt;/li&gt;
&lt;li&gt;On billing or auth failure, route to Slack/email alert&lt;/li&gt;
&lt;li&gt;Continue processing the rest of the queue&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s a workflow.&lt;/p&gt;

&lt;p&gt;A browser subscription can’t give you that shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. If you’re using CLI agents or harnesses like OpenClaw
&lt;/h3&gt;

&lt;p&gt;Tools like OpenClaw make it very easy to build heavier autonomous workflows.&lt;/p&gt;

&lt;p&gt;That’s great, but it also means you hit infrastructure limits faster.&lt;/p&gt;

&lt;p&gt;Quick-start tooling makes experimentation easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://openclaw.ai/install.sh | bash
npm i &lt;span class="nt"&gt;-g&lt;/span&gt; openclaw
openclaw onboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trap is assuming the thing that felt fine in interactive testing will hold up under unattended execution.&lt;/p&gt;

&lt;p&gt;Usually, it won’t.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I use instead for automations
&lt;/h2&gt;

&lt;p&gt;My current opinion is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;strong&gt;Claude Pro&lt;/strong&gt; or &lt;strong&gt;Claude Max&lt;/strong&gt; for interactive work&lt;/li&gt;
&lt;li&gt;use &lt;strong&gt;APIs&lt;/strong&gt; for anything unattended&lt;/li&gt;
&lt;li&gt;use an &lt;strong&gt;OpenAI-compatible layer&lt;/strong&gt; if you want portability and less migration pain&lt;/li&gt;
&lt;li&gt;use &lt;strong&gt;flat monthly pricing&lt;/strong&gt; if your biggest operational headache is token anxiety&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is why products like &lt;strong&gt;Standard Compute&lt;/strong&gt; make sense for a certain class of builder.&lt;/p&gt;

&lt;p&gt;If you’re running agents in &lt;strong&gt;n8n&lt;/strong&gt;, &lt;strong&gt;Make&lt;/strong&gt;, &lt;strong&gt;Zapier&lt;/strong&gt;, &lt;strong&gt;OpenClaw&lt;/strong&gt;, or custom workers, the painful part is not just model access. It’s cost unpredictability.&lt;/p&gt;

&lt;p&gt;Standard Compute’s pitch is basically the thing I wish more vendors admitted people want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;predictable monthly pricing&lt;/li&gt;
&lt;li&gt;no per-token billing drama&lt;/li&gt;
&lt;li&gt;OpenAI-compatible API calls&lt;/li&gt;
&lt;li&gt;support for automations and agent workloads&lt;/li&gt;
&lt;li&gt;routing across models without forcing you to rebuild your stack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a much better fit for production-ish automations than pretending a chat subscription is your compute layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete dev setup
&lt;/h2&gt;

&lt;p&gt;If I were setting this up from scratch for a small automation stack, I’d do something like this:&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_standard_compute_key"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://api.standardcompute.com/v1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  App code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Extract action items and owners.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Meeting notes: Alice owns the migration. Bob will review the API logs.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Operational rules
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;retry 429 and 5xx&lt;/li&gt;
&lt;li&gt;alert on 401/402-style failures&lt;/li&gt;
&lt;li&gt;cap concurrency at the workflow layer&lt;/li&gt;
&lt;li&gt;log request IDs and job IDs together&lt;/li&gt;
&lt;li&gt;keep prompts versioned in git&lt;/li&gt;
&lt;li&gt;test with small batches before overnight runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is dramatically more trustworthy than “hopefully the chat plan still likes me at 2:13 AM.”&lt;/p&gt;

&lt;h2&gt;
  
  
  My actual takeaway
&lt;/h2&gt;

&lt;p&gt;The weird lesson here is that &lt;strong&gt;predictability matters more than generosity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I would rather have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a clear spend cap than a vague promise of more usage&lt;/li&gt;
&lt;li&gt;a hard billing error than a silent stop&lt;/li&gt;
&lt;li&gt;a documented rate limit than fuzzy throttling&lt;/li&gt;
&lt;li&gt;an API I can swap underneath my workflows than a chat UI I have to work around&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chat subscriptions optimize for the feeling of abundance.&lt;/p&gt;

&lt;p&gt;Operators need the reality of control.&lt;/p&gt;

&lt;p&gt;So yeah: a Claude Max billing error is what finally got me to stop running agents on chat subscriptions.&lt;/p&gt;

&lt;p&gt;Not because Claude is bad.&lt;/p&gt;

&lt;p&gt;Because once an agent touches production data, runs overnight, or powers automations in n8n, Make, Zapier, OpenClaw, or your own workers, billing, limits, retries, and compatibility stop being side details.&lt;/p&gt;

&lt;p&gt;They become the whole game.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>automation</category>
      <category>api</category>
    </item>
    <item>
      <title>I finally cut AI context bloat by 80% and the fix was embarrassingly simple</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:12:07 +0000</pubDate>
      <link>https://dev.to/lars_winstand/i-finally-cut-ai-context-bloat-by-80-and-the-fix-was-embarrassingly-simple-1mlp</link>
      <guid>https://dev.to/lars_winstand/i-finally-cut-ai-context-bloat-by-80-and-the-fix-was-embarrassingly-simple-1mlp</guid>
      <description>&lt;p&gt;A few months ago I watched an agent do something deeply cursed.&lt;/p&gt;

&lt;p&gt;It had already fetched the data.&lt;br&gt;
It had already parsed the data.&lt;br&gt;
It had already stored the useful fields.&lt;/p&gt;

&lt;p&gt;And then it pasted the entire raw tool response into the next model call anyway.&lt;/p&gt;

&lt;p&gt;Full JSON blobs. Logs. Metadata. Useless fields. Everything.&lt;/p&gt;

&lt;p&gt;The next step got slower.&lt;br&gt;
The step after that got more expensive.&lt;br&gt;
Then the model started doing that annoying thing where it answers the easy part of the task and quietly ignores the important part.&lt;/p&gt;

&lt;p&gt;That was the moment the bug became obvious:&lt;/p&gt;

&lt;p&gt;Most “context window problems” are not model problems.&lt;br&gt;
They are state management problems.&lt;/p&gt;

&lt;p&gt;Once you see that, you start spotting it everywhere in n8n flows, LangGraph agents, MCP tool loops, and custom OpenAI-compatible orchestration code.&lt;/p&gt;
&lt;h2&gt;
  
  
  The actual fix
&lt;/h2&gt;

&lt;p&gt;Stop replaying raw tool output into every prompt.&lt;/p&gt;

&lt;p&gt;Keep full results in structured state outside the model.&lt;br&gt;
Send only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a short summary&lt;/li&gt;
&lt;li&gt;the exact fields needed for the next step&lt;/li&gt;
&lt;li&gt;or the exact evidence slice if verification matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That one change usually does more for agent quality than switching from one frontier model to another.&lt;/p&gt;
&lt;h2&gt;
  
  
  The anti-pattern
&lt;/h2&gt;

&lt;p&gt;Here’s the bad version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;weather_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_forecast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Austin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use this API response to decide whether to send an alert:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;result&lt;/code&gt; contains 200 lines of hourly forecast data, provider metadata, timestamps, debug fields, and trace IDs, you just turned your prompt into a junk drawer.&lt;/p&gt;

&lt;p&gt;Now the model has to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;find the relevant fields&lt;/li&gt;
&lt;li&gt;ignore the irrelevant ones&lt;/li&gt;
&lt;li&gt;remember prior steps&lt;/li&gt;
&lt;li&gt;plan the next action&lt;/li&gt;
&lt;li&gt;not get distracted by noise&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Models can do this.&lt;br&gt;
They are just not especially efficient at it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The better pattern
&lt;/h2&gt;

&lt;p&gt;Store the full result in app state.&lt;br&gt;
Pass only the fields the next step needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;weather_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_forecast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Austin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weather_raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

&lt;span class="n"&gt;weather_summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature_f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temp_f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conditions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;condition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wind_alert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alerts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Decide whether to send an alert using this data: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;weather_summary&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same workflow.&lt;br&gt;
Smaller prompt.&lt;br&gt;
Less distraction.&lt;br&gt;
Better odds the model focuses on the decision instead of re-parsing junk.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prompt size is not the same as prompt quality
&lt;/h2&gt;

&lt;p&gt;A lot of builders confuse these two ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the model can fit a long context&lt;/li&gt;
&lt;li&gt;the model should receive a long context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not the same.&lt;/p&gt;

&lt;p&gt;A giant prompt is often just a badly managed state store wearing a trench coat.&lt;/p&gt;

&lt;p&gt;Even when GPT-5, Claude Opus, Grok, Qwen, or Llama can technically accept the full history, performance still gets mushier as stale content piles up. Latency rises. Cost rises. Attention gets diffused.&lt;/p&gt;

&lt;p&gt;This is why long-running agents often feel smart for 5 steps and weirdly flaky by step 20.&lt;/p&gt;
&lt;h2&gt;
  
  
  The 3 patterns, ranked
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What happens in practice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw tool output in prompt&lt;/td&gt;
&lt;td&gt;Biggest prompt growth. Highest distraction. Latency and cost climb every step.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Summarized tool result in prompt&lt;/td&gt;
&lt;td&gt;Usually the best balance. Smaller prompt footprint, but bad summaries can hide edge cases.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured state outside prompt&lt;/td&gt;
&lt;td&gt;Best default for multi-step agents. Small recurring prompts, cleaner reasoning, better orchestration.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My opinionated take: structured state outside the prompt should be the default.&lt;/p&gt;

&lt;p&gt;If you are building anything more serious than a toy bot, replaying raw tool output into every step is lazy architecture.&lt;/p&gt;
&lt;h2&gt;
  
  
  What should live where?
&lt;/h2&gt;

&lt;p&gt;This is the mental model I wish more agent builders started with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Short-term conversational state stays attached to the thread.&lt;/li&gt;
&lt;li&gt;Long-term facts and tool results live outside the prompt in a database or memory store.&lt;/li&gt;
&lt;li&gt;The next model call gets only the smallest useful slice of that state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;Not complicated. Just disciplined.&lt;/p&gt;
&lt;h2&gt;
  
  
  Real examples
&lt;/h2&gt;
&lt;h3&gt;
  
  
  LangGraph: checkpoint state, don’t replay transcripts
&lt;/h3&gt;

&lt;p&gt;LangGraph gets this right.&lt;/p&gt;

&lt;p&gt;Instead of stuffing previous tool output back into every prompt, persist graph state and resume by thread.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.checkpoint.postgres&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PostgresSaver&lt;/span&gt;

&lt;span class="n"&gt;DB_URI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;PostgresSaver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_conn_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DB_URI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the grown-up version of memory.&lt;/p&gt;

&lt;p&gt;The model gets what the next node needs.&lt;br&gt;
Postgres keeps the rest.&lt;/p&gt;
&lt;h3&gt;
  
  
  n8n: trim memory on purpose
&lt;/h3&gt;

&lt;p&gt;n8n is refreshingly practical here.&lt;/p&gt;

&lt;p&gt;If you are building AI workflows in n8n, you already have the tools to avoid context bloat:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple Memory&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;Postgres&lt;/li&gt;
&lt;li&gt;Xata&lt;/li&gt;
&lt;li&gt;Zep&lt;/li&gt;
&lt;li&gt;Motorhead&lt;/li&gt;
&lt;li&gt;Chat Memory Manager&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The existence of Chat Memory Manager should tell you everything.&lt;/p&gt;

&lt;p&gt;If a workflow platform ships a node specifically to inspect and reduce memory, then “just append to chat history forever” is obviously not the best pattern.&lt;/p&gt;
&lt;h3&gt;
  
  
  MCP: tool output is app state first, prompt material second
&lt;/h3&gt;

&lt;p&gt;MCP makes this cleaner because tools already have structure.&lt;/p&gt;

&lt;p&gt;Example tool definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get_weather"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Get current weather information for a location"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That should push you toward a simple pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;call the tool&lt;/li&gt;
&lt;li&gt;store the full structured result&lt;/li&gt;
&lt;li&gt;pass only the needed fields into the next prompt&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tool_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;72F&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conditions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wind_alert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hourly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider_metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{...},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trace_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abc123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weather_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_result&lt;/span&gt;

&lt;span class="n"&gt;prompt_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conditions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conditions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wind_alert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wind_alert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt becomes a working note, not a warehouse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt caching helps, but it does not fix bad context hygiene
&lt;/h2&gt;

&lt;p&gt;This part gets misunderstood a lot.&lt;/p&gt;

&lt;p&gt;Anthropic prompt caching is useful when you keep reusing the same stable prefix.&lt;/p&gt;

&lt;p&gt;That means things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;system instructions&lt;/li&gt;
&lt;li&gt;tool definitions&lt;/li&gt;
&lt;li&gt;fixed documents&lt;/li&gt;
&lt;li&gt;repeated scaffolding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not magically make a sloppy prompt good.&lt;/p&gt;

&lt;p&gt;Claude caching only helps when the prefix is identical, and cache entries last 5 minutes by default. That’s great for repeated prefixes. It does nothing to make a giant raw tool dump more relevant.&lt;/p&gt;

&lt;p&gt;If anything, caching can hide bad architecture for a while because the bill hurts less before quality problems become obvious.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-5-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cache_control&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ephemeral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are an AI assistant tasked with analyzing literary works.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Analyze the major themes in Pride and Prejudice.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use caching for stable prefixes.&lt;br&gt;
Do not use it as an excuse to keep shoving junk into prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick before/after refactor
&lt;/h2&gt;

&lt;p&gt;Here’s what this looks like in a normal agent loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;search_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search_docs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;crm_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_customer_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    User request: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_request&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

    Search results:
    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;search_results&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

    CRM data:
    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;crm_data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

    Decide what to do next.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works at first.&lt;br&gt;
Then &lt;code&gt;search_results&lt;/code&gt; gets huge.&lt;br&gt;
Then &lt;code&gt;crm_data&lt;/code&gt; gets huge.&lt;br&gt;
Then every step drags around more baggage.&lt;/p&gt;

&lt;h3&gt;
  
  
  After
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;search_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search_docs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;crm_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_customer_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_results_raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;search_results&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;crm_data_raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crm_data&lt;/span&gt;

    &lt;span class="n"&gt;prompt_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top_matches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;search_results&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;crm_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open_tickets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;crm_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open_tickets_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;crm_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Decide what to do next using this state: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prompt_payload&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This version is less fragile.&lt;/p&gt;

&lt;p&gt;And when the model needs exact evidence later, you can pull the raw state back in selectively.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should you keep raw output in the prompt?
&lt;/h2&gt;

&lt;p&gt;There are real exceptions.&lt;/p&gt;

&lt;p&gt;Sometimes the next step genuinely needs exact source material.&lt;/p&gt;

&lt;p&gt;Keep raw output in-context when you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;legal or compliance citations&lt;/li&gt;
&lt;li&gt;exact stack traces for debugging&lt;/li&gt;
&lt;li&gt;code review on full diffs&lt;/li&gt;
&lt;li&gt;extraction from messy text&lt;/li&gt;
&lt;li&gt;verification against original evidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But even then, pass the exact slice that matters.&lt;br&gt;
Not the entire upstream universe.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I use now
&lt;/h2&gt;

&lt;p&gt;This is the heuristic that cleaned up most of my agent workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If information is needed for reasoning, summarize it.&lt;/li&gt;
&lt;li&gt;If it is needed for retrieval, store it.&lt;/li&gt;
&lt;li&gt;If it is needed for verification, pass the exact slice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That rule has saved me more latency, more cost, and more agent quality than swapping models ever did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters even more when you pay per token
&lt;/h2&gt;

&lt;p&gt;This problem gets expensive fast in production.&lt;/p&gt;

&lt;p&gt;If your agent runs 24/7 in n8n, Make, Zapier, OpenClaw, or a custom loop, every extra blob you keep replaying becomes recurring cost.&lt;/p&gt;

&lt;p&gt;That’s one reason teams end up obsessing over token usage dashboards instead of fixing the architecture.&lt;/p&gt;

&lt;p&gt;A cleaner state design reduces prompt bloat at the source.&lt;/p&gt;

&lt;p&gt;And if you are using an OpenAI-compatible endpoint with flat monthly pricing, this gets even nicer because you can optimize for agent quality and throughput instead of constantly asking whether one more tool call is going to spike the bill.&lt;/p&gt;

&lt;p&gt;That’s the part I like about Standard Compute: it works as a drop-in OpenAI API replacement, so you can keep your existing SDKs and agent code, but stop thinking in per-token panic mode while you clean up orchestration.&lt;/p&gt;

&lt;p&gt;The architecture fix still matters.&lt;br&gt;
You just get to apply it without token anxiety hanging over every experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical checklist
&lt;/h2&gt;

&lt;p&gt;If your agent feels slower and dumber over time, check these first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are you pasting raw tool output into every next prompt?&lt;/li&gt;
&lt;li&gt;Are you storing canonical results outside the model?&lt;/li&gt;
&lt;li&gt;Are you summarizing tool results before the next reasoning step?&lt;/li&gt;
&lt;li&gt;Are you passing exact evidence only when verification is required?&lt;/li&gt;
&lt;li&gt;Are you using prompt caching only for stable prefixes?&lt;/li&gt;
&lt;li&gt;Are you trimming memory in n8n or your orchestration layer?&lt;/li&gt;
&lt;li&gt;Are you treating the model like a reasoning engine instead of a database?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you fix just those, you will probably get a bigger improvement than another model bake-off.&lt;/p&gt;

&lt;p&gt;I went looking for a model fix.&lt;/p&gt;

&lt;p&gt;What I found was an architecture fix.&lt;/p&gt;

&lt;p&gt;Most “context window problems” are really just us using chat history as a storage layer.&lt;/p&gt;

&lt;p&gt;Once you stop shoving raw tool output back into the next prompt, agents get faster, cheaper, and noticeably less confused.&lt;/p&gt;

&lt;p&gt;Not because the models changed.&lt;/p&gt;

&lt;p&gt;Because we finally stopped making them carry our state management mistakes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>devops</category>
    </item>
    <item>
      <title>I thought GPT-4o got 80% faster, but it was just prompt caching messing with my benchmark</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Sun, 11 Oct 2026 06:11:15 +0000</pubDate>
      <link>https://dev.to/lars_winstand/i-thought-gpt-4o-got-80-faster-but-it-was-just-prompt-caching-messing-with-my-benchmark-3c1f</link>
      <guid>https://dev.to/lars_winstand/i-thought-gpt-4o-got-80-faster-but-it-was-just-prompt-caching-messing-with-my-benchmark-3c1f</guid>
      <description>&lt;p&gt;I got fooled by a benchmark that looked incredible.&lt;/p&gt;

&lt;p&gt;Same agent workflow. Same model. Same code path.&lt;/p&gt;

&lt;p&gt;Second run was dramatically faster.&lt;/p&gt;

&lt;p&gt;My first reaction was the same dumb little hit of engineer dopamine most of us get: nice, we optimized something.&lt;/p&gt;

&lt;p&gt;We didn’t.&lt;/p&gt;

&lt;p&gt;The prompt prefix matched, the cache kicked in, and my “model improvement” was mostly reuse.&lt;/p&gt;

&lt;p&gt;That sounds obvious after the fact. But a lot of LLM benchmark posts still miss it. People run the same agent twice, latency drops, and suddenly GPT-4o or Claude or Gemini is supposedly faster, cheaper, or more stable.&lt;/p&gt;

&lt;p&gt;Usually the benchmark just got warm.&lt;/p&gt;

&lt;p&gt;If you build agents in n8n, Make, Zapier, OpenClaw, or your own Python workers, this gets even easier to miss because so much of the request stays identical between calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark looked great right up until I checked usage
&lt;/h2&gt;

&lt;p&gt;Agent workloads are basically built to trigger prompt caching.&lt;/p&gt;

&lt;p&gt;A typical request includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a large system prompt&lt;/li&gt;
&lt;li&gt;tool schemas&lt;/li&gt;
&lt;li&gt;examples&lt;/li&gt;
&lt;li&gt;conversation history&lt;/li&gt;
&lt;li&gt;planner / critic prompts&lt;/li&gt;
&lt;li&gt;structured output schemas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user’s latest message is often the smallest part of the request.&lt;/p&gt;

&lt;p&gt;The expensive part is the scaffolding.&lt;/p&gt;

&lt;p&gt;So when the second run gets much faster, that doesn’t automatically mean the model improved. It often means the provider reused the big stable prefix.&lt;/p&gt;

&lt;p&gt;For production, that’s good.&lt;/p&gt;

&lt;p&gt;For benchmarking, it can absolutely lie to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to log if you want numbers that mean anything
&lt;/h2&gt;

&lt;p&gt;If you’re not logging cache-related usage fields, you’re guessing.&lt;/p&gt;

&lt;p&gt;Here are the ones that matter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;What to look for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Prompt Caching&lt;/td&gt;
&lt;td&gt;&lt;code&gt;usage.prompt_tokens_details.cached_tokens&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic Prompt Caching&lt;/td&gt;
&lt;td&gt;cache usage fields in the response usage object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Gemini Implicit Caching&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;usage.total_cached_tokens&lt;/code&gt; or Vertex AI &lt;code&gt;cachedContentTokenCount&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenRouter&lt;/td&gt;
&lt;td&gt;sticky routing behavior and shared &lt;code&gt;session_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OpenAI is the easiest example.&lt;/p&gt;

&lt;p&gt;Prompt caching is enabled by default on supported models. It kicks in for prompts over 1,024 tokens, and the response tells you how many prompt tokens were served from cache.&lt;/p&gt;

&lt;p&gt;Example response shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2006&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"completion_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt_tokens_details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cached_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"audio_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;cached_tokens&lt;/code&gt; goes from &lt;code&gt;0&lt;/code&gt; on run one to &lt;code&gt;1920&lt;/code&gt; on run two, you did not discover a magical optimization.&lt;/p&gt;

&lt;p&gt;You discovered that the prefix matched.&lt;/p&gt;

&lt;p&gt;That’s the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the second run also looked cheaper
&lt;/h2&gt;

&lt;p&gt;Because caching changes both latency and cost.&lt;/p&gt;

&lt;p&gt;OpenAI has been explicit about this: cached input is cheaper than uncached input on supported models.&lt;/p&gt;

&lt;p&gt;Google says implicit cache hits on Vertex AI can get major discounts too.&lt;/p&gt;

&lt;p&gt;So when someone says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Gemini 2.5 Pro got way cheaper in our pipeline&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;my first question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;what changed in the model?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;did you log &lt;code&gt;usage.total_cached_tokens&lt;/code&gt;?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is no, the pricing story is incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Python example: log the cache fields or don’t trust the benchmark
&lt;/h2&gt;

&lt;p&gt;Here’s a minimal OpenAI-compatible example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a careful code review assistant.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize this repository architecture and list the top risks.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_once&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;

    &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;
    &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens_details&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens_details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens_details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completion_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completion_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;run_once&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;run_once&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the second call is faster and &lt;code&gt;cached_tokens&lt;/code&gt; jumps, that result is warm-cache behavior.&lt;/p&gt;

&lt;p&gt;Not raw model speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you use curl, same rule
&lt;/h2&gt;

&lt;p&gt;You don’t need a full benchmark harness to catch this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.openai.com/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$OPENAI_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "gpt-4o",
    "messages": [
      {"role": "system", "content": "You are a careful code review assistant."},
      {"role": "user", "content": "Summarize this repository architecture and list the top risks."}
    ]
  }'&lt;/span&gt; | jq &lt;span class="s1"&gt;'.usage'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it twice.&lt;/p&gt;

&lt;p&gt;Check whether cached token fields appear or increase.&lt;/p&gt;

&lt;p&gt;That one step will save you from a lot of fake benchmark confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anthropic and Gemini can fool you just as easily
&lt;/h2&gt;

&lt;p&gt;This is not an OpenAI-only problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anthropic
&lt;/h3&gt;

&lt;p&gt;Anthropic supports prompt caching with &lt;code&gt;cache_control&lt;/code&gt;. Cache lifetime is usually around 5 minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cache_control&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ephemeral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are an AI assistant...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Analyze the major themes in Pride and Prejudice.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means follow-up requests can look much faster if the cacheable prefix is unchanged.&lt;/p&gt;

&lt;p&gt;There’s also an annoying timing edge case: if your streamed response takes several minutes, you can accidentally fall out of the cache window and think the benchmark is random.&lt;/p&gt;

&lt;p&gt;It isn’t random. Your timing is just sloppy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini
&lt;/h3&gt;

&lt;p&gt;Gemini 2.5+ models also have implicit caching behavior.&lt;/p&gt;

&lt;p&gt;So yes, the second call may be faster.&lt;/p&gt;

&lt;p&gt;No, that does not mean Gemini “learned” your workflow.&lt;/p&gt;

&lt;p&gt;If you’re on Vertex AI, inspect cached token fields. If you’re not logging them, you’re missing the main reason the benchmark changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenRouter can make the benchmark look cleaner than it really is
&lt;/h2&gt;

&lt;p&gt;This one matters if you benchmark through OpenRouter.&lt;/p&gt;

&lt;p&gt;OpenRouter uses sticky routing after a cached request so later requests for the same model and conversation can keep hitting the same provider-side cache.&lt;/p&gt;

&lt;p&gt;That’s useful in production.&lt;/p&gt;

&lt;p&gt;It’s bad for sloppy benchmarking because now the router is helping your benchmark stay warm.&lt;/p&gt;

&lt;p&gt;Example payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openai/gpt-4o"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Summarize this codebase"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agent-run-42"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If later calls improve, you need to ask whether the model got better or whether OpenRouter kept sending you back to the same warm provider.&lt;/p&gt;

&lt;p&gt;Those are very different explanations.&lt;/p&gt;

&lt;h2&gt;
  
  
  n8n can fake the win before the request even reaches the model
&lt;/h2&gt;

&lt;p&gt;This is the part people really hate hearing.&lt;/p&gt;

&lt;p&gt;Sometimes the model didn’t get faster.&lt;/p&gt;

&lt;p&gt;Sometimes your workflow got shorter.&lt;/p&gt;

&lt;p&gt;In n8n, nodes like Remove Duplicates can reduce how many items actually reach the LLM across runs.&lt;/p&gt;

&lt;p&gt;So the benchmark story becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;n8n filtered duplicate items&lt;/li&gt;
&lt;li&gt;fewer requests hit GPT-4o or Claude&lt;/li&gt;
&lt;li&gt;later runs looked faster and cheaper&lt;/li&gt;
&lt;li&gt;everyone credited the model&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s not model improvement.&lt;/p&gt;

&lt;p&gt;That’s upstream dedupe.&lt;/p&gt;

&lt;p&gt;The same thing can happen in Make, Zapier, or custom queues if retries, filters, or batching behavior change between runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  My benchmark rule now: separate cold from warm or don’t publish it
&lt;/h2&gt;

&lt;p&gt;This is the simplest fix.&lt;/p&gt;

&lt;p&gt;If your benchmark doesn’t separate cold and warm runs, it’s unfinished.&lt;/p&gt;

&lt;p&gt;Here’s the checklist I use now.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Log cache metrics on every request
&lt;/h3&gt;

&lt;p&gt;At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI: &lt;code&gt;usage.prompt_tokens_details.cached_tokens&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Gemini: &lt;code&gt;usage.total_cached_tokens&lt;/code&gt; or &lt;code&gt;cachedContentTokenCount&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic: cache usage fields in the response usage object&lt;/li&gt;
&lt;li&gt;OpenRouter: routing/session behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Hash the fully rendered prompt
&lt;/h3&gt;

&lt;p&gt;Do not just version the user message.&lt;/p&gt;

&lt;p&gt;Hash the final request body after templating, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;system prompt&lt;/li&gt;
&lt;li&gt;developer prompt&lt;/li&gt;
&lt;li&gt;tool definitions&lt;/li&gt;
&lt;li&gt;tool order&lt;/li&gt;
&lt;li&gt;examples&lt;/li&gt;
&lt;li&gt;conversation history&lt;/li&gt;
&lt;li&gt;response schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of those change, cache behavior changes too.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Benchmark cold and warm separately
&lt;/h3&gt;

&lt;p&gt;Run one set with unique prefixes.&lt;/p&gt;

&lt;p&gt;Run another set with stable prefixes.&lt;/p&gt;

&lt;p&gt;Label them honestly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cold-start latency&lt;/li&gt;
&lt;li&gt;warm-cache latency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not average them into one vanity number.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Control the timing window
&lt;/h3&gt;

&lt;p&gt;Cache TTL matters.&lt;/p&gt;

&lt;p&gt;If your benchmark drifts across cache expiration windows, your numbers will drift too.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Verify the workflow didn’t reduce actual model calls
&lt;/h3&gt;

&lt;p&gt;Before celebrating a cost or latency win, confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no dedupe node removed items&lt;/li&gt;
&lt;li&gt;no retry logic changed request count&lt;/li&gt;
&lt;li&gt;no batching layer altered call volume&lt;/li&gt;
&lt;li&gt;no router stickiness hid the real path&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A practical benchmark harness pattern
&lt;/h2&gt;

&lt;p&gt;This is the shape I’d recommend for agent teams.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prompt_fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prompt_fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;

    &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;
    &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens_details&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens_details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fingerprint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completion_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completion_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a strict API design reviewer.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review this OpenAPI spec and list breaking changes.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This won’t solve every benchmarking problem.&lt;/p&gt;

&lt;p&gt;But it will stop you from calling a warm cache hit a model breakthrough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The annoying truth: prompt caching is good, fake benchmark stories are not
&lt;/h2&gt;

&lt;p&gt;Prompt caching is not cheating.&lt;/p&gt;

&lt;p&gt;It’s one of the best features for real agent workloads.&lt;/p&gt;

&lt;p&gt;If you run long-lived automations, coding agents, support bots, or multi-step workflows, you absolutely want big stable prefixes to be reused.&lt;/p&gt;

&lt;p&gt;That’s a real production win.&lt;/p&gt;

&lt;p&gt;The mistake is claiming you measured model quality, raw speed, or provider pricing when what you really measured was cache friendliness.&lt;/p&gt;

&lt;p&gt;Those are different things.&lt;/p&gt;

&lt;p&gt;Once you notice this, a lot of benchmark posts start looking suspiciously magical.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the second run is always faster&lt;/li&gt;
&lt;li&gt;the giant agent gets “smarter” after turn three&lt;/li&gt;
&lt;li&gt;Claude suddenly becomes cheaper in a multi-turn evaluator&lt;/li&gt;
&lt;li&gt;Gemini “settles in” after a few calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Maybe.&lt;/p&gt;

&lt;p&gt;But usually the boring answer wins.&lt;/p&gt;

&lt;p&gt;The prefix matched.&lt;br&gt;
The router stayed sticky.&lt;br&gt;
The workflow dropped duplicates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters even more for teams building agents
&lt;/h2&gt;

&lt;p&gt;If you’re building serious automations, you need predictable behavior, not benchmark theater.&lt;/p&gt;

&lt;p&gt;That’s one reason I like what Standard Compute is doing.&lt;/p&gt;

&lt;p&gt;Standard Compute gives you unlimited AI compute for a flat monthly price through an OpenAI-compatible API. So if you’re running agents in n8n, Make, Zapier, OpenClaw, or custom workers, you can keep the same SDK patterns without living in per-token panic.&lt;/p&gt;

&lt;p&gt;That doesn’t remove the need for honest benchmarking.&lt;/p&gt;

&lt;p&gt;It just means you can optimize for actual workflow performance instead of obsessing over every token spike from repeated agent calls.&lt;/p&gt;

&lt;p&gt;For teams running lots of long-context, multi-step automations, that tradeoff makes a lot of sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you benchmark repeated LLM calls without checking cache fields, your numbers are probably lying.&lt;/p&gt;

&lt;p&gt;Log the usage fields.&lt;br&gt;
Separate cold from warm.&lt;br&gt;
Hash the full prompt.&lt;br&gt;
Check the workflow layer.&lt;/p&gt;

&lt;p&gt;Then publish the numbers.&lt;/p&gt;

&lt;p&gt;Anything else is just benchmark fan fiction.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>benchmarking</category>
      <category>devops</category>
    </item>
    <item>
      <title>Our chatbot not replying in groups turned out to be a Slack plumbing bug, not an AI bug</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Sat, 10 Oct 2026 22:11:44 +0000</pubDate>
      <link>https://dev.to/lars_winstand/our-chatbot-not-replying-in-groups-turned-out-to-be-a-slack-plumbing-bug-not-an-ai-bug-1dg6</link>
      <guid>https://dev.to/lars_winstand/our-chatbot-not-replying-in-groups-turned-out-to-be-a-slack-plumbing-bug-not-an-ai-bug-1dg6</guid>
      <description>&lt;p&gt;If your bot works in DMs but goes weirdly silent in Slack channels, start by blaming your event handling.&lt;/p&gt;

&lt;p&gt;Not GPT-5.&lt;br&gt;
Not Claude.&lt;br&gt;
Not your prompt.&lt;/p&gt;

&lt;p&gt;We lost a bunch of time learning this the dumb way.&lt;/p&gt;

&lt;p&gt;Our bot was great in 1:1 chats. In Slack DMs it could hold context, call tools, summarize results, and generally look like a competent AI teammate.&lt;/p&gt;

&lt;p&gt;Then we dropped the exact same bot into a busy channel.&lt;/p&gt;

&lt;p&gt;It became a ghost.&lt;/p&gt;

&lt;p&gt;Sometimes it replied.&lt;br&gt;
Sometimes it replied twice.&lt;br&gt;
Sometimes the backend finished successfully and Slack showed nothing.&lt;br&gt;
Sometimes it answered in the wrong thread.&lt;/p&gt;

&lt;p&gt;That kind of failure is extra annoying because it makes you debug the wrong layer first.&lt;/p&gt;

&lt;p&gt;We blamed the model.&lt;br&gt;
We swapped GPT-5 for Claude.&lt;br&gt;
We trimmed prompts.&lt;br&gt;
We argued about whether Llama or Qwen would be more reliable in channels.&lt;/p&gt;

&lt;p&gt;None of that mattered.&lt;/p&gt;

&lt;p&gt;The real problem was that we treated Slack group chats like DMs, and Slack absolutely does not work that way.&lt;/p&gt;
&lt;h2&gt;
  
  
  The first bug: DMs and mentions are different event types
&lt;/h2&gt;

&lt;p&gt;This is the first thing I’d check in any Slack bot.&lt;/p&gt;

&lt;p&gt;A direct message to your app comes in as a &lt;code&gt;message&lt;/code&gt; event with &lt;code&gt;channel_type="im"&lt;/code&gt;.&lt;br&gt;
A mention in a channel comes in as &lt;code&gt;app_mention&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those are different event streams with different behavior.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"channel_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"im"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D024BE91L"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello hello can you hear me?"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Channel mention example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app_mention"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C123ABC456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;@U0LAN0Z89&amp;gt; is it everything a river should be?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1515449522.000016"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one distinction explains a lot of “works in DMs, fails in channels” bugs.&lt;/p&gt;

&lt;p&gt;In DMs, the shape is simple: user talks to bot.&lt;/p&gt;

&lt;p&gt;In channels, everything gets more fragile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mention parsing&lt;/li&gt;
&lt;li&gt;channel membership&lt;/li&gt;
&lt;li&gt;permissions&lt;/li&gt;
&lt;li&gt;thread routing&lt;/li&gt;
&lt;li&gt;rate limits&lt;/li&gt;
&lt;li&gt;duplicate events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your code has one generic &lt;code&gt;handleIncomingMessage()&lt;/code&gt; path for everything, there’s a decent chance that’s your bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it only broke in busy channels
&lt;/h2&gt;

&lt;p&gt;Quiet channels hide bad architecture.&lt;/p&gt;

&lt;p&gt;If your bot mostly lives in DMs or low-traffic channels, a synchronous handler can look fine for weeks.&lt;/p&gt;

&lt;p&gt;Then one incident channel gets busy.&lt;br&gt;
Five people mention the bot.&lt;br&gt;
One tool call takes 8 seconds.&lt;br&gt;
One GitHub request stalls.&lt;br&gt;
One retry comes in.&lt;br&gt;
And suddenly your “AI reliability issue” is obviously just broken event lifecycle handling.&lt;/p&gt;

&lt;p&gt;Slack expects a fast HTTP 200 acknowledgment.&lt;/p&gt;

&lt;p&gt;The practical rule is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;you have about 3 seconds to ack&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you don’t ack quickly, Slack may retry the event, and now you’re in duplicate-processing land.&lt;/p&gt;

&lt;p&gt;This is the dangerous flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive &lt;code&gt;app_mention&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Call GPT-5 or Claude&lt;/li&gt;
&lt;li&gt;Query your vector DB&lt;/li&gt;
&lt;li&gt;Call GitHub&lt;/li&gt;
&lt;li&gt;Call Jira&lt;/li&gt;
&lt;li&gt;Summarize everything&lt;/li&gt;
&lt;li&gt;Finally reply to Slack&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That feels natural when you first build it.&lt;/p&gt;

&lt;p&gt;It’s also how you build a bot that disappears under load.&lt;/p&gt;
&lt;h2&gt;
  
  
  The boring fix that actually worked
&lt;/h2&gt;

&lt;p&gt;We moved all slow work out of the request path.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ack immediately&lt;/li&gt;
&lt;li&gt;enqueue work&lt;/li&gt;
&lt;li&gt;process in a background worker&lt;/li&gt;
&lt;li&gt;post the final answer later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the shape in Slack Bolt for Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slack_bolt&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;App&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process_before_response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ack_fast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_long_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# slow work goes here
&lt;/span&gt;    &lt;span class="c1"&gt;# call model
&lt;/span&gt;    &lt;span class="c1"&gt;# call tools
&lt;/span&gt;    &lt;span class="c1"&gt;# build final answer
&lt;/span&gt;
    &lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the idea, but in production I’d usually push this into a queue instead of doing everything inside the listener.&lt;/p&gt;

&lt;p&gt;Something more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BackgroundTasks&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/slack/events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;slack_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;background_tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackgroundTasks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Slack URL verification
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url_verification&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;challenge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;challenge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. dedupe by event_id
&lt;/span&gt;    &lt;span class="c1"&gt;# 2. enqueue background work
&lt;/span&gt;    &lt;span class="n"&gt;background_tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# ack immediately
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# idempotency check here
&lt;/span&gt;    &lt;span class="c1"&gt;# model call here
&lt;/span&gt;    &lt;span class="c1"&gt;# tool calls here
&lt;/span&gt;    &lt;span class="c1"&gt;# post reply here
&lt;/span&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the framework.&lt;/p&gt;

&lt;p&gt;The important part is the split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;webhook path does validation + dedupe + enqueue&lt;/li&gt;
&lt;li&gt;worker does the expensive stuff&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reliable Slack bots are event systems first and AI apps second.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second bug: Slack can drop messages even when your backend succeeded
&lt;/h2&gt;

&lt;p&gt;This one was nastier.&lt;/p&gt;

&lt;p&gt;We assumed that if our worker finished and called &lt;code&gt;chat.postMessage&lt;/code&gt;, the message would show up.&lt;/p&gt;

&lt;p&gt;That is not safe in busy channels.&lt;/p&gt;

&lt;p&gt;Slack rate-limits &lt;code&gt;chat.postMessage&lt;/code&gt; at roughly &lt;strong&gt;1 message per second per channel&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Per channel.&lt;/p&gt;

&lt;p&gt;That means if your bot posts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“thinking...”&lt;/li&gt;
&lt;li&gt;“checking GitHub...”&lt;/li&gt;
&lt;li&gt;“checking Datadog...”&lt;/li&gt;
&lt;li&gt;“found 3 issues...”&lt;/li&gt;
&lt;li&gt;“here’s the answer”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and three people mention it in the same thread at once, you’ve basically built a rate-limit machine.&lt;/p&gt;

&lt;p&gt;The ugly part is that your logs can still look fine while users see silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed
&lt;/h2&gt;

&lt;p&gt;We stopped treating outbound messages like free writes.&lt;/p&gt;

&lt;p&gt;The new rules were:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ack immediately&lt;/li&gt;
&lt;li&gt;Queue all work&lt;/li&gt;
&lt;li&gt;Dedupe retries&lt;/li&gt;
&lt;li&gt;Serialize outbound posts per channel&lt;/li&gt;
&lt;li&gt;Throttle to about 1 msg/sec/channel&lt;/li&gt;
&lt;li&gt;Prefer one solid answer over five cute updates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That improved reliability more than any prompt tweak.&lt;/p&gt;

&lt;p&gt;Not because the model got smarter.&lt;br&gt;
Because the plumbing stopped fighting the app.&lt;/p&gt;
&lt;h2&gt;
  
  
  Thread state is where a lot of bots quietly fall apart
&lt;/h2&gt;

&lt;p&gt;In DMs, state is easy.&lt;/p&gt;

&lt;p&gt;In channels, state is thread-shaped.&lt;/p&gt;

&lt;p&gt;If you reply without the right &lt;code&gt;thread_ts&lt;/code&gt;, your answer lands in the wrong place or becomes useless noise in the channel.&lt;/p&gt;

&lt;p&gt;A lot of teams do this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;receive a new message&lt;/li&gt;
&lt;li&gt;call &lt;code&gt;conversations.replies&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;rebuild the whole thread&lt;/li&gt;
&lt;li&gt;send that to the model&lt;/li&gt;
&lt;li&gt;repeat forever&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That works at first.&lt;br&gt;
It’s also expensive, slow, and increasingly brittle.&lt;/p&gt;

&lt;p&gt;A better pattern is to keep your own compact thread state.&lt;/p&gt;

&lt;p&gt;Store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;channel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;root &lt;code&gt;thread_ts&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;normalized user turns&lt;/li&gt;
&lt;li&gt;tool outputs&lt;/li&gt;
&lt;li&gt;model outputs&lt;/li&gt;
&lt;li&gt;a compact rolling summary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use Slack history as recovery, not as your primary memory system.&lt;/p&gt;

&lt;p&gt;Here’s the tradeoff:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What actually happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rebuild thread from Slack every turn&lt;/td&gt;
&lt;td&gt;Easy to start, but slower, noisier, and more fragile under load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintain your own thread state&lt;/td&gt;
&lt;td&gt;Slightly more engineering, much more reliable for long-running agents&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you’re running workflows in n8n, Make, Zapier, OpenClaw, or your own worker stack, this matters even more.&lt;/p&gt;

&lt;p&gt;Those tools make it easy to ship a bot quickly.&lt;br&gt;
They also make it easy to hide state bugs until traffic spikes.&lt;/p&gt;
&lt;h2&gt;
  
  
  A practical event pipeline that doesn’t embarrass you
&lt;/h2&gt;

&lt;p&gt;If I were rebuilding this from scratch, I’d do it like this.&lt;/p&gt;
&lt;h3&gt;
  
  
  1) Split ingress by conversation type
&lt;/h3&gt;

&lt;p&gt;Handle DMs and mentions separately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;channel_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;channel_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;im&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app_mention&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel_mention&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different paths should have different logic for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;permissions&lt;/li&gt;
&lt;li&gt;mention parsing&lt;/li&gt;
&lt;li&gt;reply routing&lt;/li&gt;
&lt;li&gt;thread handling&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Ack first, think later
&lt;/h3&gt;

&lt;p&gt;Anything slow goes to background work.&lt;/p&gt;

&lt;p&gt;That includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPT-5 calls&lt;/li&gt;
&lt;li&gt;Claude calls&lt;/li&gt;
&lt;li&gt;embeddings/search&lt;/li&gt;
&lt;li&gt;GitHub lookups&lt;/li&gt;
&lt;li&gt;Jira lookups&lt;/li&gt;
&lt;li&gt;Notion reads&lt;/li&gt;
&lt;li&gt;Google Drive scans&lt;/li&gt;
&lt;li&gt;internal tool calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your webhook should be boring.&lt;br&gt;
Boring is good.&lt;/p&gt;
&lt;h3&gt;
  
  
  3) Treat retries as normal
&lt;/h3&gt;

&lt;p&gt;Slack retries are not edge cases.&lt;/p&gt;

&lt;p&gt;Store &lt;code&gt;event_id&lt;/code&gt; and make processing idempotent.&lt;/p&gt;

&lt;p&gt;Pseudo-code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;already_processed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="nf"&gt;mark_processing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;mark_processed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;mark_failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you skip this, duplicate replies are just a matter of time.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Rate-limit outbound messages per channel
&lt;/h3&gt;

&lt;p&gt;One queue per channel is a sane default.&lt;/p&gt;

&lt;p&gt;Pseudo-code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;channel_queues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post_with_throttle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;channel_queues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;next_msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;slack_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat_postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;next_msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In real code you’d use a proper worker, lock, or async queue, but the design point stands:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;throttle per channel, not globally&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5) Prefer edits over spam
&lt;/h3&gt;

&lt;p&gt;If you want progress updates, update one message instead of posting five new ones.&lt;/p&gt;

&lt;p&gt;That usually looks cleaner for users and is friendlier to rate limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  6) Own your thread state
&lt;/h3&gt;

&lt;p&gt;Slack is a transport layer.&lt;br&gt;
It should not be your source of truth.&lt;/p&gt;

&lt;p&gt;Minimal thread record example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C123ABC456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"thread_ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1712345678.123456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"participants"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"U111"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"U222"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"check prod errors"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Looking into GitHub and Datadog"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Investigating production error spike after deploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_updated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-10T12:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a stable context source for long-running agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Slack and Discord have the same class of failure
&lt;/h2&gt;

&lt;p&gt;The labels differ, but the architecture lesson is the same.&lt;/p&gt;

&lt;p&gt;Slack has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app_mention&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;threads&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chat.postMessage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;channel-specific behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Discord has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;interactions&lt;/li&gt;
&lt;li&gt;deferred responses&lt;/li&gt;
&lt;li&gt;followups&lt;/li&gt;
&lt;li&gt;edit windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different APIs, same rule:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;respond fast, queue slow work, dedupe retries, and control outbound writes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you do long-running agent work inline, both platforms will punish you.&lt;/p&gt;

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

&lt;p&gt;When a bot works in DMs but not in channels, this is the checklist I’d run in order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify the incoming event type
&lt;/h3&gt;

&lt;p&gt;Log the event shape.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;debug_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check whether you’re actually receiving &lt;code&gt;app_mention&lt;/code&gt; and not assuming all traffic is &lt;code&gt;message&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify channel membership and scopes
&lt;/h3&gt;

&lt;p&gt;Make sure the app is actually in the channel and has the scopes it needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify fast ack timing
&lt;/h3&gt;

&lt;p&gt;Log how long your webhook takes before returning 200.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# validate + enqueue
&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ack path took &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that number is drifting upward, you’re moving work back into the request path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify dedupe
&lt;/h3&gt;

&lt;p&gt;Log &lt;code&gt;event_id&lt;/code&gt; and retry headers.&lt;/p&gt;

&lt;p&gt;If duplicate replies exist, you probably don’t have idempotency under control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify thread routing
&lt;/h3&gt;

&lt;p&gt;Make sure replies use the correct &lt;code&gt;thread_ts&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify outbound rate limiting
&lt;/h3&gt;

&lt;p&gt;If the bot is chatty in one channel, assume rate limits are part of the problem until proven otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this connects to AI infra
&lt;/h2&gt;

&lt;p&gt;This bug looked like a model problem because model calls were the most visible slow step.&lt;/p&gt;

&lt;p&gt;That’s common in agent systems.&lt;/p&gt;

&lt;p&gt;People blame GPT-5, Claude, Grok, or tool-calling reliability when the real issue is orchestration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bad queue design&lt;/li&gt;
&lt;li&gt;bad retries&lt;/li&gt;
&lt;li&gt;bad event routing&lt;/li&gt;
&lt;li&gt;bad rate limiting&lt;/li&gt;
&lt;li&gt;bad state management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s also why predictable AI infrastructure matters.&lt;/p&gt;

&lt;p&gt;When you’re building bots, automations, or long-running agents, you want the freedom to offload the slow work without obsessing over every token or every retry path.&lt;/p&gt;

&lt;p&gt;That’s the appeal of Standard Compute: you can keep your app OpenAI-compatible, swap in a flat-rate endpoint, and let agents run in the background without per-token cost anxiety creeping into every architecture decision.&lt;/p&gt;

&lt;p&gt;Especially if you’re wiring together Slack, GitHub, Jira, Notion, n8n, Make, Zapier, or custom workers, the expensive part is usually not one single prompt. It’s the whole loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The annoying truth
&lt;/h2&gt;

&lt;p&gt;Our bot never needed a better personality.&lt;br&gt;
It needed better manners.&lt;/p&gt;

&lt;p&gt;The real fix was not prompt engineering.&lt;br&gt;
It was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;separate handling for &lt;code&gt;message.im&lt;/code&gt; and &lt;code&gt;app_mention&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ack within 3 seconds&lt;/li&gt;
&lt;li&gt;background workers for slow tasks&lt;/li&gt;
&lt;li&gt;dedupe on retries&lt;/li&gt;
&lt;li&gt;thread-aware replies&lt;/li&gt;
&lt;li&gt;per-channel outbound throttling&lt;/li&gt;
&lt;li&gt;state outside Slack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your chatbot stops replying in groups while behaving perfectly in DMs, start with the boring plumbing.&lt;/p&gt;

&lt;p&gt;That’s the stuff that actually fixes production bots.&lt;/p&gt;

</description>
      <category>slack</category>
      <category>python</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>My support bot stopped forgetting everything when I changed AI memory write triggers</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Sat, 03 Oct 2026 22:09:39 +0000</pubDate>
      <link>https://dev.to/lars_winstand/my-support-bot-stopped-forgetting-everything-when-i-changed-ai-memory-write-triggers-40o1</link>
      <guid>https://dev.to/lars_winstand/my-support-bot-stopped-forgetting-everything-when-i-changed-ai-memory-write-triggers-40o1</guid>
      <description>&lt;p&gt;I had a support bot that looked great in demos and acted like it had amnesia in production.&lt;/p&gt;

&lt;p&gt;The failure mode was embarrassingly simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customer already rebooted the router three times&lt;/li&gt;
&lt;li&gt;customer already confirmed the firmware version&lt;/li&gt;
&lt;li&gt;customer already asked for email follow-up instead of SMS&lt;/li&gt;
&lt;li&gt;bot asked them to do all of that again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stack was not exotic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;n8n for orchestration&lt;/li&gt;
&lt;li&gt;pgvector for long-term memory&lt;/li&gt;
&lt;li&gt;an OpenAI-compatible API for model calls&lt;/li&gt;
&lt;li&gt;GPT-5.4 for most runs&lt;/li&gt;
&lt;li&gt;Claude Opus 4.6 for harder replies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model had the transcript. The model had a big context window. The workflow had retrieval.&lt;/p&gt;

&lt;p&gt;And it still behaved like it had never seen the case before.&lt;/p&gt;

&lt;p&gt;The fix was not a better model.&lt;/p&gt;

&lt;p&gt;The fix was changing when memory gets written.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The rule that fixed my support agent: write memory only on 5 event types — confirmed facts, preference changes, state-changing troubleshooting steps, decisions, and outcomes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once I did that, retrieval got smaller, cleaner, and actually useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real bug: I stored chat history instead of case history
&lt;/h2&gt;

&lt;p&gt;My first version wrote almost every turn into pgvector.&lt;/p&gt;

&lt;p&gt;That included junk like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"yes"&lt;/li&gt;
&lt;li&gt;"still broken"&lt;/li&gt;
&lt;li&gt;"I already tried that"&lt;/li&gt;
&lt;li&gt;"ok"&lt;/li&gt;
&lt;li&gt;"can you explain that again"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first this feels safe. More data should mean better recall, right?&lt;/p&gt;

&lt;p&gt;I think that is exactly backwards for support workflows.&lt;/p&gt;

&lt;p&gt;For support bots, full-transcript memory is usually worse than event memory because it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pollutes retrieval&lt;/li&gt;
&lt;li&gt;increases repeated mistakes&lt;/li&gt;
&lt;li&gt;bloats prompts&lt;/li&gt;
&lt;li&gt;burns tokens reloading low-value context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your workflow runs continuously in n8n, Make, Zapier, OpenClaw, or a custom agent loop, this gets worse over time. Every ticket adds more clutter. Every retrieval step gets noisier. Every prompt carries more baggage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bad fix I tried first
&lt;/h2&gt;

&lt;p&gt;Naturally, I tried the wrong fix before the right one.&lt;/p&gt;

&lt;p&gt;I changed the memory writer to summarize every conversation chunk before storing it.&lt;/p&gt;

&lt;p&gt;That sounded smarter. It was not.&lt;/p&gt;

&lt;p&gt;Now I had compressed noise instead of raw noise.&lt;/p&gt;

&lt;p&gt;GPT-5.4 and Grok 4.20 both did the same thing overloaded memory systems usually do: they latched onto plausible but low-value details, then repeated steps that were already resolved.&lt;/p&gt;

&lt;p&gt;This is the part a lot of people miss when building support automations:&lt;/p&gt;

&lt;p&gt;Bigger memory is not better memory.&lt;/p&gt;

&lt;p&gt;If your memory layer is sloppy, the model does not become more informed. It becomes more distractible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 memory write triggers that actually worked
&lt;/h2&gt;

&lt;p&gt;I stopped writing every turn and only wrote memory on 5 event types.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Confirmed facts
&lt;/h3&gt;

&lt;p&gt;Store facts only when they are explicitly confirmed.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Customer is on Netgear Nighthawk R7000&lt;/li&gt;
&lt;li&gt;Account is on the Pro plan&lt;/li&gt;
&lt;li&gt;ISP is Comcast&lt;/li&gt;
&lt;li&gt;Device is running firmware 1.0.11&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bad examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer probably has an old router&lt;/li&gt;
&lt;li&gt;This sounds like a DNS issue&lt;/li&gt;
&lt;li&gt;User may be on the free tier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the model inferred it, it does not belong in long-term memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Preference changes
&lt;/h3&gt;

&lt;p&gt;These are easy to ignore and they matter constantly.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer prefers email over phone&lt;/li&gt;
&lt;li&gt;Do not restart devices during business hours&lt;/li&gt;
&lt;li&gt;Contact only after 5 PM local time&lt;/li&gt;
&lt;li&gt;Wants concise replies, not step-by-step explanations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A support bot can be technically correct and still feel broken if it forgets preferences.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. State-changing troubleshooting steps
&lt;/h3&gt;

&lt;p&gt;This was the biggest one.&lt;/p&gt;

&lt;p&gt;Store the step only if it changed system state.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Factory reset completed&lt;/li&gt;
&lt;li&gt;DNS changed from ISP default to Cloudflare&lt;/li&gt;
&lt;li&gt;Firmware rolled back from 1.0.11 to 1.0.9&lt;/li&gt;
&lt;li&gt;Router moved from bridge mode to router mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not store suggestions.&lt;/p&gt;

&lt;p&gt;Bad memory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Suggested factory reset&lt;/li&gt;
&lt;li&gt;Recommended checking DNS&lt;/li&gt;
&lt;li&gt;Asked customer to reboot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not state. That is conversation.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Decisions
&lt;/h3&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Escalate to Tier 2&lt;/li&gt;
&lt;li&gt;Pause automation and wait for screenshot&lt;/li&gt;
&lt;li&gt;Schedule on-site technician visit&lt;/li&gt;
&lt;li&gt;Close loop pending customer response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Decisions shape what the next run should do. They belong in memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Outcomes
&lt;/h3&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Issue resolved after firmware rollback&lt;/li&gt;
&lt;li&gt;Ticket closed unresolved due to inactivity&lt;/li&gt;
&lt;li&gt;Packet loss persisted after modem replacement&lt;/li&gt;
&lt;li&gt;Escalation accepted by network team&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Outcomes stop the agent from reopening dead paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule in table form
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event Type&lt;/th&gt;
&lt;th&gt;Store It?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Confirmed hardware model&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer says "ok"&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS actually changed&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bot suggested DNS change&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer changed contact preference&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversation summary of small talk&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalate to Tier 2&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Issue resolved after rollback&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What the memory payload started to look like
&lt;/h2&gt;

&lt;p&gt;Instead of stuffing transcript chunks into pgvector, I started storing structured events.&lt;/p&gt;

&lt;p&gt;Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ticket_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T-18422"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"state_change"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-14T16:22:11Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Factory reset completed on Netgear Nighthawk R7000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"device"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Netgear Nighthawk R7000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"factory_reset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"completed"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ticket_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T-18422"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"preference_change"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-14T16:31:02Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer prefers email follow-up instead of SMS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gave retrieval something useful to rank.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical write filter
&lt;/h2&gt;

&lt;p&gt;This is the kind of logic I wish I had started with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shouldWriteMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;confirmed&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;changedState&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirmed_fact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confirmed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;preference_change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;state_change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changedState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;decision&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outcome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the retrieval side stayed aggressive about keeping context small.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildMemoryContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;memories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MemoryRecord&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;memories&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`- [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;slice(0, 5)&lt;/code&gt; mattered more than I expected.&lt;/p&gt;

&lt;p&gt;Not 50 records. Not the whole ticket history. Just the highest-signal events.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this changes the prompt
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Here is the previous conversation history:
[huge blob of transcript chunks and summaries]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Relevant case memory:
- [confirmed_fact] Customer is on Netgear Nighthawk R7000
- [state_change] Factory reset completed
- [state_change] DNS changed from ISP default to Cloudflare
- [decision] Escalation to Tier 2 pending screenshot
- [preference_change] Customer prefers email follow-up

Do not repeat completed troubleshooting steps.
Continue from the current case state.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second prompt is shorter and much harder for the model to misunderstand.&lt;/p&gt;

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

&lt;p&gt;The behavior shift was immediate.&lt;/p&gt;

&lt;p&gt;The bot stopped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;asking customers to repeat resolved steps&lt;/li&gt;
&lt;li&gt;re-suggesting fixes that already failed&lt;/li&gt;
&lt;li&gt;losing handoff context between runs&lt;/li&gt;
&lt;li&gt;wasting tokens on giant memory payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It got better at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;continuing open tickets correctly&lt;/li&gt;
&lt;li&gt;handling asynchronous follow-ups&lt;/li&gt;
&lt;li&gt;escalating with cleaner summaries&lt;/li&gt;
&lt;li&gt;respecting customer preferences&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This also improved reliability in long-running workflows. If your agent is running all day in n8n or Make, bad memory rules compound. Good memory rules compound too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters even more if you pay per token
&lt;/h2&gt;

&lt;p&gt;This is not just a quality problem. It is also a cost problem.&lt;/p&gt;

&lt;p&gt;Bad retrieval design quietly becomes an expensive habit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;larger prompts&lt;/li&gt;
&lt;li&gt;more retrieval overhead&lt;/li&gt;
&lt;li&gt;more retries after wrong answers&lt;/li&gt;
&lt;li&gt;more model calls because the workflow keeps looping on already-resolved issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are paying per token, noisy memory punishes you twice: once in quality and again on the bill.&lt;/p&gt;

&lt;p&gt;This is exactly why I like using an OpenAI-compatible layer that does not make me obsess over every token while I tune workflows.&lt;/p&gt;

&lt;p&gt;With Standard Compute, I can keep the same OpenAI SDK patterns, route across models like GPT-5.4, Claude Opus 4.6, and Grok 4.20, and run agent-heavy automations without watching a per-token meter all day.&lt;/p&gt;

&lt;p&gt;That does not remove the need for good memory design. Nothing removes that. But it does make iteration a lot less annoying when you are testing retrieval, prompts, and multi-step automations in n8n or custom agent loops.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you are building a support bot, my advice is simple
&lt;/h2&gt;

&lt;p&gt;Treat memory like a timeline of state changes, not a backup of the conversation.&lt;/p&gt;

&lt;p&gt;Support is not a creative writing task. It is closer to a state machine with human language wrapped around it.&lt;/p&gt;

&lt;p&gt;Store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what is true&lt;/li&gt;
&lt;li&gt;what changed&lt;/li&gt;
&lt;li&gt;what was decided&lt;/li&gt;
&lt;li&gt;what happened next&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not store every sentence just because you can.&lt;/p&gt;

&lt;p&gt;That one change fixed more of my support bot's "memory" than switching models ever did.&lt;/p&gt;

&lt;p&gt;If your agent keeps forgetting obvious things, I would look at memory write triggers before I blame the model.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>n8n</category>
      <category>agents</category>
      <category>devops</category>
    </item>
    <item>
      <title>TIL the best automations reduce context window by using cheap models for boring steps and saving Claude Opus for the hard part</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Sat, 03 Oct 2026 06:10:16 +0000</pubDate>
      <link>https://dev.to/lars_winstand/til-the-best-automations-reduce-context-window-by-using-cheap-models-for-boring-steps-and-saving-58md</link>
      <guid>https://dev.to/lars_winstand/til-the-best-automations-reduce-context-window-by-using-cheap-models-for-boring-steps-and-saving-58md</guid>
      <description>&lt;p&gt;I learned this the annoying way: I inspected a support-ticket automation that was doing cleanup, classification, extraction, summarization, and escalation decisions with the same expensive model.&lt;/p&gt;

&lt;p&gt;It worked.&lt;/p&gt;

&lt;p&gt;That was the problem.&lt;/p&gt;

&lt;p&gt;When a workflow works, nobody asks whether it’s wasting money and context on glorified janitor tasks.&lt;/p&gt;

&lt;p&gt;And most automations absolutely are.&lt;/p&gt;

&lt;p&gt;If you’re building with n8n, Make, Zapier, LangChain, OpenClaw, or plain API calls, the highest-leverage optimization usually isn’t a better prompt.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;use cheap, fast models for boring steps&lt;/li&gt;
&lt;li&gt;use expensive reasoning models only for ambiguous or high-risk steps&lt;/li&gt;
&lt;li&gt;shrink context before it reaches your strongest model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That pattern saves money, reduces context bloat, and usually makes the workflow easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real waste: treating every step like frontier reasoning
&lt;/h2&gt;

&lt;p&gt;A lot of teams say they’re building agents with GPT-5, Claude Opus, or Gemini.&lt;/p&gt;

&lt;p&gt;Then you inspect the actual pipeline and half the calls are doing this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;classify this ticket&lt;/li&gt;
&lt;li&gt;extract these fields&lt;/li&gt;
&lt;li&gt;rewrite this into valid JSON&lt;/li&gt;
&lt;li&gt;tag this document&lt;/li&gt;
&lt;li&gt;summarize this thread&lt;/li&gt;
&lt;li&gt;check whether required keys exist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not the same job as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;reason through an ambiguous escalation with conflicting evidence&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yet people keep paying frontier-model prices for both.&lt;/p&gt;

&lt;p&gt;That makes no sense.&lt;/p&gt;

&lt;p&gt;Anthropic’s pricing makes the argument pretty clearly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Haiku 4.5: $1 input / $5 output per 1M tokens&lt;/li&gt;
&lt;li&gt;Claude Opus 4.6: $5 input / $25 output per 1M tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a 5x spread.&lt;/p&gt;

&lt;p&gt;So if your workflow starts with extraction, tagging, cleanup, or first-pass summarization, that’s where I’d optimize first.&lt;/p&gt;

&lt;p&gt;Not the final reasoning step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context bloat is usually self-inflicted
&lt;/h2&gt;

&lt;p&gt;The second problem is even more common: people keep sending giant prompts to the wrong model.&lt;/p&gt;

&lt;p&gt;Typical anti-pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;take a messy email thread&lt;/li&gt;
&lt;li&gt;add CRM notes&lt;/li&gt;
&lt;li&gt;add internal comments&lt;/li&gt;
&lt;li&gt;add a giant system prompt&lt;/li&gt;
&lt;li&gt;send all of it to the best model&lt;/li&gt;
&lt;li&gt;repeat for every step&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So classification sees the entire novel.&lt;br&gt;
Extraction sees the entire novel.&lt;br&gt;
Summarization sees the entire novel again.&lt;/p&gt;

&lt;p&gt;That’s backwards.&lt;/p&gt;

&lt;p&gt;The cheap model should do the trimming.&lt;/p&gt;

&lt;p&gt;Use a fast model to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;normalize text&lt;/li&gt;
&lt;li&gt;dedupe repeated content&lt;/li&gt;
&lt;li&gt;extract fields&lt;/li&gt;
&lt;li&gt;compress long threads&lt;/li&gt;
&lt;li&gt;convert unstructured junk into structured JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then pass the smaller result to Claude Opus 4.6 or GPT-5 only if the task is actually hard.&lt;/p&gt;

&lt;p&gt;That’s how you reduce context window pressure in practice.&lt;/p&gt;

&lt;p&gt;Not with heroic prompt surgery. With better pipeline design.&lt;/p&gt;
&lt;h2&gt;
  
  
  The vendors are basically telling us to do this
&lt;/h2&gt;

&lt;p&gt;Google is pretty blunt here.&lt;/p&gt;

&lt;p&gt;The Gemini API pricing page offers Batch API pricing at a 50% cost reduction for supported models.&lt;/p&gt;

&lt;p&gt;That’s basically Google saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;stop using premium synchronous calls for bulk background work&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Example pricing called out in the original research:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gemini 3.8 Flash standard: $0.75 input / $3.75 output per 1M tokens&lt;/li&gt;
&lt;li&gt;Gemini 3.8 Flash Batch: $0.375 input / $1.875 output per 1M tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re summarizing 10,000 support conversations overnight, that should be batch work.&lt;br&gt;
If you’re evaluating a single churn-risk enterprise escalation, that’s where premium reasoning belongs.&lt;/p&gt;

&lt;p&gt;Anthropic sends the same signal with prompt caching.&lt;/p&gt;

&lt;p&gt;If your automation repeats long system instructions or reusable context, Claude Opus 4.6 prompt cache hits/refreshes at $0.50 per 1M tokens can change the economics a lot.&lt;/p&gt;

&lt;p&gt;Same idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;don’t recompute what you can cache&lt;/li&gt;
&lt;li&gt;don’t reason deeply about what you can preprocess cheaply&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Routing is now an engineering primitive
&lt;/h2&gt;

&lt;p&gt;This gets more interesting once routing becomes part of the API layer.&lt;/p&gt;

&lt;p&gt;OpenRouter’s provider routing lets you optimize for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;price&lt;/li&gt;
&lt;li&gt;throughput&lt;/li&gt;
&lt;li&gt;max latency&lt;/li&gt;
&lt;li&gt;fallbacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means routing is no longer just “I like Claude for writing.”&lt;/p&gt;

&lt;p&gt;It becomes operational.&lt;/p&gt;

&lt;p&gt;Cheap first. Fast second. Strongest only when needed.&lt;/p&gt;

&lt;p&gt;A support workflow can prefer low-cost providers for first-pass classification, then escalate if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;latency spikes&lt;/li&gt;
&lt;li&gt;output fails validation&lt;/li&gt;
&lt;li&gt;confidence is low&lt;/li&gt;
&lt;li&gt;the case matches a risky category&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s much better than blind loyalty to one model vendor.&lt;/p&gt;

&lt;p&gt;Example request shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openai/gpt-5.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Classify this ticket"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allow_fallbacks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"preferred_max_latency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That little &lt;code&gt;provider&lt;/code&gt; block matters more than most people think.&lt;/p&gt;

&lt;p&gt;It means your workflow can optimize for cost, speed, and reliability at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern I keep coming back to: cheap model -&amp;gt; validate -&amp;gt; escalate
&lt;/h2&gt;

&lt;p&gt;If I had to recommend one pattern to most teams, it would be this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;cheap model does extraction or classification&lt;/li&gt;
&lt;li&gt;validate the output&lt;/li&gt;
&lt;li&gt;escalate only failures or ambiguous cases&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;This is the cleanest way to stop paying premium-model prices for routine work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: support ticket triage
&lt;/h3&gt;

&lt;p&gt;First pass:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;classify urgency&lt;/li&gt;
&lt;li&gt;extract account ID&lt;/li&gt;
&lt;li&gt;detect billing / outage / legal / enterprise keywords&lt;/li&gt;
&lt;li&gt;produce structured JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the JSON validates and confidence is high, continue.&lt;br&gt;
If validation fails or the case is risky, escalate to Claude Opus 4.6 or GPT-5.&lt;/p&gt;

&lt;p&gt;Example schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"urgency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"low | medium | high | critical"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"billing | bug | feature_request | cancellation | legal | other"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string | null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"needs_human"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer mentions production outage and enterprise SLA"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example: document extraction pipeline
&lt;/h3&gt;

&lt;p&gt;Cheap model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parse invoice, PDF, email, or form&lt;/li&gt;
&lt;li&gt;extract fields to JSON&lt;/li&gt;
&lt;li&gt;normalize dates, totals, IDs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validation step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;required keys exist&lt;/li&gt;
&lt;li&gt;number formats are valid&lt;/li&gt;
&lt;li&gt;dates parse correctly&lt;/li&gt;
&lt;li&gt;totals aren’t contradictory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expensive model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;only repair malformed outputs&lt;/li&gt;
&lt;li&gt;only reason over ambiguous or conflicting data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s better engineering, not just cheaper inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical orchestration examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  n8n pattern
&lt;/h3&gt;

&lt;p&gt;n8n is good at this because you can split the workflow into explicit stages.&lt;/p&gt;

&lt;p&gt;A simple shape:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Webhook node receives support ticket&lt;/li&gt;
&lt;li&gt;Cheap LLM call extracts structure&lt;/li&gt;
&lt;li&gt;Code node validates schema&lt;/li&gt;
&lt;li&gt;IF node checks confidence / category / validation result&lt;/li&gt;
&lt;li&gt;Strong model handles exceptions&lt;/li&gt;
&lt;li&gt;Ticket gets routed to human or auto-resolved&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pseudo-flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Webhook
  -&amp;gt; LLM (cheap model)
  -&amp;gt; Code (validate JSON)
  -&amp;gt; IF (valid &amp;amp;&amp;amp; confidence &amp;gt; 0.85 &amp;amp;&amp;amp; category != legal)
      -&amp;gt; Auto-route
      -&amp;gt; Else -&amp;gt; LLM (Claude Opus 4.6 / GPT-5)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  LangChain example
&lt;/h3&gt;

&lt;p&gt;If you’re using LangChain, swapping models by step is straightforward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.chat_models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;init_chat_model&lt;/span&gt;

&lt;span class="n"&gt;classifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;init_chat_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anthropic:claude-3-5-haiku-latest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;reasoner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;init_chat_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai:gpt-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;raw_ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Customer says their production workspace is down and legal will review the SLA.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;classification_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Extract JSON with fields:
- urgency
- category
- needs_human
- confidence
- reason

Ticket:
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;raw_ticket&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;first_pass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;classifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classification_prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_pass&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then validate before escalating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_escalate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;legal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urgency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_pass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;should_escalate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reasoner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review this support case and decide next action:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;raw_ticket&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;First pass:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Batch the boring stuff
&lt;/h3&gt;

&lt;p&gt;If the user does not need the result right now, batch it.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nightly summarization of support threads&lt;/li&gt;
&lt;li&gt;CRM note cleanup&lt;/li&gt;
&lt;li&gt;bulk document compression&lt;/li&gt;
&lt;li&gt;asynchronous tagging jobs&lt;/li&gt;
&lt;li&gt;historical conversation labeling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s exactly the kind of workload where lower-cost batch processing wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple stack I’d actually use
&lt;/h2&gt;

&lt;p&gt;If I were building this today, I’d reach for something like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Haiku 4.5 for extraction, tagging, classification, and structured cleanup&lt;/li&gt;
&lt;li&gt;Gemini Flash Batch for bulk asynchronous summarization&lt;/li&gt;
&lt;li&gt;Claude Opus 4.6 or GPT-5 for exception handling and edge-case reasoning&lt;/li&gt;
&lt;li&gt;OpenRouter for routing, fallback, and price-aware provider selection&lt;/li&gt;
&lt;li&gt;n8n or LangChain for orchestration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives you a clean separation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cheap model for discipline&lt;/li&gt;
&lt;li&gt;expensive model for judgment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the split most automations need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yes, this adds complexity
&lt;/h2&gt;

&lt;p&gt;It does.&lt;/p&gt;

&lt;p&gt;Using one strong model everywhere is simpler.&lt;/p&gt;

&lt;p&gt;Routing adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more prompts&lt;/li&gt;
&lt;li&gt;more failure modes&lt;/li&gt;
&lt;li&gt;more testing&lt;/li&gt;
&lt;li&gt;more evaluation work between steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For small workflows, one strong model is often fine.&lt;/p&gt;

&lt;p&gt;If your automation is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;low volume&lt;/li&gt;
&lt;li&gt;rarely triggered&lt;/li&gt;
&lt;li&gt;expensive to debug&lt;/li&gt;
&lt;li&gt;not context heavy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then simplicity may be the better tradeoff.&lt;/p&gt;

&lt;p&gt;But once repeated boring steps show up thousands of times a day, routing starts paying for itself very quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  One warning: cheap isn’t always cheap
&lt;/h2&gt;

&lt;p&gt;Raw token pricing can lie to you.&lt;/p&gt;

&lt;p&gt;Tokenizer differences, retries, malformed outputs, and escalation rates all matter.&lt;/p&gt;

&lt;p&gt;The original post also pointed out that Anthropic notes newer tokenizers can produce about 30% more tokens for the same text in some cases.&lt;/p&gt;

&lt;p&gt;So don’t stop at pricing tables.&lt;/p&gt;

&lt;p&gt;Measure end-to-end workflow economics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how often first pass succeeds&lt;/li&gt;
&lt;li&gt;how often you escalate&lt;/li&gt;
&lt;li&gt;how much context you removed before reasoning&lt;/li&gt;
&lt;li&gt;how many retries happen&lt;/li&gt;
&lt;li&gt;how much latency each stage adds&lt;/li&gt;
&lt;li&gt;how much human review you avoided&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the real unit of cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What it’s actually good for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;Cheap extraction, tagging, classification, and structured cleanup at $1 input / $5 output per 1M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4.6&lt;/td&gt;
&lt;td&gt;Hard reasoning, ambiguous validation, and exception handling at $5 input / $25 output per 1M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4.6 prompt caching&lt;/td&gt;
&lt;td&gt;Repeated system prompts and reusable context, with cache hits/refreshes at $0.50 per 1M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.8 Flash standard&lt;/td&gt;
&lt;td&gt;Fast synchronous summarization or lightweight user-facing steps at $0.75 input / $3.75 output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.8 Flash Batch&lt;/td&gt;
&lt;td&gt;Background summarization and bulk processing with 50% lower cost at $0.375 input / $1.875 output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenRouter routing&lt;/td&gt;
&lt;td&gt;Price sorting, latency controls, throughput-aware routing, and fallbacks across providers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I’d optimize first
&lt;/h2&gt;

&lt;p&gt;If you want the least painful, highest-impact change, start with one of these.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Support triage
&lt;/h3&gt;

&lt;p&gt;Send first-pass classification and tagging to a low-cost model.&lt;br&gt;
Escalate only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;billing disputes&lt;/li&gt;
&lt;li&gt;legal threats&lt;/li&gt;
&lt;li&gt;enterprise accounts&lt;/li&gt;
&lt;li&gt;critical incidents&lt;/li&gt;
&lt;li&gt;low-confidence outputs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  2. Document extraction
&lt;/h3&gt;

&lt;p&gt;Use a cheap model to turn PDFs, emails, and forms into JSON.&lt;br&gt;
Validate the schema.&lt;br&gt;
Send only broken outputs to a stronger model.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Background summarization
&lt;/h3&gt;

&lt;p&gt;Move async summaries, note cleanup, and bulk compression onto a batch-friendly model.&lt;br&gt;
Keep synchronous decision-making on your strongest model.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Context cleanup before reasoning
&lt;/h3&gt;

&lt;p&gt;This one is underrated.&lt;/p&gt;

&lt;p&gt;Before the expensive model sees anything, run a cheap pass that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;removes duplicates&lt;/li&gt;
&lt;li&gt;extracts facts&lt;/li&gt;
&lt;li&gt;compresses long threads&lt;/li&gt;
&lt;li&gt;strips irrelevant metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That alone can dramatically reduce context usage and improve downstream consistency.&lt;/p&gt;
&lt;h2&gt;
  
  
  My blunt rule
&lt;/h2&gt;

&lt;p&gt;Use the cheapest model that can reliably produce valid structure.&lt;/p&gt;

&lt;p&gt;Use the strongest model only where mistakes are actually expensive.&lt;/p&gt;

&lt;p&gt;Batch anything the user does not need immediately.&lt;/p&gt;

&lt;p&gt;Cache repeated prompts whenever the vendor supports it.&lt;/p&gt;

&lt;p&gt;And measure escalations, not just token price.&lt;/p&gt;

&lt;p&gt;That last one matters most.&lt;/p&gt;

&lt;p&gt;Because the real question isn’t:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;which model is best?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;which step deserves intelligence, and which step just needs discipline?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you look at automations that way, sending everything to one expensive model starts to look less like sophistication and more like laziness.&lt;/p&gt;

&lt;p&gt;One more thing: if you’re tired of building this kind of routing while also babysitting per-token costs, that’s basically the problem Standard Compute is trying to remove. It gives you an OpenAI-compatible API with flat monthly pricing, so you can run automations and agents without constantly watching token spend. Same SDK shape, less pricing anxiety.&lt;/p&gt;

&lt;p&gt;That part is especially appealing if you’re running n8n, Make, Zapier, OpenClaw, or custom agent workflows all day.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quick test plan
&lt;/h2&gt;

&lt;p&gt;If you want to try this this week, do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1) pick one workflow with repeated boring steps&lt;/span&gt;
&lt;span class="c"&gt;# 2) identify the expensive model calls&lt;/span&gt;
&lt;span class="c"&gt;# 3) split first-pass extraction/classification from final reasoning&lt;/span&gt;
&lt;span class="c"&gt;# 4) add schema validation&lt;/span&gt;
&lt;span class="c"&gt;# 5) log escalation rate&lt;/span&gt;
&lt;span class="c"&gt;# 6) compare cost + latency + quality after 3-7 days&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;first_pass_success_rate
schema_validation_failure_rate
escalation_rate
avg_tokens_before_cleanup
avg_tokens_after_cleanup
p50_latency
p95_latency
human_review_rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a real answer fast.&lt;/p&gt;

&lt;p&gt;And in most cases, the answer is the same one I learned the annoying way:&lt;/p&gt;

&lt;p&gt;your best model should not be doing your boring work.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>llm</category>
      <category>devops</category>
    </item>
    <item>
      <title>I stopped letting one agent do everything and my tool calls got way less cursed</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Fri, 02 Oct 2026 14:10:57 +0000</pubDate>
      <link>https://dev.to/lars_winstand/i-stopped-letting-one-agent-do-everything-and-my-tool-calls-got-way-less-cursed-l51</link>
      <guid>https://dev.to/lars_winstand/i-stopped-letting-one-agent-do-everything-and-my-tool-calls-got-way-less-cursed-l51</guid>
      <description>&lt;p&gt;I realized I was asking one poor model to do four jobs badly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Plan the task&lt;/li&gt;
&lt;li&gt;Pick the right tool&lt;/li&gt;
&lt;li&gt;Recover from failures&lt;/li&gt;
&lt;li&gt;Write a clean final answer like nothing exploded&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That setup looks great in a demo.&lt;/p&gt;

&lt;p&gt;In production, it’s how you get a Slack bot that says “done” after failing on step two.&lt;/p&gt;

&lt;p&gt;I’ve seen the same failure pattern in n8n, LangChain, OpenAI function-calling loops, and custom Python agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one giant GPT node with access to everything&lt;/li&gt;
&lt;li&gt;retries handled by vibes&lt;/li&gt;
&lt;li&gt;broken API responses treated like “reasoning problems”&lt;/li&gt;
&lt;li&gt;too much context dumped into one loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix was not a better prompt.&lt;/p&gt;

&lt;p&gt;The fix was splitting orchestration from generation.&lt;/p&gt;

&lt;p&gt;Stop asking one model to be the planner, dispatcher, retry loop, validator, and summarizer.&lt;/p&gt;

&lt;p&gt;Give it supervision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: one agent is doing workflow engine work
&lt;/h2&gt;

&lt;p&gt;A lot of agent setups are really orchestration engines wearing chatbot costumes.&lt;/p&gt;

&lt;p&gt;We tell the model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;think through the task&lt;/li&gt;
&lt;li&gt;choose tools&lt;/li&gt;
&lt;li&gt;inspect outputs&lt;/li&gt;
&lt;li&gt;detect failures&lt;/li&gt;
&lt;li&gt;retry if needed&lt;/li&gt;
&lt;li&gt;decide when it’s done&lt;/li&gt;
&lt;li&gt;summarize the result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not one job.&lt;/p&gt;

&lt;p&gt;That is a control plane.&lt;/p&gt;

&lt;p&gt;And once you treat it like one, a lot of weird agent behavior suddenly makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually makes tool-calling agents flaky?
&lt;/h2&gt;

&lt;p&gt;Most tool-use failures are not because GPT-5, Claude Opus 4.6, or Grok 4.20 are “bad at reasoning.”&lt;/p&gt;

&lt;p&gt;They’re usually workflow bugs.&lt;/p&gt;

&lt;p&gt;Common ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too many tools exposed to one agent&lt;/li&gt;
&lt;li&gt;No explicit retry policy when an API fails&lt;/li&gt;
&lt;li&gt;Retrieval is unbounded, so the model gets flooded with junk&lt;/li&gt;
&lt;li&gt;State from one task leaks into another&lt;/li&gt;
&lt;li&gt;The same model is doing reasoning, validation, exception handling, and cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the killer.&lt;/p&gt;

&lt;p&gt;When the agent breaks, people usually respond by making the prompt longer.&lt;/p&gt;

&lt;p&gt;More instructions.&lt;br&gt;
More examples.&lt;br&gt;
More all-caps warnings.&lt;/p&gt;

&lt;p&gt;Sometimes that helps.&lt;br&gt;
Usually it just creates a more polite failure.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Which parts of this workflow should not be left to model improvisation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question leads straight to the supervisor pattern.&lt;/p&gt;
&lt;h2&gt;
  
  
  The pattern: supervisor + narrow workers
&lt;/h2&gt;

&lt;p&gt;This is the version I keep coming back to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A supervisor plans the work&lt;/li&gt;
&lt;li&gt;Specialist workers handle narrow tasks&lt;/li&gt;
&lt;li&gt;The runtime handles retries, validation, tracing, and state transitions&lt;/li&gt;
&lt;li&gt;A final step summarizes the result&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The supervisor is not there to be clever.&lt;/p&gt;

&lt;p&gt;It’s there to keep the system honest.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;which worker gets the task&lt;/li&gt;
&lt;li&gt;which tools are allowed&lt;/li&gt;
&lt;li&gt;whether a failure should retry or stop&lt;/li&gt;
&lt;li&gt;what counts as success&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That one change removes a shocking amount of chaos.&lt;/p&gt;
&lt;h2&gt;
  
  
  A minimal example with the OpenAI Agents SDK
&lt;/h2&gt;

&lt;p&gt;The OpenAI Agents SDK already nudges you in this direction.&lt;/p&gt;

&lt;p&gt;Minimal shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agents&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Runner&lt;/span&gt;

&lt;span class="n"&gt;planner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Planner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Break work into steps and delegate to the right worker.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;planner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Find the latest API pricing changes and summarize them&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;final_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That snippet is tiny, but the important part is architectural:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Agent&lt;/code&gt; defines behavior&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Runner&lt;/code&gt; owns the loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is already better than raw “model, keep calling tools until you feel done.”&lt;/p&gt;

&lt;p&gt;If you add handoffs, validation, and tracing, you get a much safer setup without building everything from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why narrow workers beat one giant generalist
&lt;/h2&gt;

&lt;p&gt;Because tool choice gets worse as the tool list grows.&lt;/p&gt;

&lt;p&gt;If you give one agent 12 tools, it will eventually get creative in a bad way.&lt;/p&gt;

&lt;p&gt;A web research worker should do web research.&lt;br&gt;
A file retrieval worker should do file retrieval.&lt;br&gt;
A reporting worker should format output.&lt;/p&gt;

&lt;p&gt;Not every worker needs access to every tool.&lt;/p&gt;

&lt;p&gt;That’s how you reduce blast radius.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example: bounded retrieval matters
&lt;/h3&gt;

&lt;p&gt;One of the easiest reliability wins is limiting retrieval.&lt;/p&gt;

&lt;p&gt;Don’t do this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetch everything&lt;/li&gt;
&lt;li&gt;dump it all into context&lt;/li&gt;
&lt;li&gt;hope the model sorts it out&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do this instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agents&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FileSearchTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WebSearchTool&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ResearchWorker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;WebSearchTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;search_content_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;image_settings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;caption&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;FileSearchTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;max_num_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;vector_store_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VECTOR_STORE_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even here, I’d usually split this into two workers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one worker for web&lt;/li&gt;
&lt;li&gt;one worker for internal files&lt;/li&gt;
&lt;li&gt;supervisor decides which one to call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not elegance.&lt;br&gt;
The point is fewer weird failures.&lt;/p&gt;
&lt;h2&gt;
  
  
  What breaks real agents is boring operational stuff
&lt;/h2&gt;

&lt;p&gt;Most agent demos don’t die on reasoning.&lt;/p&gt;

&lt;p&gt;They die on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP timeouts&lt;/li&gt;
&lt;li&gt;429s from Notion&lt;/li&gt;
&lt;li&gt;malformed Jira payloads&lt;/li&gt;
&lt;li&gt;partial failures halfway through a long run&lt;/li&gt;
&lt;li&gt;state that gets corrupted after a retry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why long-running workflow tooling matters.&lt;/p&gt;

&lt;p&gt;If your automation touches Gmail, HubSpot, PostgreSQL, Slack, and an internal API, the main problem is often not “smarter prompting.”&lt;/p&gt;

&lt;p&gt;It’s resumability.&lt;br&gt;
Checkpointing.&lt;br&gt;
Explicit failure handling.&lt;/p&gt;

&lt;p&gt;That’s why tools like LangGraph are interesting.&lt;/p&gt;

&lt;p&gt;Not because they make prompts prettier.&lt;br&gt;
Because they treat agents like workflows.&lt;/p&gt;

&lt;p&gt;Install is simple:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The bigger shift is mental.&lt;/p&gt;

&lt;p&gt;You have to admit your agent is not just a chat loop.&lt;/p&gt;

&lt;p&gt;It is a workflow with failure modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  State is not one thing
&lt;/h2&gt;

&lt;p&gt;This part gets ignored a lot.&lt;/p&gt;

&lt;p&gt;People say “context” like it’s one big blob.&lt;/p&gt;

&lt;p&gt;It isn’t.&lt;/p&gt;

&lt;p&gt;There’s session state, tool state, conversation history, sandbox state, intermediate outputs, retry metadata, and final outputs.&lt;/p&gt;

&lt;p&gt;When teams blur all of that together, agents get haunted fast.&lt;/p&gt;

&lt;p&gt;You start seeing things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;today’s invoice parser using yesterday’s customer record&lt;/li&gt;
&lt;li&gt;a failed code execution polluting the next task&lt;/li&gt;
&lt;li&gt;retries reusing stale tool outputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A supervisor layer helps because it can control state transitions explicitly.&lt;/p&gt;

&lt;p&gt;Something like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;open a fresh job session&lt;/li&gt;
&lt;li&gt;pass only required state to each worker&lt;/li&gt;
&lt;li&gt;checkpoint outputs after each step&lt;/li&gt;
&lt;li&gt;isolate execution environments&lt;/li&gt;
&lt;li&gt;clean up resources at the end&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is much easier to debug than one giant conversational loop with mystery memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which stack should you use?
&lt;/h2&gt;

&lt;p&gt;My blunt version:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Best use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Responses API&lt;/td&gt;
&lt;td&gt;Short-lived workflows where you want direct control over tool dispatch, loop logic, and state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Agents SDK&lt;/td&gt;
&lt;td&gt;Multi-agent workflows where you want handoffs, guardrails, sessions, tracing, and a managed runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LangGraph&lt;/td&gt;
&lt;td&gt;Long-running, stateful automations where durable execution and resume-after-failure matter most&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the tradeoff view:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What you’re really signing up for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Responses API&lt;/td&gt;
&lt;td&gt;More plumbing, more control, less hidden behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Agents SDK&lt;/td&gt;
&lt;td&gt;Good default if you want supervisor-style structure without building every runtime piece yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LangGraph&lt;/td&gt;
&lt;td&gt;More workflow engineering up front, much better operational behavior for complex automations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you’re already building in Python, the OpenAI Agents SDK is a pretty reasonable place to start.&lt;/p&gt;

&lt;p&gt;If your automations run long and touch lots of systems, I’d look hard at LangGraph.&lt;/p&gt;

&lt;p&gt;If the workflow is short and deterministic, the Responses API may be enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is overkill
&lt;/h2&gt;

&lt;p&gt;Not every automation needs a mini control plane.&lt;/p&gt;

&lt;p&gt;If your agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;uses one or two deterministic tools&lt;/li&gt;
&lt;li&gt;runs for a short time&lt;/li&gt;
&lt;li&gt;has simple success/failure logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then a single loop with strict schemas may be fine.&lt;/p&gt;

&lt;p&gt;I would not build a tiny bureaucracy for a support-ticket draft assistant that reads one knowledge base and writes one reply.&lt;/p&gt;

&lt;p&gt;The point is not “always build a supervisor.”&lt;/p&gt;

&lt;p&gt;The point is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;stop pretending one model call should be the supervisor.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My rule now
&lt;/h2&gt;

&lt;p&gt;If an agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;has more than a couple tools&lt;/li&gt;
&lt;li&gt;touches external APIs&lt;/li&gt;
&lt;li&gt;runs long enough that restart is painful&lt;/li&gt;
&lt;li&gt;needs retries that aren’t trivial&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then I assume it needs supervision.&lt;/p&gt;

&lt;p&gt;Not because the models are weak.&lt;/p&gt;

&lt;p&gt;GPT-5, Claude Opus 4.6, and Grok 4.20 can all be excellent workers.&lt;/p&gt;

&lt;p&gt;But production reliability usually comes from workflow design, not model heroics.&lt;/p&gt;

&lt;p&gt;The smartest model in the world will still have a bad day if you make it do planning, routing, validation, retries, cleanup, and summarization inside one fuzzy loop.&lt;/p&gt;

&lt;p&gt;Make the model good at the thing it should do.&lt;/p&gt;

&lt;p&gt;Keep orchestration in the runtime.&lt;/p&gt;

&lt;p&gt;That one change made my tool calls a lot less cursed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical checklist
&lt;/h2&gt;

&lt;p&gt;If you want to clean up a flaky tool-calling agent this week, I’d start here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Split planner/supervisor from worker tasks
[ ] Reduce tool access per worker
[ ] Add explicit retry rules outside the prompt
[ ] Bound retrieval results (ex: max_num_results=3)
[ ] Validate tool outputs with code, not instructions alone
[ ] Separate session state from task state
[ ] Add tracing so failures are inspectable
[ ] Decide what should retry vs escalate vs fail fast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gets you much further than another 300 tokens of prompt engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing if you run agents all day
&lt;/h2&gt;

&lt;p&gt;Once you start breaking agents into supervisors, workers, retries, and long-running loops, usage goes up fast.&lt;/p&gt;

&lt;p&gt;That’s when per-token pricing starts messing with architecture decisions.&lt;/p&gt;

&lt;p&gt;Teams end up optimizing for cost anxiety instead of reliability.&lt;br&gt;
They avoid retries.&lt;br&gt;
They shrink context too aggressively.&lt;br&gt;
They hesitate to let automations run continuously.&lt;/p&gt;

&lt;p&gt;That’s exactly why tools like Standard Compute are interesting for agent-heavy workflows.&lt;/p&gt;

&lt;p&gt;It gives you an OpenAI-compatible API with flat monthly pricing, so you can run automations, supervisors, and worker loops without staring at token burn all day.&lt;/p&gt;

&lt;p&gt;If you’re building in n8n, Make, Zapier, OpenClaw, or your own Python stack, predictable pricing changes the way you design systems.&lt;/p&gt;

&lt;p&gt;You can afford to build the safer architecture instead of the cheapest-looking demo.&lt;/p&gt;

&lt;p&gt;And for agents, that trade is usually worth it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>We thought our GPT-5.4 agent got lazier in production — it was a 3-bug workflow teaching it to quit</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Thu, 01 Oct 2026 22:10:28 +0000</pubDate>
      <link>https://dev.to/lars_winstand/we-thought-our-gpt-54-agent-got-lazier-in-production-it-was-a-3-bug-workflow-teaching-it-to-quit-1b3b</link>
      <guid>https://dev.to/lars_winstand/we-thought-our-gpt-54-agent-got-lazier-in-production-it-was-a-3-bug-workflow-teaching-it-to-quit-1b3b</guid>
      <description>&lt;h1&gt;
  
  
  We thought our GPT-5.4 agent got lazier in production — it was a 3-bug workflow teaching it to quit
&lt;/h1&gt;

&lt;p&gt;We had an n8n agent that looked great in staging.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;search&lt;/li&gt;
&lt;li&gt;pull docs&lt;/li&gt;
&lt;li&gt;compare sources&lt;/li&gt;
&lt;li&gt;verify claims&lt;/li&gt;
&lt;li&gt;write a grounded answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then we shipped it.&lt;/p&gt;

&lt;p&gt;In production, the same task started ending after one shallow pass.&lt;/p&gt;

&lt;p&gt;Symptoms were exactly what people usually call “model laziness”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shorter outputs&lt;/li&gt;
&lt;li&gt;fewer tool calls&lt;/li&gt;
&lt;li&gt;more confident wrong answers&lt;/li&gt;
&lt;li&gt;less evidence of multi-step reasoning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our first instinct was to blame GPT-5.4.&lt;/p&gt;

&lt;p&gt;That was the wrong diagnosis.&lt;/p&gt;

&lt;p&gt;The real issue was boring and very fixable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;retry cap dropped from &lt;code&gt;6&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;one n8n branch treated a partial answer as success&lt;/li&gt;
&lt;li&gt;our OpenAI-compatible API path was rewarding the first acceptable-looking response instead of the best one&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once we fixed the workflow, quality came back.&lt;/p&gt;

&lt;p&gt;That changed how I think about “lazy” agents in production.&lt;/p&gt;

&lt;p&gt;Most of the time, the model did not suddenly get worse. Your orchestration started ending runs early.&lt;/p&gt;

&lt;h2&gt;
  
  
  The production symptom that fooled us
&lt;/h2&gt;

&lt;p&gt;In staging, the agent trace looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;retrieve sources&lt;/li&gt;
&lt;li&gt;inspect docs&lt;/li&gt;
&lt;li&gt;compare claims&lt;/li&gt;
&lt;li&gt;verify one weak point&lt;/li&gt;
&lt;li&gt;synthesize answer&lt;/li&gt;
&lt;li&gt;return final output&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In production, it looked more like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;retrieve one weak source&lt;/li&gt;
&lt;li&gt;write answer anyway&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same task class. Same model family. Very different behavior.&lt;/p&gt;

&lt;p&gt;And because the final answer still looked polished, it passed casual review more often than it should have.&lt;/p&gt;

&lt;p&gt;That is the dangerous part.&lt;/p&gt;

&lt;p&gt;A broken agent rarely looks broken in an obvious way. It often looks efficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3 bugs that made GPT-5.4 look lazy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Retry cap dropped from 6 to 2
&lt;/h3&gt;

&lt;p&gt;This was the biggest quality hit.&lt;/p&gt;

&lt;p&gt;For simple classification, &lt;code&gt;2&lt;/code&gt; retries can be fine.&lt;/p&gt;

&lt;p&gt;For research, debugging, document synthesis, or anything with tool use, &lt;code&gt;2&lt;/code&gt; is often a trap. One bad retrieval result plus one tool hiccup and the agent is out of budget.&lt;/p&gt;

&lt;p&gt;Example of the kind of config drift that causes this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"task_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"research"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"max_retries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timeout_seconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks harmless until your workflow depends on search + fetch + verify.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) A branch treated partial output as success
&lt;/h3&gt;

&lt;p&gt;In our n8n flow, one formatting branch said, effectively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if output matches schema&lt;/li&gt;
&lt;li&gt;and output length is above a minimum&lt;/li&gt;
&lt;li&gt;mark run successful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means the agent could skip retrieval depth and still win.&lt;/p&gt;

&lt;p&gt;This is how you accidentally train an agent to stop early.&lt;/p&gt;

&lt;p&gt;Pseudo-logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nf"&gt;isValidJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;280&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not quality control.&lt;/p&gt;

&lt;p&gt;That is a shallow-answer reward function.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) The API path rewarded “acceptable” over “best”
&lt;/h3&gt;

&lt;p&gt;This one is common in OpenAI-compatible stacks.&lt;/p&gt;

&lt;p&gt;If your app accepts the first plausible answer and never checks whether the expected tool path ran, the orchestration layer starts selecting for speed, not depth.&lt;/p&gt;

&lt;p&gt;That can happen whether you are routing to GPT-5.4, Claude Opus 4.6, or Grok 4.20.&lt;/p&gt;

&lt;p&gt;The model is not “choosing to be lazy” in some abstract sense.&lt;/p&gt;

&lt;p&gt;Your workflow is telling it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;if you look done quickly enough, you pass&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why agents look worse in production than in staging
&lt;/h2&gt;

&lt;p&gt;Because production has constraints that staging often hides.&lt;/p&gt;

&lt;p&gt;In a clean test harness, a model usually gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one prompt&lt;/li&gt;
&lt;li&gt;predictable context&lt;/li&gt;
&lt;li&gt;generous timeout&lt;/li&gt;
&lt;li&gt;no weird branch logic&lt;/li&gt;
&lt;li&gt;no flaky tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production, the agent sits inside a box made of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry limits&lt;/li&gt;
&lt;li&gt;timeout settings&lt;/li&gt;
&lt;li&gt;parser requirements&lt;/li&gt;
&lt;li&gt;tool wrappers&lt;/li&gt;
&lt;li&gt;success conditions&lt;/li&gt;
&lt;li&gt;fallback branches&lt;/li&gt;
&lt;li&gt;queue pressure&lt;/li&gt;
&lt;li&gt;rate limiting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That box matters more than people want to admit.&lt;/p&gt;

&lt;p&gt;A strong model inside a bad loop will look worse than a decent model inside a clean loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fastest way to tell if the model is actually the problem
&lt;/h2&gt;

&lt;p&gt;Run the same task through the same scaffold and change one variable at a time.&lt;/p&gt;

&lt;p&gt;Not “same prompt, different environment.”&lt;/p&gt;

&lt;p&gt;Actually the same scaffold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;same system prompt&lt;/li&gt;
&lt;li&gt;same tool definitions&lt;/li&gt;
&lt;li&gt;same retry budget&lt;/li&gt;
&lt;li&gt;same max output settings&lt;/li&gt;
&lt;li&gt;same evaluator&lt;/li&gt;
&lt;li&gt;same API path&lt;/li&gt;
&lt;li&gt;same success criteria&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you compare production n8n against a clean notebook script, you are not isolating the model.&lt;/p&gt;

&lt;p&gt;You are changing the entire experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The debugging signal that mattered most: stop reason
&lt;/h2&gt;

&lt;p&gt;Not vibes.&lt;/p&gt;

&lt;p&gt;Not output length alone.&lt;/p&gt;

&lt;p&gt;Not “this answer feels thinner.”&lt;/p&gt;

&lt;p&gt;Stop reasons told us far more than final-answer scoring.&lt;/p&gt;

&lt;p&gt;For Anthropic agents, useful stop reasons include values like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;end_turn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_tokens&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tool_use&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pause_turn&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For OpenAI-compatible workflows, inspect whether:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the expected tool calls actually fired&lt;/li&gt;
&lt;li&gt;the reasoning path was used when expected&lt;/li&gt;
&lt;li&gt;the run ended because the model finished&lt;/li&gt;
&lt;li&gt;or because your orchestration layer decided it had enough&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only evaluate the final answer, you are debugging blind.&lt;/p&gt;

&lt;h2&gt;
  
  
  What convinced us it was the workflow, not GPT-5.4
&lt;/h2&gt;

&lt;p&gt;We ran the same broken production scaffold against multiple model families.&lt;/p&gt;

&lt;p&gt;What we saw:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPT-5.4 looked bad under the production n8n flow&lt;/li&gt;
&lt;li&gt;GPT-5.4 looked fine under the staging flow&lt;/li&gt;
&lt;li&gt;Claude Opus 4.6 also looked bad under the broken production flow&lt;/li&gt;
&lt;li&gt;Grok 4.20 looked bad too&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That pattern matters.&lt;/p&gt;

&lt;p&gt;When three strong models all become “lazy” in the same way, the workflow is usually guilty.&lt;/p&gt;

&lt;p&gt;Here is the mental model I use now:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If this changes&lt;/th&gt;
&lt;th&gt;Suspect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One model regresses, others stay stable&lt;/td&gt;
&lt;td&gt;model or provider issue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All models regress under one workflow&lt;/td&gt;
&lt;td&gt;orchestration bug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output gets shorter after retry/timeout changes&lt;/td&gt;
&lt;td&gt;early stopping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON validity improves while answer quality drops&lt;/td&gt;
&lt;td&gt;parser-first reward problem&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The false leads we chased first
&lt;/h2&gt;

&lt;p&gt;We spent too long blaming the model layer.&lt;/p&gt;

&lt;p&gt;Our guesses were reasonable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;maybe GPT-5.4 regressed&lt;/li&gt;
&lt;li&gt;maybe the OpenAI Responses API settings changed behavior&lt;/li&gt;
&lt;li&gt;maybe reasoning effort was too low&lt;/li&gt;
&lt;li&gt;maybe output token limits were clipping the answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are all real failure modes.&lt;/p&gt;

&lt;p&gt;They just were not the main problem here.&lt;/p&gt;

&lt;p&gt;The actual issue was simpler:&lt;/p&gt;

&lt;p&gt;staging rewarded grounded completion&lt;br&gt;
production rewarded acceptable formatting&lt;/p&gt;

&lt;p&gt;Agents optimize for whatever your workflow rewards.&lt;/p&gt;

&lt;p&gt;If your automation says “close enough,” GPT-5.4, Claude Opus 4.6, and Grok 4.20 will all start looking suspiciously eager to be done.&lt;/p&gt;
&lt;h2&gt;
  
  
  Workflow patterns that create fake laziness
&lt;/h2&gt;

&lt;p&gt;These are the ones I would audit first.&lt;/p&gt;
&lt;h3&gt;
  
  
  Parser-first design
&lt;/h3&gt;

&lt;p&gt;If the main objective is valid JSON, many agents will satisfy the parser before they satisfy the task.&lt;/p&gt;

&lt;p&gt;Example smell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That should almost never be the whole success condition for a research task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Direct-answer escape hatches
&lt;/h3&gt;

&lt;p&gt;If n8n, Make, Zapier, OpenClaw, LangGraph, or your custom loop allows a final answer before retrieval or verification, expect shallow completions.&lt;/p&gt;

&lt;p&gt;For research-class tasks, tool use often should not be optional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tiny retry budgets on multi-step tasks
&lt;/h3&gt;

&lt;p&gt;This one is everywhere.&lt;/p&gt;

&lt;p&gt;People use one global retry budget for everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;max_retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That might be fine for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;classification&lt;/li&gt;
&lt;li&gt;extraction&lt;/li&gt;
&lt;li&gt;light transformations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is usually bad for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;web research&lt;/li&gt;
&lt;li&gt;code debugging&lt;/li&gt;
&lt;li&gt;document comparison&lt;/li&gt;
&lt;li&gt;multi-source synthesis&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Evaluating only the final message
&lt;/h3&gt;

&lt;p&gt;This is the worst one.&lt;/p&gt;

&lt;p&gt;If you only score the final text, you hide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;skipped tool calls&lt;/li&gt;
&lt;li&gt;failed searches&lt;/li&gt;
&lt;li&gt;parser shortcuts&lt;/li&gt;
&lt;li&gt;premature exits&lt;/li&gt;
&lt;li&gt;timeout-driven summaries pretending to be conclusions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My strong opinion: this single habit causes teams to think they are comparing models when they are actually comparing orchestration mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed
&lt;/h2&gt;

&lt;p&gt;We did not switch models.&lt;/p&gt;

&lt;p&gt;We changed the workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 1: restore retry budget
&lt;/h3&gt;

&lt;p&gt;We moved the retry cap back from &lt;code&gt;2&lt;/code&gt; to &lt;code&gt;6&lt;/code&gt; for research-class tasks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;agent_profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;classification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;max_retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;research&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;max_retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;
  &lt;span class="na"&gt;debugging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;max_retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fix 2: require retrieval for research tasks
&lt;/h3&gt;

&lt;p&gt;We added a hard gate.&lt;/p&gt;

&lt;p&gt;If the task is research, at least one retrieval step must happen before a run can pass.&lt;/p&gt;

&lt;p&gt;Pseudo-code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;research&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing_required_retrieval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputSchemaValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid_schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fix 3: stop treating formatting as quality
&lt;/h3&gt;

&lt;p&gt;We changed success conditions so a run could not pass on formatting alone.&lt;/p&gt;

&lt;p&gt;That meant checking trajectory, not just output shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 4: compare traces, not just answers
&lt;/h3&gt;

&lt;p&gt;We reviewed traces side by side in our observability stack and checked stop reasons across both the OpenAI-compatible path and the Anthropic path.&lt;/p&gt;

&lt;p&gt;That made the difference obvious fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical checklist for debugging “lazy” agents
&lt;/h2&gt;

&lt;p&gt;If you think your production agent got worse, this is the order I would check things in.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Diff staging vs production config
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diff staging-agent.yaml production-agent.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for changes in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry caps&lt;/li&gt;
&lt;li&gt;timeouts&lt;/li&gt;
&lt;li&gt;token/output limits&lt;/li&gt;
&lt;li&gt;tool availability&lt;/li&gt;
&lt;li&gt;branch logic&lt;/li&gt;
&lt;li&gt;fallback behavior&lt;/li&gt;
&lt;li&gt;evaluator thresholds&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Inspect stop reasons
&lt;/h3&gt;

&lt;p&gt;Log them explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-5.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stop_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"end_turn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"task_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"research"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If research tasks are ending with zero tool calls and still passing, that is your bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Count tool calls per task type
&lt;/h3&gt;

&lt;p&gt;A simple metric catches a lot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_call_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_tool_calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_retries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_chars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_output_chars&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;agent_runs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If tool-call counts collapse after a deploy, investigate the workflow before blaming the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Compare multiple model families under the same scaffold
&lt;/h3&gt;

&lt;p&gt;This is where an OpenAI-compatible API setup helps.&lt;/p&gt;

&lt;p&gt;If GPT-5.4, Claude Opus 4.6, and Grok 4.20 all fail the same way under one loop, the loop is probably broken.&lt;/p&gt;

&lt;h3&gt;
  
  
  5) Score trajectory, not just answer text
&lt;/h3&gt;

&lt;p&gt;Track things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retrieval attempted&lt;/li&gt;
&lt;li&gt;verification attempted&lt;/li&gt;
&lt;li&gt;tool errors encountered&lt;/li&gt;
&lt;li&gt;stop reason&lt;/li&gt;
&lt;li&gt;retries used&lt;/li&gt;
&lt;li&gt;output grounded in cited material&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this matters more when you run lots of agents
&lt;/h2&gt;

&lt;p&gt;This kind of bug gets expensive fast when you are running automations all day.&lt;/p&gt;

&lt;p&gt;Not just in dollars. In bad outputs, hidden regressions, and wasted debugging time.&lt;/p&gt;

&lt;p&gt;Teams running agents in n8n, Make, Zapier, OpenClaw, or custom OpenAI-compatible stacks usually hit the same wall:&lt;/p&gt;

&lt;p&gt;they start by asking “which model is best?”&lt;/p&gt;

&lt;p&gt;Then eventually they realize the more useful question is:&lt;/p&gt;

&lt;p&gt;“what exactly is our workflow rewarding?”&lt;/p&gt;

&lt;p&gt;That is also why predictable API infrastructure matters.&lt;/p&gt;

&lt;p&gt;When you can swap models without rewriting your stack, compare traces cleanly, and run lots of evals without per-token anxiety, it gets much easier to find orchestration bugs instead of arguing about vibes.&lt;/p&gt;

&lt;p&gt;That is a big part of why Standard Compute is interesting for agent teams: it is a drop-in OpenAI-compatible API, so you can keep your existing SDKs and workflows, route across GPT-5.4, Claude Opus 4.6, and Grok 4.20, and test agent behavior without every debugging session turning into a billing event.&lt;/p&gt;

&lt;p&gt;For teams running automations 24/7, flat monthly pricing is not just a finance preference. It changes how aggressively you can evaluate, compare, and fix agent systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I use now
&lt;/h2&gt;

&lt;p&gt;Before blaming GPT-5.4 for getting lazy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inspect the trajectory&lt;/li&gt;
&lt;li&gt;check stop reasons&lt;/li&gt;
&lt;li&gt;compare the exact same scaffold&lt;/li&gt;
&lt;li&gt;verify required tool steps happened&lt;/li&gt;
&lt;li&gt;make sure your success condition is rewarding quality, not just completion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes a model really does regress.&lt;/p&gt;

&lt;p&gt;More often, production taught your agent that quitting early is the winning move.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>n8n</category>
      <category>devops</category>
    </item>
    <item>
      <title>I thought my n8n LLM bug was OpenAI and it turned out to be 5 boring workflow failures</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Wed, 30 Sep 2026 14:11:15 +0000</pubDate>
      <link>https://dev.to/lars_winstand/i-thought-my-n8n-llm-bug-was-openai-and-it-turned-out-to-be-5-boring-workflow-failures-3fbk</link>
      <guid>https://dev.to/lars_winstand/i-thought-my-n8n-llm-bug-was-openai-and-it-turned-out-to-be-5-boring-workflow-failures-3fbk</guid>
      <description>&lt;h1&gt;
  
  
  I thought my n8n LLM bug was OpenAI and it turned out to be 5 boring workflow failures
&lt;/h1&gt;

&lt;p&gt;I had one of those bugs that makes you immediately blame the model provider.&lt;/p&gt;

&lt;p&gt;The workflow was still spinning.&lt;br&gt;
The user had no response.&lt;br&gt;
There was no obvious error.&lt;br&gt;
And the AI step was the last interesting thing in the execution.&lt;/p&gt;

&lt;p&gt;So I did the obvious bad move: I blamed OpenAI, retried the run, and piled more retries onto a workflow that was already jammed.&lt;/p&gt;

&lt;p&gt;The thing that broke the illusion was simple:&lt;/p&gt;

&lt;p&gt;The provider logs showed the model had already responded.&lt;/p&gt;

&lt;p&gt;n8n was the thing still hanging.&lt;/p&gt;

&lt;p&gt;That was the moment I had to admit the bug was in my orchestration layer, not in the model.&lt;/p&gt;

&lt;p&gt;If you run AI agents in n8n, Make, Zapier, OpenClaw, or your own queue-based workflow stack, this matters a lot:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A lot of “the LLM stopped responding” bugs are not model outages. They’re worker backlog, Redis payload issues, missing timeouts, BullMQ stalls, or agent loops.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I lost hours to this once. You probably don’t need to.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fast sanity check that changed how I debug this
&lt;/h2&gt;

&lt;p&gt;Before touching prompts, retries, or provider dashboards, compare two timestamps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When the provider says the request completed&lt;/li&gt;
&lt;li&gt;When your workflow says the execution completed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the provider finished at &lt;code&gt;12:01:04&lt;/code&gt; and your client is still hanging at &lt;code&gt;12:01:45&lt;/code&gt;, the model is not your first suspect anymore.&lt;/p&gt;

&lt;p&gt;That’s an orchestration problem until proven otherwise.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why n8n makes this look like a model outage
&lt;/h2&gt;

&lt;p&gt;When you’re tired and staring at a stuck execution, it &lt;em&gt;feels&lt;/em&gt; like the model call is frozen.&lt;/p&gt;

&lt;p&gt;But in queue mode, the path is longer than people remember:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the client stays connected to the main or webhook instance&lt;/li&gt;
&lt;li&gt;the execution may run on a worker&lt;/li&gt;
&lt;li&gt;the result has to come back through Redis&lt;/li&gt;
&lt;li&gt;the workflow still has to serialize, pass data around, and finish cleanly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any slowdown in that path can look exactly like “OpenAI stopped responding.”&lt;/p&gt;

&lt;p&gt;That’s why this bug is so annoying. The model can be done, but the user still sees a spinner.&lt;/p&gt;
&lt;h2&gt;
  
  
  The 5 workflow bugs I now check before blaming the model
&lt;/h2&gt;

&lt;p&gt;These are the five issues I found, in the order I’d check them now.&lt;/p&gt;


&lt;h2&gt;
  
  
  1) n8n was waiting on a worker, not on the LLM
&lt;/h2&gt;
&lt;h3&gt;
  
  
  What it looked like
&lt;/h3&gt;

&lt;p&gt;The execution sat long enough that it felt like a dead model call.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why it fooled me
&lt;/h3&gt;

&lt;p&gt;From the outside, these look identical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;model is slow&lt;/li&gt;
&lt;li&gt;worker is backed up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all you have is a hanging HTTP request, your brain picks the AI step every time.&lt;/p&gt;
&lt;h3&gt;
  
  
  What I checked
&lt;/h3&gt;

&lt;p&gt;I compared provider-side completion time with workflow completion time, then checked queue delay and worker throughput.&lt;/p&gt;
&lt;h3&gt;
  
  
  What proved it
&lt;/h3&gt;

&lt;p&gt;The provider had already returned the response.&lt;br&gt;
The worker path had not finished processing it.&lt;/p&gt;
&lt;h3&gt;
  
  
  What I’d do now
&lt;/h3&gt;

&lt;p&gt;Check worker pressure first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example things to inspect in your deployment&lt;/span&gt;
&lt;span class="c"&gt;# Docker&lt;/span&gt;
docker ps
docker logs n8n-worker &lt;span class="nt"&gt;--tail&lt;/span&gt; 200

&lt;span class="c"&gt;# Kubernetes&lt;/span&gt;
kubectl get pods
kubectl logs deployment/n8n-worker &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you’re using queue mode, look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;too few workers&lt;/li&gt;
&lt;li&gt;workers stuck on other jobs&lt;/li&gt;
&lt;li&gt;queue backlog spikes&lt;/li&gt;
&lt;li&gt;long execution times unrelated to provider latency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If upstream finished and your workflow didn’t, the queue is guilty until proven innocent.&lt;/p&gt;




&lt;h2&gt;
  
  
  2) Redis was carrying a huge response through the queue
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What it looked like
&lt;/h3&gt;

&lt;p&gt;Small runs worked.&lt;br&gt;
Large runs felt flaky or frozen.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why it fooled me
&lt;/h3&gt;

&lt;p&gt;Big outputs make people suspect token generation or streaming first.&lt;/p&gt;

&lt;p&gt;That’s reasonable, but wrong surprisingly often.&lt;/p&gt;
&lt;h3&gt;
  
  
  What I checked
&lt;/h3&gt;

&lt;p&gt;I compared small-output runs vs large-output runs and looked at where the delay appeared.&lt;/p&gt;
&lt;h3&gt;
  
  
  What proved it
&lt;/h3&gt;

&lt;p&gt;The slowdown tracked payload size, not model latency.&lt;/p&gt;

&lt;p&gt;That means the bottleneck was response handoff, serialization, queue transport, or downstream processing.&lt;/p&gt;
&lt;h3&gt;
  
  
  Practical fix
&lt;/h3&gt;

&lt;p&gt;Stop shipping giant blobs through your workflow if you don’t need to.&lt;/p&gt;

&lt;p&gt;Bad pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"full_context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"very large text..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"raw_model_output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"huge output..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"resp_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"short result for next step"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"storage_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://bucket/run-123/output.json"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your agent produces large intermediate artifacts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;store them in object storage&lt;/li&gt;
&lt;li&gt;pass references, not full payloads&lt;/li&gt;
&lt;li&gt;trim chat history aggressively&lt;/li&gt;
&lt;li&gt;avoid unnecessary JSON expansion between nodes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A giant JSON blob moving through Redis is one of the most boring ways to fake an LLM outage.&lt;/p&gt;




&lt;h2&gt;
  
  
  3) I had no real timeout, so the workflow could wait forever
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What it looked like
&lt;/h3&gt;

&lt;p&gt;Requests didn’t fail cleanly.&lt;br&gt;
They just kept hanging.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why it fooled me
&lt;/h3&gt;

&lt;p&gt;No error means people assume “the model is still thinking.”&lt;/p&gt;

&lt;p&gt;That’s a terrible default assumption.&lt;/p&gt;
&lt;h3&gt;
  
  
  What I checked
&lt;/h3&gt;

&lt;p&gt;I audited timeout settings in three places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the HTTP client&lt;/li&gt;
&lt;li&gt;the n8n workflow or AI node&lt;/li&gt;
&lt;li&gt;any proxy or webhook layer in front&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  What proved it
&lt;/h3&gt;

&lt;p&gt;There was no meaningful upper bound forcing the run to fail fast and surface the actual bottleneck.&lt;/p&gt;
&lt;h3&gt;
  
  
  Practical fix
&lt;/h3&gt;

&lt;p&gt;Set explicit timeouts everywhere.&lt;/p&gt;

&lt;p&gt;Example Node.js request with a timeout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;45000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.openai-compatible-endpoint.com/v1/chat/completions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-5.4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Summarize this ticket&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in workflow systems, I’d strongly recommend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;per-step timeouts&lt;/li&gt;
&lt;li&gt;execution-level timeouts&lt;/li&gt;
&lt;li&gt;retry limits&lt;/li&gt;
&lt;li&gt;dead-letter handling for repeated failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A timeout is not just for protection. It’s a debugging tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  4) BullMQ stalled because the worker blocked the event loop
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What it looked like
&lt;/h3&gt;

&lt;p&gt;Jobs were retried or marked stalled even though the code looked fine at first glance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it fooled me
&lt;/h3&gt;

&lt;p&gt;The agent appeared to stop mid-task, and the LLM call was the easiest thing to blame.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I checked
&lt;/h3&gt;

&lt;p&gt;I looked for CPU-heavy work inside the same Node.js worker process handling queue jobs.&lt;/p&gt;

&lt;p&gt;Typical offenders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;giant JSON parsing&lt;/li&gt;
&lt;li&gt;expensive regex work&lt;/li&gt;
&lt;li&gt;PDF extraction&lt;/li&gt;
&lt;li&gt;big Markdown transformations&lt;/li&gt;
&lt;li&gt;synchronous loops over large payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What proved it
&lt;/h3&gt;

&lt;p&gt;The worker blocked the event loop long enough for BullMQ to treat the job as stalled.&lt;/p&gt;

&lt;p&gt;If you block the event loop for 30+ seconds, that’s not an OpenAI problem.&lt;br&gt;
That’s your worker design.&lt;/p&gt;
&lt;h3&gt;
  
  
  Minimal example of the kind of thing that hurts
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad: CPU-heavy work in the queue worker process&lt;/span&gt;
&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;huge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bigPayload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// pretend this is expensive&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// blocking the event loop&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;huge&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Better options
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;move CPU-heavy work to a separate service&lt;/li&gt;
&lt;li&gt;chunk large transforms&lt;/li&gt;
&lt;li&gt;use worker threads for CPU-bound work&lt;/li&gt;
&lt;li&gt;keep queue workers focused on orchestration, not heavy processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Useful checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Watch CPU and memory&lt;/span&gt;
htop

&lt;span class="c"&gt;# If containerized&lt;/span&gt;
docker stats
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your queue worker is doing too much non-I/O work, your “LLM outage” may just be a blocked Node process.&lt;/p&gt;




&lt;h2&gt;
  
  
  5) LangGraph was looping until recursion limits kicked in
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What it looked like
&lt;/h3&gt;

&lt;p&gt;The agent kept “working” but never gave a useful final answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it fooled me
&lt;/h3&gt;

&lt;p&gt;From the user’s perspective, a loop and a hang feel the same.&lt;/p&gt;

&lt;p&gt;No answer is no answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I checked
&lt;/h3&gt;

&lt;p&gt;I traced graph transitions and counted repeated states.&lt;/p&gt;

&lt;h3&gt;
  
  
  What proved it
&lt;/h3&gt;

&lt;p&gt;The graph was revisiting the same path until recursion limits stopped it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I’d do now
&lt;/h3&gt;

&lt;p&gt;Instrument state transitions aggressively.&lt;/p&gt;

&lt;p&gt;Pseudo-example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;visited&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;visited_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recent_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And add hard guards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;max iterations&lt;/li&gt;
&lt;li&gt;repeated-state detection&lt;/li&gt;
&lt;li&gt;explicit terminal states&lt;/li&gt;
&lt;li&gt;tool-call limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your agent can re-enter the same state forever, it’s not autonomous. It’s a polite infinite loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  My current debugging order for “LLM stopped responding”
&lt;/h2&gt;

&lt;p&gt;This is the checklist I wish I had used first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Check provider status page and request logs
2. Confirm whether the provider actually completed the request
3. Compare provider completion timestamp with n8n/BullMQ/LangGraph timing
4. Check queue backlog and worker health
5. Inspect payload size and response handoff path
6. Verify timeouts at client, workflow, and infrastructure layers
7. Look for event loop blocking or CPU-heavy worker tasks
8. Trace agent loops and recursion behavior
9. Only then blame the model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That order is less dramatic than posting “OpenAI is down again,” but it gets to the truth faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical example: compare provider logs to workflow timing
&lt;/h2&gt;

&lt;p&gt;If you’re using an OpenAI-compatible endpoint, your app code probably already gives you enough hooks to do this.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-5.4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Classify this support ticket&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;providerReturnedAtMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;responseId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then compare that with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;n8n execution duration&lt;/li&gt;
&lt;li&gt;worker completion time&lt;/li&gt;
&lt;li&gt;webhook response time&lt;/li&gt;
&lt;li&gt;downstream node timing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If provider time is 4 seconds and user-visible completion is 45 seconds, the model is not your biggest problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this gets expensive fast in agent workflows
&lt;/h2&gt;

&lt;p&gt;This is the part people underestimate.&lt;/p&gt;

&lt;p&gt;False outage diagnosis is expensive.&lt;/p&gt;

&lt;p&gt;Every blind retry can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clog your queue more&lt;/li&gt;
&lt;li&gt;duplicate downstream work&lt;/li&gt;
&lt;li&gt;increase latency for unrelated jobs&lt;/li&gt;
&lt;li&gt;burn more model usage&lt;/li&gt;
&lt;li&gt;make the original bug harder to see&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last part matters a lot if you’re paying per token and debugging high-volume automations. Retries during incident response can turn into their own bill.&lt;/p&gt;

&lt;p&gt;That’s one reason I like using OpenAI-compatible endpoints that are easy to swap in and out. If you can test the same workflow against another compatible provider without rewriting your app, it becomes much easier to separate provider issues from orchestration issues.&lt;/p&gt;

&lt;p&gt;And if your automation stack runs constantly, flat-rate compute is honestly a better fit than per-token pricing. During debugging, retries and test runs stop feeling like financial damage.&lt;/p&gt;

&lt;p&gt;That’s the appeal of something like &lt;a href="https://standardcompute.com" rel="noopener noreferrer"&gt;Standard Compute&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI-compatible API&lt;/li&gt;
&lt;li&gt;works with existing SDKs and HTTP clients&lt;/li&gt;
&lt;li&gt;useful for n8n, Make, Zapier, OpenClaw, and custom agent workflows&lt;/li&gt;
&lt;li&gt;flat monthly pricing instead of per-token billing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For teams running agents all day, that’s not just a pricing detail. It changes how safely you can test, retry, and debug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;The lesson is not that OpenAI, Anthropic, or Grok never fail.&lt;/p&gt;

&lt;p&gt;They do.&lt;/p&gt;

&lt;p&gt;The lesson is that workflow bugs impersonate model outages extremely well.&lt;/p&gt;

&lt;p&gt;So now my default assumption is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the provider already answered, the bug is probably mine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That assumption has saved me more time than any prompt tweak ever has.&lt;/p&gt;

&lt;p&gt;If you’re debugging hanging LLM workflows in n8n or another automation stack, start with the response path, not the model.&lt;/p&gt;

&lt;p&gt;That’s usually where the real problem is hiding.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>ai</category>
      <category>openai</category>
      <category>devops</category>
    </item>
    <item>
      <title>I thought my cheap AI workflow was fine until I counted 3,000 tiny calls</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Wed, 30 Sep 2026 06:11:07 +0000</pubDate>
      <link>https://dev.to/lars_winstand/i-thought-my-cheap-ai-workflow-was-fine-until-i-counted-3000-tiny-calls-4eaa</link>
      <guid>https://dev.to/lars_winstand/i-thought-my-cheap-ai-workflow-was-fine-until-i-counted-3000-tiny-calls-4eaa</guid>
      <description>&lt;p&gt;I didn’t get burned by one giant GPT-5 prompt.&lt;/p&gt;

&lt;p&gt;I got burned by a workflow that looked cheap.&lt;/p&gt;

&lt;p&gt;You know the kind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetch a batch of records&lt;/li&gt;
&lt;li&gt;classify each one&lt;/li&gt;
&lt;li&gt;extract a field or two&lt;/li&gt;
&lt;li&gt;write a short summary&lt;/li&gt;
&lt;li&gt;move on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lead enrichment. Support triage. CRM cleanup. Scraped page normalization.&lt;/p&gt;

&lt;p&gt;Nothing fancy. Nothing that looks like it should trigger budget panic.&lt;/p&gt;

&lt;p&gt;And that’s exactly why it’s dangerous.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: every step is cheap, but the workflow isn’t
&lt;/h2&gt;

&lt;p&gt;The mistake is simple: people inspect one AI call at a time.&lt;/p&gt;

&lt;p&gt;They say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“It’s just a classifier.”&lt;/li&gt;
&lt;li&gt;“It’s only extracting pain points.”&lt;/li&gt;
&lt;li&gt;“The summary is two sentences.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All true.&lt;/p&gt;

&lt;p&gt;But nobody multiplies.&lt;/p&gt;

&lt;p&gt;Here’s a totally normal n8n flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fetch 1,000 leads
-&amp;gt; Loop Over Items (Batch Size: 1)
-&amp;gt; OpenAI node: classify company
-&amp;gt; OpenAI node: extract pain points
-&amp;gt; OpenAI node: draft outreach angle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is already:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1,000 records * 3 AI steps = 3,000 model calls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that’s before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;validation passes&lt;/li&gt;
&lt;li&gt;fallback prompts&lt;/li&gt;
&lt;li&gt;confidence scoring&lt;/li&gt;
&lt;li&gt;second-model QA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not an edge case. This is a standard automation pattern.&lt;/p&gt;

&lt;p&gt;If you build with n8n, Zapier, Make, or custom worker queues, you’ve probably done this already.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiny prompts can be the expensive part
&lt;/h2&gt;

&lt;p&gt;Not just in dollars.&lt;/p&gt;

&lt;p&gt;Also in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request throughput&lt;/li&gt;
&lt;li&gt;rate limits&lt;/li&gt;
&lt;li&gt;queueing behavior&lt;/li&gt;
&lt;li&gt;retry storms&lt;/li&gt;
&lt;li&gt;operational complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lot of teams focus on token count because that’s what pricing pages train you to do.&lt;/p&gt;

&lt;p&gt;But provider limits are usually not just about tokens.&lt;/p&gt;

&lt;p&gt;OpenAI separates RPM and TPM for a reason. You can be nowhere near your token-per-minute cap and still hit request-per-minute limits. Their docs explicitly call out the idea that if your RPM is 20, then 20 requests of only 100 tokens each can still max you out.&lt;/p&gt;

&lt;p&gt;That changes the architecture discussion.&lt;/p&gt;

&lt;p&gt;If your workload is “analyze one giant contract,” token cost is the problem.&lt;/p&gt;

&lt;p&gt;If your workload is “touch 8,000 CRM rows and make 3 tiny decisions on each,” request multiplication is usually the real problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this fools people in testing
&lt;/h2&gt;

&lt;p&gt;Because staging lies.&lt;/p&gt;

&lt;p&gt;A test run on 20 records looks cheap.&lt;/p&gt;

&lt;p&gt;Then someone points the workflow at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the whole HubSpot table&lt;/li&gt;
&lt;li&gt;the full Zendesk queue&lt;/li&gt;
&lt;li&gt;a scraped Apify dataset&lt;/li&gt;
&lt;li&gt;a backlog of support tickets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And suddenly the cost shape changes.&lt;/p&gt;

&lt;p&gt;Not because prompts got bigger.&lt;/p&gt;

&lt;p&gt;Because you turned on a machine that makes tiny calls thousands of times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick way to estimate whether your workflow is actually cheap
&lt;/h2&gt;

&lt;p&gt;I now do this before I trust any AI automation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;items_per_day&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000
&lt;span class="nv"&gt;ai_steps_per_item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;span class="nv"&gt;retry_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.1
&lt;span class="nv"&gt;validation_calls_per_item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1

&lt;span class="nv"&gt;base_calls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;items_per_day &lt;span class="o"&gt;*&lt;/span&gt; ai_steps_per_item&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nv"&gt;validation_calls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;items_per_day &lt;span class="o"&gt;*&lt;/span&gt; validation_calls_per_item&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nv"&gt;retry_calls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;python3 - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
items=1000
steps=3
retry_rate=0.1
print(int(items * steps * retry_rate))
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Base calls: &lt;/span&gt;&lt;span class="nv"&gt;$base_calls&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Validation calls: &lt;/span&gt;&lt;span class="nv"&gt;$validation_calls&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Retry calls: &lt;/span&gt;&lt;span class="nv"&gt;$retry_calls&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even rough math is enough.&lt;/p&gt;

&lt;p&gt;If the answer is “we’re making 4,000 to 10,000 model requests a day,” you do not have a tiny workflow.&lt;/p&gt;

&lt;p&gt;You have a high-frequency AI system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The official fixes are real, but they solve specific problems
&lt;/h2&gt;

&lt;p&gt;I’m not anti-OpenAI Batch API or anti-Anthropic prompt caching.&lt;/p&gt;

&lt;p&gt;Both are good.&lt;/p&gt;

&lt;p&gt;But they are not universal fixes for “my automation explodes into thousands of micro-calls.”&lt;/p&gt;

&lt;h2&gt;
  
  
  1) OpenAI Batch API: great if you can wait
&lt;/h2&gt;

&lt;p&gt;OpenAI’s Batch API is legitimately useful.&lt;/p&gt;

&lt;p&gt;It offers a 50% discount versus synchronous calls, and OpenAI explicitly positions it for jobs like large-scale classification and embeddings.&lt;/p&gt;

&lt;p&gt;That maps well to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nightly enrichment&lt;/li&gt;
&lt;li&gt;backfills&lt;/li&gt;
&lt;li&gt;offline cleanup jobs&lt;/li&gt;
&lt;li&gt;large async processing queues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example request shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"custom_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"request-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/v1/chat/completions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"gpt-3.5-turbo-0125"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"You are a helpful assistant."&lt;/span&gt;&lt;span class="p"&gt;},{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Hello world!"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="nl"&gt;"max_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But there are tradeoffs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;asynchronous only&lt;/li&gt;
&lt;li&gt;can take up to 24 hours&lt;/li&gt;
&lt;li&gt;no streaming&lt;/li&gt;
&lt;li&gt;not a drop-in answer for real-time routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your support workflow needs to classify a ticket now, Batch is not your answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  2) Anthropic prompt caching: amazing when the prefix repeats
&lt;/h2&gt;

&lt;p&gt;Anthropic prompt caching is also very real.&lt;/p&gt;

&lt;p&gt;When you have a large repeated prompt prefix, it can cut both latency and cost dramatically.&lt;/p&gt;

&lt;p&gt;That’s excellent for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;long system prompts&lt;/li&gt;
&lt;li&gt;many-shot examples&lt;/li&gt;
&lt;li&gt;repeated document context&lt;/li&gt;
&lt;li&gt;“chat with a book” style workloads&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-5-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cache_control&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ephemeral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are an AI assistant tasked with analyzing literary works.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Analyze the major themes in Pride and Prejudice.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But caching helps when requests share stable context.&lt;/p&gt;

&lt;p&gt;It does not magically fix workloads where every row is different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every Salesforce lead is unique&lt;/li&gt;
&lt;li&gt;every Zendesk ticket is unique&lt;/li&gt;
&lt;li&gt;every scraped page is unique&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can’t cache uniqueness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real question: what cost problem do you actually have?
&lt;/h2&gt;

&lt;p&gt;Most teams skip this and jump straight to model comparisons.&lt;/p&gt;

&lt;p&gt;That’s backwards.&lt;/p&gt;

&lt;p&gt;First figure out the shape of your workload.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI synchronous API&lt;/td&gt;
&lt;td&gt;Real-time flows where latency matters, but you still deal with per-token billing and RPM/TPM limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Batch API&lt;/td&gt;
&lt;td&gt;Large asynchronous jobs where lower cost matters more than immediate completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic prompt caching&lt;/td&gt;
&lt;td&gt;Repeated prompt prefixes, long shared context, and workloads that benefit from cache reuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-item multi-step automation in n8n, Make, or Zapier&lt;/td&gt;
&lt;td&gt;Easy to build, easy to underestimate, and very likely to multiply request count fast&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your workload is a few giant prompts, token pricing is the main issue.&lt;/p&gt;

&lt;p&gt;If your workload is thousands of tiny calls, the issue is usually a mix of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request count&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;fan-out per item&lt;/li&gt;
&lt;li&gt;provider rate limits&lt;/li&gt;
&lt;li&gt;automation platform task limits&lt;/li&gt;
&lt;li&gt;engineers getting conservative because every extra step feels billable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters more than people admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-token pricing changes how people design automations
&lt;/h2&gt;

&lt;p&gt;This is the hidden tax.&lt;/p&gt;

&lt;p&gt;Teams start making worse technical decisions because they’re trying not to trigger more model calls.&lt;/p&gt;

&lt;p&gt;I’ve seen teams do all of these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Remove QA steps because “it’s probably fine.”&lt;/li&gt;
&lt;li&gt;Skip classifying every inbound support ticket, so humans still sort manually.&lt;/li&gt;
&lt;li&gt;Avoid enriching lower-priority leads because the long tail feels too expensive.&lt;/li&gt;
&lt;li&gt;Collapse separate steps into one messy prompt because another call feels too costly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is not clean engineering.&lt;/p&gt;

&lt;p&gt;That is workflow design shaped by billing anxiety.&lt;/p&gt;

&lt;p&gt;And the model bill is not the only meter.&lt;/p&gt;

&lt;p&gt;You may also be managing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zapier task limits&lt;/li&gt;
&lt;li&gt;n8n execution volume&lt;/li&gt;
&lt;li&gt;OpenAI RPM and TPM&lt;/li&gt;
&lt;li&gt;Anthropic spend caps&lt;/li&gt;
&lt;li&gt;fallback routing logic across providers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At some point this stops being a prompt engineering problem and becomes systems design.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a lot of automations really are: tiny AI committees
&lt;/h2&gt;

&lt;p&gt;This is the mental model that finally made it click for me.&lt;/p&gt;

&lt;p&gt;A lot of record-level automations do not make one AI decision.&lt;/p&gt;

&lt;p&gt;They make a committee.&lt;/p&gt;

&lt;p&gt;For one item, you might do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;classify intent&lt;/li&gt;
&lt;li&gt;extract fields&lt;/li&gt;
&lt;li&gt;normalize to JSON&lt;/li&gt;
&lt;li&gt;score confidence&lt;/li&gt;
&lt;li&gt;draft a response&lt;/li&gt;
&lt;li&gt;run safety review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every call is defensible.&lt;/p&gt;

&lt;p&gt;Together, they behave like a swarm.&lt;/p&gt;

&lt;p&gt;That’s why I’m increasingly opinionated about this:&lt;/p&gt;

&lt;p&gt;For high-volume operational workflows, pricing model matters almost as much as model quality.&lt;/p&gt;

&lt;p&gt;Not because GPT-5, Claude Opus, Grok, Qwen, or Llama are bad.&lt;/p&gt;

&lt;p&gt;Because once your team stops fearing each micro-call, you build better automations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cleaner step separation&lt;/li&gt;
&lt;li&gt;better validation&lt;/li&gt;
&lt;li&gt;more second-pass QA&lt;/li&gt;
&lt;li&gt;longer-running agents&lt;/li&gt;
&lt;li&gt;fewer hacks to save pennies&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I’d do in practice
&lt;/h2&gt;

&lt;p&gt;If I’m building a real system, I’d break the problem down like this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use batching when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;the job is asynchronous&lt;/li&gt;
&lt;li&gt;latency does not matter&lt;/li&gt;
&lt;li&gt;the workload is large and predictable&lt;/li&gt;
&lt;li&gt;you want the cheapest possible processing path&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use caching when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;the prompt prefix is large and repeated&lt;/li&gt;
&lt;li&gt;many requests share the same context&lt;/li&gt;
&lt;li&gt;latency and cost both benefit from reuse&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Rethink pricing architecture when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;each item triggers multiple small calls&lt;/li&gt;
&lt;li&gt;workflows run continuously&lt;/li&gt;
&lt;li&gt;retries and validation are common&lt;/li&gt;
&lt;li&gt;the team is hesitating to add useful AI steps because of cost anxiety&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That third category is where a lot of agent and automation teams actually live.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical workflow audit you can run this week
&lt;/h2&gt;

&lt;p&gt;If you have an n8n, Make, Zapier, or custom agent workflow, map it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;workflow_audit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;items_per_day&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
  &lt;span class="na"&gt;ai_steps_per_item&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;average_retries_per_100_calls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;
  &lt;span class="na"&gt;validation_calls_per_item&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;fallback_model_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;real_time_steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;classify_ticket&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;route_priority&lt;/span&gt;
  &lt;span class="na"&gt;async_steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nightly_summary&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;enrichment_backfill&lt;/span&gt;
  &lt;span class="na"&gt;repeated_prompt_prefixes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;support_policy_context&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;extraction_schema&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then answer these questions honestly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many items arrive per day?&lt;/li&gt;
&lt;li&gt;How many AI nodes run per item?&lt;/li&gt;
&lt;li&gt;How many retries happen in production?&lt;/li&gt;
&lt;li&gt;How many validation or fallback calls happen after failures?&lt;/li&gt;
&lt;li&gt;Which steps truly need real-time latency?&lt;/li&gt;
&lt;li&gt;Which steps could be batched?&lt;/li&gt;
&lt;li&gt;Which prompts actually share enough prefix to benefit from caching?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That exercise usually reveals one of two stories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Story A: your problem is giant repeated context
&lt;/h2&gt;

&lt;p&gt;If that’s true, prompt caching can help a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Story B: your problem is death by 10,000 tiny calls
&lt;/h2&gt;

&lt;p&gt;If that’s true, you need to stop evaluating cost one prompt at a time.&lt;/p&gt;

&lt;p&gt;You need to think in workflow volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Standard Compute fits
&lt;/h2&gt;

&lt;p&gt;This is exactly why products like Standard Compute are interesting for agent and automation workloads.&lt;/p&gt;

&lt;p&gt;If you’re running lots of small calls across n8n, Make, Zapier, OpenClaw, or custom workers, flat-rate unlimited compute changes the design space.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Can we afford one more validation step?”&lt;/li&gt;
&lt;li&gt;“Should we skip QA on lower-value records?”&lt;/li&gt;
&lt;li&gt;“Can this agent run all day?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can build the workflow you actually want.&lt;/p&gt;

&lt;p&gt;Standard Compute is a drop-in OpenAI-compatible API, so you can usually swap it into existing SDKs or HTTP clients without rebuilding your stack. Under the hood it routes across models like GPT-5.4, Claude Opus 4.6, and Grok 4.20, with batching, prompt optimization, and throttling designed for exactly this kind of high-frequency automation work.&lt;/p&gt;

&lt;p&gt;That matters if your bottleneck is not one giant prompt.&lt;/p&gt;

&lt;p&gt;It matters if your bottleneck is thousands of tiny decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  My takeaway
&lt;/h2&gt;

&lt;p&gt;Before you optimize prompts, count calls.&lt;/p&gt;

&lt;p&gt;Before you compare GPT-5 vs Claude, count calls.&lt;/p&gt;

&lt;p&gt;Before you celebrate a cheap staging run, count calls.&lt;/p&gt;

&lt;p&gt;Most teams think their cost problem is “big prompts are expensive.”&lt;/p&gt;

&lt;p&gt;A surprising number actually have a different problem:&lt;/p&gt;

&lt;p&gt;small prompts, repeated constantly, inside workflows that looked harmless.&lt;/p&gt;

&lt;p&gt;That was my mistake.&lt;/p&gt;

&lt;p&gt;If you’re building AI automations, don’t price a single request.&lt;/p&gt;

&lt;p&gt;Price the swarm.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>n8n</category>
      <category>openai</category>
    </item>
    <item>
      <title>My headless AI server setup stopped dying when I got boring about queues and 30-second watchdogs</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Tue, 29 Sep 2026 14:13:11 +0000</pubDate>
      <link>https://dev.to/lars_winstand/my-headless-ai-server-setup-stopped-dying-when-i-got-boring-about-queues-and-30-second-watchdogs-3im5</link>
      <guid>https://dev.to/lars_winstand/my-headless-ai-server-setup-stopped-dying-when-i-got-boring-about-queues-and-30-second-watchdogs-3im5</guid>
      <description>&lt;p&gt;I realized our setup was fake-stable the first time a background agent stayed “running” while doing absolutely nothing.&lt;/p&gt;

&lt;p&gt;No crash.&lt;/p&gt;

&lt;p&gt;No stack trace.&lt;/p&gt;

&lt;p&gt;Docker said the container was up. The process existed. CPU was low. Memory looked fine.&lt;/p&gt;

&lt;p&gt;Meanwhile, webhook jobs were stacking up like a sink full of dishes.&lt;/p&gt;

&lt;p&gt;That was the week I stopped trusting "container is running" as a useful definition of healthy.&lt;/p&gt;

&lt;p&gt;If you’re moving from a laptop-and-good-vibes setup to a real headless AI server for always-on agents, this failure mode shows up fast.&lt;/p&gt;

&lt;p&gt;Usually the stack looks something like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;n8n or a custom webhook service&lt;/li&gt;
&lt;li&gt;Stripe / GitHub / Discord / Slack webhooks&lt;/li&gt;
&lt;li&gt;cron jobs&lt;/li&gt;
&lt;li&gt;long-running GPT-5 or Claude tasks&lt;/li&gt;
&lt;li&gt;maybe OpenClaw or another gateway in front&lt;/li&gt;
&lt;li&gt;a few Docker containers and a prayer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything looks fine until one worker hangs without technically dying.&lt;/p&gt;

&lt;p&gt;The fix was not a new framework.&lt;/p&gt;

&lt;p&gt;It was three boring things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;separate ingress from execution&lt;/li&gt;
&lt;li&gt;make jobs replay-safe&lt;/li&gt;
&lt;li&gt;put workers behind one OpenAI-compatible endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the whole post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture mistake that caused most of the pain
&lt;/h2&gt;

&lt;p&gt;The biggest mistake was letting webhook intake, cron scheduling, and LLM-heavy execution all happen in the same process.&lt;/p&gt;

&lt;p&gt;That works on a laptop.&lt;/p&gt;

&lt;p&gt;It gets weird in production.&lt;/p&gt;

&lt;p&gt;A slow Claude call delays a Stripe webhook.&lt;br&gt;
A giant PDF parse blocks the event loop.&lt;br&gt;
A CPU spike from OCR makes your Discord bot miss heartbeats.&lt;br&gt;
One stuck worker makes the whole service look haunted.&lt;/p&gt;

&lt;p&gt;The clean split is the same pattern n8n uses in queue mode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one main instance handles timers and webhooks&lt;/li&gt;
&lt;li&gt;jobs get pushed into Redis&lt;/li&gt;
&lt;li&gt;separate workers pull jobs and execute them&lt;/li&gt;
&lt;li&gt;shared state goes into a real database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation matters more than most teams think.&lt;/p&gt;

&lt;p&gt;Ingress should stay lightweight.&lt;br&gt;
Execution should be allowed to be slow, retryable, and disposable.&lt;/p&gt;
&lt;h2&gt;
  
  
  A minimal version of the pattern
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Stripe/GitHub/cron/webhooks]
            |
            v
    [main ingress service]
            |
            v
        [Redis queue]
            |
    -------------------
    |        |        |
| --- | --- |
    v        v        v
[worker] [worker] [worker]
    |        |        |
| --- | --- |
    ---------+--------
            |
            v
   [OpenAI-compatible endpoint]
            |
            v
 [GPT-5.4 / Claude Opus 4.6 / Grok 4.20]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you’re using n8n, this is basically why queue mode exists.&lt;/p&gt;

&lt;p&gt;Also: once you do this, local-only state starts breaking things.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQLite stops being a serious option for shared execution state&lt;/li&gt;
&lt;li&gt;local filesystem storage becomes a problem for binary data&lt;/li&gt;
&lt;li&gt;PostgreSQL and S3-compatible storage start looking very reasonable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the boring infrastructure tax for not losing jobs.&lt;/p&gt;
&lt;h2&gt;
  
  
  A queue does not save you from bad job design
&lt;/h2&gt;

&lt;p&gt;A queue gives you another chance.&lt;/p&gt;

&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;Whether that second chance helps or hurts depends on idempotency.&lt;/p&gt;

&lt;p&gt;If a worker does this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;charge a card&lt;/li&gt;
&lt;li&gt;send an email&lt;/li&gt;
&lt;li&gt;write to Salesforce&lt;/li&gt;
&lt;li&gt;crash before ack&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;...then the queue may redeliver the job.&lt;/p&gt;

&lt;p&gt;If the task is not replay-safe, you just built a duplicate side-effect machine.&lt;/p&gt;

&lt;p&gt;This is not theoretical. It’s how these systems behave.&lt;/p&gt;

&lt;p&gt;With Celery, for example, tasks can be redelivered if the worker dies before acknowledgment. Features like &lt;code&gt;acks_late&lt;/code&gt; help, but they do not magically make your side effects safe.&lt;/p&gt;

&lt;p&gt;The real fix is application-level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;store idempotency keys&lt;/li&gt;
&lt;li&gt;record processing state in a database&lt;/li&gt;
&lt;li&gt;make external side effects safe to retry&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Stripe will absolutely teach you this lesson
&lt;/h2&gt;

&lt;p&gt;Stripe’s webhook model is basically a giant sign that says: expect duplicates, expect retries, design accordingly.&lt;/p&gt;

&lt;p&gt;If your endpoint is down, Stripe retries undelivered events for up to 3 days.&lt;/p&gt;

&lt;p&gt;You can also recover manually by listing undelivered events.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-G&lt;/span&gt; https://api.stripe.com/v1/events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"sk_test_...:"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;ending_before&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;evt_001 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"types[]=payment_intent.succeeded"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"types[]=payment_intent.payment_failed"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;delivery_success&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the curl command.&lt;/p&gt;

&lt;p&gt;The important part is that your system has to know whether it already processed &lt;code&gt;evt_001&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A simple pattern is a table like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;processed_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then your worker logic becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleStripeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alreadyProcessed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;processed_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alreadyProcessed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;duplicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;doSideEffects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;processed_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That pattern is not glamorous.&lt;/p&gt;

&lt;p&gt;It is also the difference between “retries are safe” and “why did we send 4 receipts?”&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker restart policies are useful, but they did not solve the real problem
&lt;/h2&gt;

&lt;p&gt;Yes, use restart policies.&lt;/p&gt;

&lt;p&gt;They help with crashes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; worker &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped my-worker-image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But restart policies only help when the process exits.&lt;/p&gt;

&lt;p&gt;My actual problem was worse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the process was alive&lt;/li&gt;
&lt;li&gt;the container was alive&lt;/li&gt;
&lt;li&gt;the worker was not making progress&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a different class of failure.&lt;/p&gt;

&lt;p&gt;Docker does not know your Node.js event loop is wedged.&lt;br&gt;
Docker does not know your Python worker is stuck in a bad call.&lt;br&gt;
Docker does not know your queue heartbeat stopped.&lt;/p&gt;

&lt;p&gt;That’s why “container is up” is not enough.&lt;/p&gt;
&lt;h2&gt;
  
  
  BullMQ was the first tool that made this obvious
&lt;/h2&gt;

&lt;p&gt;BullMQ has a much more useful idea of health than Docker does.&lt;/p&gt;

&lt;p&gt;It marks jobs as stalled when an active worker stops renewing its lock.&lt;/p&gt;

&lt;p&gt;By default, that check is around 30 seconds.&lt;/p&gt;

&lt;p&gt;That is exactly the kind of signal I needed.&lt;/p&gt;

&lt;p&gt;Because a worker that is still running but hasn’t renewed a lock in 30 seconds is not healthy in any meaningful sense.&lt;/p&gt;

&lt;p&gt;A basic BullMQ worker looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bullmq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;IORedis&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ioredis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IORedis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jobs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jobs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Processing job &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// your LLM / automation work here&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Job &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; completed`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Job &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a retry config with jitter should be your default, not an afterthought:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-retry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;jitter: 0.5&lt;/code&gt; matters a lot.&lt;/p&gt;

&lt;p&gt;Without jitter, a worker fleet tends to fail together and retry together.&lt;/p&gt;

&lt;p&gt;That’s how a small outage turns into a retry storm.&lt;/p&gt;

&lt;h2&gt;
  
  
  CPU-heavy jobs can make a healthy-looking worker useless
&lt;/h2&gt;

&lt;p&gt;This one bit me hard.&lt;/p&gt;

&lt;p&gt;If you run CPU-heavy work in the same Node.js process that needs to keep queue heartbeats alive, you can create stalls without crashing anything.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OCR&lt;/li&gt;
&lt;li&gt;PDF parsing&lt;/li&gt;
&lt;li&gt;image conversion&lt;/li&gt;
&lt;li&gt;local embedding generation&lt;/li&gt;
&lt;li&gt;giant JSON transforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The worker process is technically alive.&lt;/p&gt;

&lt;p&gt;But queue progress says otherwise.&lt;/p&gt;

&lt;p&gt;That’s why sandboxed processors or separate worker processes are worth it.&lt;/p&gt;

&lt;p&gt;If a task can block the event loop, isolate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should supervise what?
&lt;/h2&gt;

&lt;p&gt;The best answer I found was: use multiple layers, but give each layer one job.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What it’s actually good at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;systemd watchdog + service restart&lt;/td&gt;
&lt;td&gt;Detects hung processes via heartbeat instead of only exit codes. Great for VM or bare-metal workers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker restart policy&lt;/td&gt;
&lt;td&gt;Restarts exited containers. Good default. Weak for hung-but-still-running workers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BullMQ or Celery&lt;/td&gt;
&lt;td&gt;Decouples ingress from execution, supports retries and redelivery, and tolerates worker death if jobs are replay-safe.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My ranking for real-world usefulness:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;queue semantics&lt;/li&gt;
&lt;li&gt;stall detection / watchdogs&lt;/li&gt;
&lt;li&gt;container restart policies&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Container restarts save you from crashes.&lt;/p&gt;

&lt;p&gt;Queues save you from reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other thing that cleaned this up: one OpenAI-compatible endpoint
&lt;/h2&gt;

&lt;p&gt;This was a much bigger deal than I expected.&lt;/p&gt;

&lt;p&gt;Before, different workers were calling different providers directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one service used OpenAI&lt;/li&gt;
&lt;li&gt;another used Anthropic&lt;/li&gt;
&lt;li&gt;another used Groq&lt;/li&gt;
&lt;li&gt;somebody had an OpenRouter integration&lt;/li&gt;
&lt;li&gt;retry logic was duplicated in multiple places&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gets ugly fast.&lt;/p&gt;

&lt;p&gt;Especially when every layer retries differently.&lt;/p&gt;

&lt;p&gt;OpenAI-style clients already need backoff for 429s and transient failures. If your queue retries jobs and your model client also retries aggressively, you can easily create a retry storm from both sides.&lt;/p&gt;

&lt;p&gt;Putting all workers behind one OpenAI-compatible endpoint fixed a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one auth pattern&lt;/li&gt;
&lt;li&gt;one SDK shape&lt;/li&gt;
&lt;li&gt;one timeout policy&lt;/li&gt;
&lt;li&gt;one retry policy&lt;/li&gt;
&lt;li&gt;one place to route requests between models&lt;/li&gt;
&lt;li&gt;one place to observe failures and rate limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That architecture is a great fit for AI agents and automations.&lt;/p&gt;

&lt;p&gt;It also makes provider swaps much less painful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for teams running always-on agents
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;n8n workers&lt;/li&gt;
&lt;li&gt;Make or Zapier handoffs&lt;/li&gt;
&lt;li&gt;custom Node/Python agents&lt;/li&gt;
&lt;li&gt;webhook-driven automations&lt;/li&gt;
&lt;li&gt;internal tools firing LLM jobs all day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...then the model layer should not be reimplemented in every service.&lt;/p&gt;

&lt;p&gt;A single OpenAI-compatible endpoint gives you a clean boundary.&lt;/p&gt;

&lt;p&gt;This is exactly why products like Standard Compute are interesting for this setup.&lt;/p&gt;

&lt;p&gt;You point your workers at one endpoint, keep your existing OpenAI-compatible SDKs, and avoid wiring provider-specific behavior into every queue consumer.&lt;/p&gt;

&lt;p&gt;The other practical benefit is cost predictability.&lt;/p&gt;

&lt;p&gt;When agents run 24/7, per-token billing gets annoying fast. A flat monthly model is a lot easier to reason about when you’re scaling background jobs, retries, and automations across a worker fleet.&lt;/p&gt;

&lt;p&gt;For this kind of architecture, that matters more than people admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch with a single endpoint
&lt;/h2&gt;

&lt;p&gt;A single endpoint also centralizes failure.&lt;/p&gt;

&lt;p&gt;That is real.&lt;/p&gt;

&lt;p&gt;If your gateway is down or misconfigured, every worker feels it.&lt;/p&gt;

&lt;p&gt;So if you do this, you still need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;health checks&lt;/li&gt;
&lt;li&gt;request timeouts&lt;/li&gt;
&lt;li&gt;circuit breakers where appropriate&lt;/li&gt;
&lt;li&gt;good logs and metrics&lt;/li&gt;
&lt;li&gt;fallback behavior for critical paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still, I would take one well-observed gateway over a zoo of half-maintained provider clients every time.&lt;/p&gt;

&lt;p&gt;Especially for teams that want interchangeable workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is overkill
&lt;/h2&gt;

&lt;p&gt;If your setup is tiny, don’t cargo-cult a distributed system.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;one internal script&lt;/li&gt;
&lt;li&gt;no external webhooks&lt;/li&gt;
&lt;li&gt;no concurrency&lt;/li&gt;
&lt;li&gt;no long-running jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...then a single process with systemd, strict timeouts, and a dead-letter path may be enough.&lt;/p&gt;

&lt;p&gt;You do not need Redis, BullMQ, PostgreSQL, S3, and six workers to rename files once an hour.&lt;/p&gt;

&lt;p&gt;But once you add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stripe or GitHub webhooks&lt;/li&gt;
&lt;li&gt;concurrent jobs&lt;/li&gt;
&lt;li&gt;long-running GPT-5 / Claude tasks&lt;/li&gt;
&lt;li&gt;multiple team members expecting reliability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...the “simple” setup stops being simple.&lt;/p&gt;

&lt;p&gt;It becomes fragile.&lt;/p&gt;

&lt;p&gt;And fragile always looks cheap right before it gets expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The baseline I trust now
&lt;/h2&gt;

&lt;p&gt;If I were rebuilding a headless AI server setup today, this is the baseline:&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Separate ingress from execution
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;webhooks and cron hit a lightweight main service&lt;/li&gt;
&lt;li&gt;execution happens in workers pulling from Redis via BullMQ or Celery&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Make every job replay-safe
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;store idempotency keys&lt;/li&gt;
&lt;li&gt;record processing state in a database&lt;/li&gt;
&lt;li&gt;assume duplicates will happen&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3) Use supervision that can detect hangs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Docker restart policies for crashes&lt;/li&gt;
&lt;li&gt;BullMQ stalled-job detection for queue workers&lt;/li&gt;
&lt;li&gt;systemd watchdog where OS-level supervision makes sense&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4) Put model access behind one OpenAI-compatible endpoint
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;one client shape&lt;/li&gt;
&lt;li&gt;one retry policy&lt;/li&gt;
&lt;li&gt;one place for routing, auth, quotas, and observability&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5) Move shared state off local disk
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL instead of SQLite for multi-worker setups&lt;/li&gt;
&lt;li&gt;S3-compatible storage instead of local files when workers share binaries&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6) Tune retries once
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;queue retries and model-client retries should cooperate&lt;/li&gt;
&lt;li&gt;add jitter so workers do not stampede together&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A practical checklist
&lt;/h2&gt;

&lt;p&gt;If you want something actionable, start here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. run Redis&lt;/span&gt;

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; redis &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 6379:6379 &lt;span class="se"&gt;\&lt;/span&gt;
  redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 2. split your app into:&lt;/span&gt;
&lt;span class="c"&gt;#    - ingress service&lt;/span&gt;
&lt;span class="c"&gt;#    - worker service&lt;/span&gt;
&lt;span class="c"&gt;#    - shared database&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 3. add health checks that measure progress, not just process existence&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 4. make every external side effect idempotent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 5. point all workers at one OpenAI-compatible endpoint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you do only those five things, your setup will already be less fragile than a surprising number of “production” agent stacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weirdly comforting part
&lt;/h2&gt;

&lt;p&gt;The most reliable headless AI server setup I’ve used is boring on purpose.&lt;/p&gt;

&lt;p&gt;Not the fanciest agent framework.&lt;br&gt;
Not the prettiest architecture diagram.&lt;br&gt;
Not the stack with the most logos in it.&lt;/p&gt;

&lt;p&gt;Just:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;queue in the middle&lt;/li&gt;
&lt;li&gt;watchdog on the worker&lt;/li&gt;
&lt;li&gt;one OpenAI-compatible endpoint in front&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That setup assumes workers will freeze.&lt;br&gt;
It assumes webhooks will retry.&lt;br&gt;
It assumes jobs will replay.&lt;br&gt;
It assumes somebody will eventually ship CPU-heavy nonsense into a process that was supposed to stay responsive.&lt;/p&gt;

&lt;p&gt;That’s why it works.&lt;/p&gt;

&lt;p&gt;And if you’re running always-on AI agents, “nothing broke at 3 a.m.” is a much better definition of success than “the container was still up.”&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>automation</category>
      <category>node</category>
    </item>
    <item>
      <title>My Google AI API 500 errors stopped being scary when I stopped retrying the whole workflow</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Mon, 28 Sep 2026 22:10:33 +0000</pubDate>
      <link>https://dev.to/lars_winstand/my-google-ai-api-500-errors-stopped-being-scary-when-i-stopped-retrying-the-whole-workflow-ko1</link>
      <guid>https://dev.to/lars_winstand/my-google-ai-api-500-errors-stopped-being-scary-when-i-stopped-retrying-the-whole-workflow-ko1</guid>
      <description>&lt;p&gt;I knew something was wrong when one flaky Gemini call turned into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 duplicate HubSpot updates&lt;/li&gt;
&lt;li&gt;1 duplicate Jira issue&lt;/li&gt;
&lt;li&gt;4 Slack messages&lt;/li&gt;
&lt;li&gt;and a very annoyed ops channel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first it looked like a normal "Google AI API is throwing random 500s" problem.&lt;/p&gt;

&lt;p&gt;It wasn’t.&lt;/p&gt;

&lt;p&gt;The real bug was that we were retrying the entire automation instead of retrying the model call.&lt;/p&gt;

&lt;p&gt;That distinction matters a lot once your workflow has side effects.&lt;/p&gt;

&lt;p&gt;If you’re seeing intermittent &lt;code&gt;500&lt;/code&gt;, &lt;code&gt;502&lt;/code&gt;, &lt;code&gt;503&lt;/code&gt;, or &lt;code&gt;429&lt;/code&gt; errors from Gemini or Vertex AI, the fix usually is not “add more retries everywhere.” The fix is to move the retry boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first thing that surprised me: Google was already retrying
&lt;/h2&gt;

&lt;p&gt;If you use the Gemini Python SDK, Google already retries transient failures by default.&lt;/p&gt;

&lt;p&gt;That includes transient &lt;code&gt;429&lt;/code&gt; and &lt;code&gt;5xx&lt;/code&gt; responses, with exponential backoff.&lt;/p&gt;

&lt;p&gt;So if your production automation is still blowing up after “we added retries,” one of these is probably true:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You’re using raw HTTP or an automation HTTP node, so you don’t actually have safe retry behavior.&lt;/li&gt;
&lt;li&gt;You wrapped the entire workflow in retry logic, which replays every side effect.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That second one is the expensive mistake.&lt;/p&gt;

&lt;p&gt;If your workflow does this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;fetch lead&lt;/li&gt;
&lt;li&gt;build prompt&lt;/li&gt;
&lt;li&gt;call Gemini&lt;/li&gt;
&lt;li&gt;write to PostgreSQL&lt;/li&gt;
&lt;li&gt;update HubSpot&lt;/li&gt;
&lt;li&gt;send Slack message&lt;/li&gt;
&lt;li&gt;retry whole thing on failure&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;...then one transient model failure becomes a duplicate generator.&lt;/p&gt;

&lt;p&gt;The model error is annoying.&lt;/p&gt;

&lt;p&gt;The replay damage is worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a “random 500” usually means in practice
&lt;/h2&gt;

&lt;p&gt;A lot of teams treat &lt;code&gt;500&lt;/code&gt; as "Google is broken."&lt;/p&gt;

&lt;p&gt;Sometimes that’s true.&lt;/p&gt;

&lt;p&gt;But with Gemini and Vertex AI, &lt;code&gt;500&lt;/code&gt; can also mean overload, dependency failures, shared-capacity pressure, or quota-related behavior that doesn’t show up as a clean &lt;code&gt;429&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That’s why these incidents feel spooky in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one run succeeds&lt;/li&gt;
&lt;li&gt;the next one fails&lt;/li&gt;
&lt;li&gt;the retry half-works&lt;/li&gt;
&lt;li&gt;then downstream systems are in a weird state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only watch for explicit rate limits, you miss the actual pattern.&lt;/p&gt;

&lt;p&gt;Your "random 500s" may really be burst traffic, project-wide contention, or spend throttling wearing a different mask.&lt;/p&gt;

&lt;h2&gt;
  
  
  My opinionated take: workflow-level retry is lazy engineering
&lt;/h2&gt;

&lt;p&gt;If a workflow has side effects, retrying the whole thing is the wrong default.&lt;/p&gt;

&lt;p&gt;The retry boundary should sit around the model call, not around everything before and after it.&lt;/p&gt;

&lt;p&gt;This is the rule I trust now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;collect data once&lt;/li&gt;
&lt;li&gt;call the model in an isolated step&lt;/li&gt;
&lt;li&gt;write side effects once&lt;/li&gt;
&lt;li&gt;replay only the failed step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don’t do that, you get the usual mess:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate HubSpot or Salesforce writes&lt;/li&gt;
&lt;li&gt;duplicate Gmail or SendGrid emails&lt;/li&gt;
&lt;li&gt;duplicate Slack or Discord alerts&lt;/li&gt;
&lt;li&gt;duplicate Zendesk or Jira tickets&lt;/li&gt;
&lt;li&gt;half-finished database state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point it’s not really an LLM problem anymore.&lt;/p&gt;

&lt;p&gt;It’s a workflow design problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern that fixed it for us
&lt;/h2&gt;

&lt;p&gt;The cleanest version of this is to isolate the LLM call into its own sub-workflow or worker.&lt;/p&gt;

&lt;p&gt;Instead of this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;fetch records&lt;/li&gt;
&lt;li&gt;build prompt&lt;/li&gt;
&lt;li&gt;call Gemini&lt;/li&gt;
&lt;li&gt;update CRM&lt;/li&gt;
&lt;li&gt;send Slack alert&lt;/li&gt;
&lt;li&gt;retry whole workflow if anything fails&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do this instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;fetch records&lt;/li&gt;
&lt;li&gt;persist a replay-safe payload&lt;/li&gt;
&lt;li&gt;call a sub-workflow or worker that only handles Gemini&lt;/li&gt;
&lt;li&gt;return structured output&lt;/li&gt;
&lt;li&gt;perform downstream writes once&lt;/li&gt;
&lt;li&gt;route failures to an error handler&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That one change removes most of the blast radius.&lt;/p&gt;

&lt;p&gt;If Gemini throws a transient &lt;code&gt;5xx&lt;/code&gt;, only the model step retries.&lt;/p&gt;

&lt;p&gt;Your CRM write doesn’t happen twice.&lt;/p&gt;

&lt;p&gt;Your Slack alert doesn’t fire twice.&lt;/p&gt;

&lt;p&gt;Your upstream data fetch doesn’t get repeated for no reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete n8n version
&lt;/h2&gt;

&lt;p&gt;n8n is actually pretty good at this if you use the primitives it gives you.&lt;/p&gt;

&lt;p&gt;Useful pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Error Workflows&lt;/li&gt;
&lt;li&gt;Error Trigger&lt;/li&gt;
&lt;li&gt;sub-workflows&lt;/li&gt;
&lt;li&gt;execution data&lt;/li&gt;
&lt;li&gt;log streaming&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A decent shape looks like this:&lt;/p&gt;

&lt;h3&gt;
  
  
  Main workflow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Fetch records&lt;/li&gt;
&lt;li&gt;Normalize payload&lt;/li&gt;
&lt;li&gt;Save payload + execution ID&lt;/li&gt;
&lt;li&gt;Call sub-workflow for Gemini&lt;/li&gt;
&lt;li&gt;Receive structured result&lt;/li&gt;
&lt;li&gt;Write downstream side effects once&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Error workflow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Start with &lt;code&gt;Error Trigger&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Capture failed execution ID&lt;/li&gt;
&lt;li&gt;Log prompt + payload&lt;/li&gt;
&lt;li&gt;Alert Slack&lt;/li&gt;
&lt;li&gt;Queue replay of only the Gemini step&lt;/li&gt;
&lt;li&gt;Escalate after max attempts&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That turns a noisy crash into something debuggable.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you use raw HTTP, you own the retry logic
&lt;/h2&gt;

&lt;p&gt;This is where a lot of teams accidentally create their own outage.&lt;/p&gt;

&lt;p&gt;If you call Gemini through direct REST, an n8n HTTP Request node, Make, Zapier, or a custom worker, you need to implement retry policy yourself.&lt;/p&gt;

&lt;p&gt;Minimum bar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;retry_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;408&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;max_attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="n"&gt;base_delay_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;max_delay_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;span class="n"&gt;use_jitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the retry should wrap only the model request.&lt;/p&gt;

&lt;p&gt;Not the whole business process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: isolated retry wrapper in Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;RETRY_ON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;408&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;MAX_ATTEMPTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="n"&gt;BASE_DELAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;MAX_DELAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_gemini_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_ATTEMPTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RETRY_ON&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;MAX_ATTEMPTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASE_DELAY&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;MAX_DELAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;jitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s still not enough by itself.&lt;/p&gt;

&lt;p&gt;You also need idempotency around whatever happens after the model returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retry boundary I trust now
&lt;/h2&gt;

&lt;p&gt;This is the design I’d recommend to anyone running AI automations in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Make the model call idempotent
&lt;/h3&gt;

&lt;p&gt;Give each model request a stable operation ID tied to the business event.&lt;/p&gt;

&lt;p&gt;Not the execution attempt.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lead_enrichment:hubspot_contact_12345
support_triage:zendesk_ticket_98765
invoice_review:invoice_2026_00412
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the same job replays, your system should recognize it as the same operation.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Persist enough context to replay one step
&lt;/h3&gt;

&lt;p&gt;Store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prompt input&lt;/li&gt;
&lt;li&gt;normalized source data&lt;/li&gt;
&lt;li&gt;model name&lt;/li&gt;
&lt;li&gt;temperature&lt;/li&gt;
&lt;li&gt;candidate count&lt;/li&gt;
&lt;li&gt;execution ID&lt;/li&gt;
&lt;li&gt;business object ID&lt;/li&gt;
&lt;li&gt;timestamp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don’t log the exact request shape, replay becomes guesswork.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Cap retries and fail deliberately
&lt;/h3&gt;

&lt;p&gt;Do not let a worker spin forever because one model is having a bad hour.&lt;/p&gt;

&lt;p&gt;After max attempts, route to one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dead-letter queue&lt;/li&gt;
&lt;li&gt;delayed retry&lt;/li&gt;
&lt;li&gt;human review&lt;/li&gt;
&lt;li&gt;fallback model path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fallback routing is not cheating.&lt;/p&gt;

&lt;p&gt;It’s production engineering.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Smooth traffic before blaming the model
&lt;/h3&gt;

&lt;p&gt;A lot of “random instability” is really bursty traffic.&lt;/p&gt;

&lt;p&gt;If your cron job wakes up and slams Gemini with a huge batch, shared-capacity systems can get weird fast.&lt;/p&gt;

&lt;p&gt;Paced workers beat spiky workers.&lt;/p&gt;

&lt;p&gt;Queues beat bursts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Are you debugging one API key when the whole project is overloaded?
&lt;/h2&gt;

&lt;p&gt;This is another easy trap.&lt;/p&gt;

&lt;p&gt;Gemini and Vertex AI limits are not always about a single request or a single API key.&lt;/p&gt;

&lt;p&gt;They can be project-wide.&lt;/p&gt;

&lt;p&gt;So if you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an n8n workflow&lt;/li&gt;
&lt;li&gt;a Make scenario&lt;/li&gt;
&lt;li&gt;a Python worker&lt;/li&gt;
&lt;li&gt;a Zapier automation&lt;/li&gt;
&lt;li&gt;maybe a background agent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...all hitting the same Google project, failures can look random unless you correlate them with project-wide traffic.&lt;/p&gt;

&lt;p&gt;That means you should track at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;requests per minute&lt;/li&gt;
&lt;li&gt;tokens per minute&lt;/li&gt;
&lt;li&gt;requests per day&lt;/li&gt;
&lt;li&gt;spend spikes&lt;/li&gt;
&lt;li&gt;retry volume&lt;/li&gt;
&lt;li&gt;error rate by status code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only inspect one failing execution, you’ll miss the real cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  SDK vs raw HTTP vs managed routing
&lt;/h2&gt;

&lt;p&gt;This is one of those boring implementation details that decides whether your week stays calm.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What happens when Gemini gets flaky&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemini API via official SDK&lt;/td&gt;
&lt;td&gt;Safer defaults. Built-in transient retry behavior. Less custom work.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini API via direct REST or n8n HTTP Request&lt;/td&gt;
&lt;td&gt;You own retries, jitter, caps, and safe replay boundaries. Easier to get wrong.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertex AI pay-as-you-go&lt;/td&gt;
&lt;td&gt;Shared-capacity behavior means burst shape matters a lot.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertex AI Provisioned Throughput&lt;/td&gt;
&lt;td&gt;Better when you need more consistent service and retries alone aren’t enough.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My bias: if you’re doing direct HTTP in production, be honest that you’re taking on reliability work.&lt;/p&gt;

&lt;p&gt;That’s fine.&lt;/p&gt;

&lt;p&gt;Just don’t pretend it’s the same as using an SDK with sane defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terminal commands that are actually useful during incident response
&lt;/h2&gt;

&lt;p&gt;If you’re testing Vertex AI auth manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud auth print-access-token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to inspect whether your worker is replaying too aggressively, log attempt counts explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"gemini_attempt"&lt;/span&gt; app.log | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you aren’t logging operation IDs yet, fix that first.&lt;/p&gt;

&lt;p&gt;A lot of debugging pain disappears once you can answer this question quickly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did the model fail once, or did our workflow replay the same business event four times?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What changed for me after fixing the boundary
&lt;/h2&gt;

&lt;p&gt;Once we stopped retrying the whole workflow, the incidents got much less dramatic.&lt;/p&gt;

&lt;p&gt;We still saw transient model failures.&lt;/p&gt;

&lt;p&gt;That part never fully goes away.&lt;/p&gt;

&lt;p&gt;But the failures became contained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one failed model step&lt;/li&gt;
&lt;li&gt;one replayable unit&lt;/li&gt;
&lt;li&gt;one error record&lt;/li&gt;
&lt;li&gt;zero duplicate side effects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a very different operational story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Standard Compute fits if you’re tired of this game
&lt;/h2&gt;

&lt;p&gt;A lot of teams end up here because per-token pricing makes them afraid to add the reliability layers they actually need.&lt;/p&gt;

&lt;p&gt;They avoid extra retries.&lt;br&gt;
They avoid fallback models.&lt;br&gt;
They avoid always-on agents.&lt;br&gt;
They avoid richer automation because every failure path has a billing consequence.&lt;/p&gt;

&lt;p&gt;That’s exactly the problem Standard Compute is trying to remove.&lt;/p&gt;

&lt;p&gt;Standard Compute gives you an OpenAI-compatible API with unlimited AI compute at a flat monthly price, so you can run agents, retries, batching, and automations without token anxiety.&lt;/p&gt;

&lt;p&gt;If you’re building on n8n, Make, Zapier, OpenClaw, or custom workers, that matters more than people admit.&lt;/p&gt;

&lt;p&gt;Predictable cost changes architecture decisions.&lt;/p&gt;

&lt;p&gt;It’s a lot easier to build safe retry boundaries and fallback paths when every extra call doesn’t feel like a tiny financial penalty.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway I wish I had earlier
&lt;/h2&gt;

&lt;p&gt;If your Google AI API &lt;code&gt;500&lt;/code&gt; errors keep showing up in production, stop asking only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;how do we retry harder?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask better questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the model call isolated from side effects?&lt;/li&gt;
&lt;li&gt;Can I replay only the Gemini step?&lt;/li&gt;
&lt;li&gt;Am I using the official SDK or raw HTTP?&lt;/li&gt;
&lt;li&gt;Am I tracking project-wide traffic instead of staring at one failed request?&lt;/li&gt;
&lt;li&gt;Do I need better throughput guarantees instead of more retry code?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When an LLM stops responding, the winning move usually isn’t prompt magic.&lt;/p&gt;

&lt;p&gt;It’s boring architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;idempotency keys&lt;/li&gt;
&lt;li&gt;isolated model steps&lt;/li&gt;
&lt;li&gt;capped retries&lt;/li&gt;
&lt;li&gt;jitter&lt;/li&gt;
&lt;li&gt;sub-workflows&lt;/li&gt;
&lt;li&gt;dead-letter queues&lt;/li&gt;
&lt;li&gt;clean error lanes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Less exciting than blaming Gemini.&lt;/p&gt;

&lt;p&gt;Much more effective.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>python</category>
      <category>n8n</category>
    </item>
  </channel>
</rss>
