<?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: Swapnoneel Saha</title>
    <description>The latest articles on DEV Community by Swapnoneel Saha (@swapnoneel123).</description>
    <link>https://dev.to/swapnoneel123</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%2F1320336%2Fc601d462-6b3a-47ac-b0ae-889c5fc913d1.png</url>
      <title>DEV Community: Swapnoneel Saha</title>
      <link>https://dev.to/swapnoneel123</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/swapnoneel123"/>
    <language>en</language>
    <item>
      <title>What Is Granular Access Control? RBAC, ABAC, and AI</title>
      <dc:creator>Swapnoneel Saha</dc:creator>
      <pubDate>Wed, 09 Sep 2026 17:25:41 +0000</pubDate>
      <link>https://dev.to/swapnoneel123/what-is-granular-access-control-rbac-abac-and-ai-54eo</link>
      <guid>https://dev.to/swapnoneel123/what-is-granular-access-control-rbac-abac-and-ai-54eo</guid>
      <description>&lt;p&gt;"Can this person use the AI API?" is one question.&lt;/p&gt;

&lt;p&gt;"Can this service call gpt-5, at most 200 times an hour, with a 50 dollar cap this month, with no file system tools attached, and only until Friday?" is a completely different question.&lt;/p&gt;

&lt;p&gt;Granular access control is what turns the first question into the second one. It means defining permissions at the smallest useful unit, so an identity can take exactly one action, on exactly one resource, within exactly one budget, under exactly one set of conditions, and nothing else.&lt;/p&gt;

&lt;p&gt;That idea has a name, the principle of least privilege, and it just means giving each caller the minimum access it needs. Granular access control is how you actually write that down.&lt;/p&gt;

&lt;p&gt;And the reason it stopped being a nice-to-have this year is that most of the things asking for permission are no longer people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access control is a dial, not a switch
&lt;/h2&gt;

&lt;p&gt;Strip a permission down and it is three things: a &lt;strong&gt;subject&lt;/strong&gt; (who is asking), an &lt;strong&gt;action&lt;/strong&gt; (what they want to do), and a &lt;strong&gt;resource&lt;/strong&gt; (what they want to do it to). Every access control system you have ever used is just a way of writing those three things down and checking them.&lt;/p&gt;

&lt;p&gt;Coarse-grained access control leaves all three parts wide open. "Engineers can use the database" is one permission covering forty people, six verbs, and every table you own. It is one line of config, and it is fast to write, which is exactly why it survives so long.&lt;/p&gt;

&lt;p&gt;Granular access control narrows the same three parts instead. "The reporting service can run SELECT on the &lt;code&gt;orders&lt;/code&gt; table, and nothing else." One caller, one verb, one table.&lt;/p&gt;

&lt;p&gt;Think of a house key against a hotel keycard. The house key opens everything you own, forever, and the only way to take it back is to change the lock. The keycard opens room 402, on floor 4, until Friday at 11 in the morning, and the front desk can kill it from the lobby without touching a single door.&lt;/p&gt;

&lt;p&gt;Both are access control. The keycard is granular.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F01pzq01gmahs1mm3fwa8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F01pzq01gmahs1mm3fwa8.png" alt="Granular access control illustrated as a hotel keycard" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So granularity is not a product you buy or a model you adopt. It is how far down you are willing to turn the dial, and how much extra work you are willing to do in exchange. Turn it too far and you get a permissions table nobody can read. Leave it too coarse and one leaked credential owns the whole system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five dimensions you can narrow a permission on
&lt;/h2&gt;

&lt;p&gt;Most explanations of granular access control stop at "give people less access", which is true and completely useless as advice. The useful version is that a permission can be narrowed along five separate axes, and you can turn each one independently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q78ahupqavcnq2hz7j1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q78ahupqavcnq2hz7j1.png" alt="The five dimensions of granular access control" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here they are, from the one everybody already does to the one almost nobody does.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Who is asking
&lt;/h3&gt;

&lt;p&gt;The identity. This is the axis every system starts with, and the one that changed the most recently.&lt;/p&gt;

&lt;p&gt;For a long time a subject meant a person, or a service account somebody created once and forgot about. Now a subject is just as likely to be a coding agent, a CI job, a background worker, or a chain of three agents where the last one has no idea which human started the request.&lt;/p&gt;

&lt;p&gt;These are called non-human identities, and they are the ones quietly holding most of the over-permissioned access. Sonrai's cloud access research, published in May 2026, found that &lt;a href="https://sonraisecurity.com/blog/why-92-percent-cloud-permissions-never-used/" rel="noopener noreferrer"&gt;92% of identities with sensitive permissions&lt;/a&gt; did not use them once in 90 days, and 87% of that group were machine identities. That is a vendor scanning its own customers and not an independent audit, so take the exact number lightly.&lt;/p&gt;

&lt;p&gt;The fix here is easy to say and annoying to do: one identity per caller, never one shared credential per team.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. What action they want to take
&lt;/h3&gt;

&lt;p&gt;Read, write, update, delete. List versus fetch. Call a model versus look at what models exist.&lt;/p&gt;

&lt;p&gt;Coarse systems have two levels here, usually named something like read and admin. Granular systems split up the verbs that can do different amounts of damage. Being able to view a log line and being able to export the whole log table are not the same permission, even though both are "reading".&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Which resource, exactly
&lt;/h3&gt;

&lt;p&gt;This is where granularity gets real, because a resource is rarely one flat thing.&lt;/p&gt;

&lt;p&gt;A database can be scoped at the server, the database, the table, the column, or the row. An AI setup can be scoped at the provider, the model, or a single tool that a model is allowed to invoke. Each step down that ladder means less damage if the key leaks, and one more thing to maintain.&lt;/p&gt;

&lt;p&gt;Row-level scoping is the one worth knowing by name. It answers "which of these records can you see", which is a different question from "can you see this table", and it is the difference between a support agent seeing their own tickets and a support agent seeing everybody's.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. How much they can consume
&lt;/h3&gt;

&lt;p&gt;Here is the axis classic access control mostly ignores, and it is the one that bites hardest with AI.&lt;/p&gt;

&lt;p&gt;Permission has traditionally been a boolean. You can call this endpoint, or you cannot. But when a single call can cost real money and a loop can make ten thousand of them, "yes" without a ceiling is not a permission, it is an open tab.&lt;/p&gt;

&lt;p&gt;So the granular version attaches quantity to the grant. This key may spend 200 dollars a month. This key may burn 10,000 tokens an hour. This key may make 100 requests a minute. Developers have been asking providers for exactly this for years, and OpenAI's own developer forum carries a &lt;a href="https://community.openai.com/t/api-key-specific-rate-and-spending-limits-would-be-good/1374358" rel="noopener noreferrer"&gt;feature request for per-key spending limits&lt;/a&gt; with people piling into it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Under what conditions
&lt;/h3&gt;

&lt;p&gt;The context around the request. Time of day, source IP, environment, device posture, whether the key has expired yet.&lt;/p&gt;

&lt;p&gt;Expiry is the underrated one. A permission with no end date is a permission you will forget you granted, and every "how did this old key still work" incident starts there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is granular access control the same as RBAC?
&lt;/h2&gt;

&lt;p&gt;No, and this trips up a lot of people, so it is worth separating properly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqjtyhvz59r4ngk3s42q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqjtyhvz59r4ngk3s42q.png" alt="RBAC, ABAC, and ReBAC compared" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RBAC&lt;/strong&gt; (role-based access control) groups permissions into roles and hands roles to identities. It is the most common model in the world because it maps onto how companies actually think. You are a developer, developers get the developer role, done.&lt;/p&gt;

&lt;p&gt;RBAC can be granular or coarse depending entirely on how you write the roles. That is the part people miss. A role called &lt;code&gt;admin&lt;/code&gt; with every permission attached is RBAC and it is not granular at all.&lt;/p&gt;

&lt;p&gt;The trouble shows up when you try to push RBAC down the dial. Every new condition needs its own role, so you get &lt;code&gt;developer-staging&lt;/code&gt;, &lt;code&gt;developer-staging-eu&lt;/code&gt;, &lt;code&gt;developer-staging-eu-readonly&lt;/code&gt;, and pretty soon nobody can tell you what any of them do. The industry name for this is &lt;a href="https://www.osohq.com/learn/rbac-vs-abac" rel="noopener noreferrer"&gt;role explosion&lt;/a&gt;, and it is the standard failure mode of a team that discovered granularity and only had roles to express it with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ABAC&lt;/strong&gt; (attribute-based access control) fixes that by evaluating attributes at request time instead of pre-baking roles. Department, clearance, resource classification, time, location. It gets you much finer control without the role count exploding, and the price you pay is that when a request gets denied, you have to trace which attribute failed instead of just reading a role name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ReBAC&lt;/strong&gt; (relationship-based access control) asks a third question: how is this subject related to this resource? You can edit the doc because you created it. You can see the profile because you manage that person. Google Drive works this way, and so does every app where ownership is the real rule.&lt;/p&gt;

&lt;p&gt;Well, which one should you use? Honestly, most real systems end up with RBAC for the broad strokes and one of the other two layered on for the cases roles cannot express. Granularity comes from the rules you write, and not from the model you picked. You can write a terrible coarse ABAC policy. Plenty of people have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI made this urgent
&lt;/h2&gt;

&lt;p&gt;I run Claude Code and GPT Codex daily, and both of them do things a normal API consumer never did. They read files and call tools. And they chain several model calls together off one instruction I typed half-awake.&lt;/p&gt;

&lt;p&gt;That breaks the two assumptions most permission systems were built on: that whoever is asking is a person, and that every request is something a person actually asked for.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft0ckl54bbgbqz57nzaw1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft0ckl54bbgbqz57nzaw1.png" alt="AI agents expand access control risk" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The numbers back this up. 1Password surveyed 1,000 security and engineering staff at large US firms in late May and early June 2026, and found that agents in production reached &lt;a href="https://www.helpnetsecurity.com/2026/07/29/1password-ai-agent-governance/" rel="noopener noreferrer"&gt;roughly twice as much data&lt;/a&gt; as had actually been approved. In the same survey, 33% of developers running agents reported a breach or security incident tied to an over-privileged non-human identity, and 40% said they leave agents holding persistent access to systems and secrets after the task is finished.&lt;/p&gt;

&lt;p&gt;None of that is an AI-specific vulnerability. It is the same over-permissioning problem as always, running at a speed and volume that humans never generated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where granular access control breaks in practice
&lt;/h2&gt;

&lt;p&gt;Everybody agrees with least privilege. Almost nobody has it. So it is worth being specific about where the plan falls apart, because the reason is usually the setup, and not laziness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A provider key cannot be narrowed.&lt;/strong&gt; Your OpenAI or Anthropic key is a single credential with your whole account behind it. There is no version of it that means "gpt-4o only, 50 dollars, no tool calls". So the moment more than one service needs model access, you either share one key and lose all attribution, or you mint several and lose all central control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enforcement lives in the wrong place.&lt;/strong&gt; If the permission check is inside your application code, then every new service, notebook, cron job, and intern's side script has to re-implement it correctly. One of them will not. And the ones that skip it will not show up in any policy list, because they never registered with the policy system in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You cannot enforce what you cannot attribute.&lt;/strong&gt; At Zonko Labs I built an internal tool that captured our AI product's data logs and generated reports on latency and probable slowdowns, and the thing that made it useful was that every log line could be traced back to a specific caller. Without that, a spend spike is just a number going up. You cannot tighten a permission when you do not know which caller needs loosening.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsxibvxloeoljo26wt89q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsxibvxloeoljo26wt89q.png" alt="Common granular access control failure modes" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the pattern behind all three is the same. Granular access control needs a place in the request path that sees every call, knows who made it, and can say no before the call leaves your network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the gateway comes in
&lt;/h2&gt;

&lt;p&gt;Let me be honest about where this actually helps first. If you are one person with one API key on one machine, you do not need any of this, and a gateway will not make your side project safer. It also cannot fix a permission model you have not thought about. A gateway enforces the rules you write, so if you write "allow everything", you get everything, faster.&lt;/p&gt;

&lt;p&gt;But the moment there are several services, several models, and a few agents in the mix, the check has to sit in the traffic path. That is what an AI gateway is: a proxy that every model call goes through. Which makes it the one place you can check a permission once, instead of in every service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkav5q2ggtprmlizj8nvz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkav5q2ggtprmlizj8nvz.png" alt="AI gateway enforcing permissions on model calls" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; is the open-source one I keep pointing people at, partly because the whole thing sits in &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;a public GitHub repo&lt;/a&gt;, so you can read exactly what it enforces rather than trusting a feature list.&lt;/p&gt;

&lt;p&gt;The thing you hand out is a &lt;strong&gt;virtual key&lt;/strong&gt;. Your real provider credentials sit inside the gateway, and each caller gets its own scoped key instead. And the things you can scope that key on are almost exactly the five dimensions above.&lt;/p&gt;

&lt;p&gt;Take the rule in plain words first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This key belongs to the support team. It can only use OpenAI, only the &lt;code&gt;gpt-4o&lt;/code&gt; model, and it stops working on the first of next year.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Written out in Bifrost's &lt;a href="https://docs.getbifrost.ai/features/governance" rel="noopener noreferrer"&gt;governance config&lt;/a&gt;, that is:&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;"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;"vk-support-bot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_active"&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;"allow_all_providers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2027-01-01T00:00:00Z"&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_configs"&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;"provider"&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"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"allowed_models"&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;"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;"blacklisted_models"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"team_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;"team-support"&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;Reading that back: &lt;code&gt;allow_all_providers&lt;/code&gt; set to false means any provider not listed here is denied, including ones added to the gateway later. &lt;code&gt;allowed_models&lt;/code&gt; narrows dimension three to a single model. &lt;code&gt;expires_at&lt;/code&gt; is dimension five, so the key dies on its own without anybody remembering to revoke it. And &lt;code&gt;team_id&lt;/code&gt; attaches it to a team, which is where the budget lives.&lt;/p&gt;

&lt;p&gt;A few more things worth knowing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spending and rate caps sit on the key itself.&lt;/strong&gt; A budget is &lt;code&gt;max_limit&lt;/code&gt; plus a &lt;code&gt;reset_duration&lt;/code&gt; of &lt;code&gt;1m&lt;/code&gt;, &lt;code&gt;1h&lt;/code&gt;, &lt;code&gt;1d&lt;/code&gt;, &lt;code&gt;1w&lt;/code&gt;, &lt;code&gt;1M&lt;/code&gt;, &lt;code&gt;1Q&lt;/code&gt; or &lt;code&gt;1Y&lt;/code&gt;, and rate limits are separate &lt;code&gt;token_max_limit&lt;/code&gt; and &lt;code&gt;request_max_limit&lt;/code&gt; counters. Blowing through them returns real status codes rather than a generic failure: 402 for &lt;code&gt;budget_exceeded&lt;/code&gt;, 429 for &lt;code&gt;token_limited&lt;/code&gt;, 403 for &lt;code&gt;model_blocked&lt;/code&gt;. That distinction sounds small until you are the one reading the error at 2 in the morning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool access is deny-by-default.&lt;/strong&gt; If you have wired up MCP servers, a virtual key with no MCP configuration gets no tools at all. Where tools are granted, they are listed explicitly in &lt;code&gt;tools_to_execute&lt;/code&gt;, and the key's list acts as a ceiling that a request cannot widen. That is dimension three pushed all the way down to individual tools, which matters a lot given how much damage one file system tool can do inside an agent loop! (If MCP is new to you, I wrote &lt;a href="https://www.swapnoneel.site/blog/what-is-an-mcp-server" rel="noopener noreferrer"&gt;a beginner's guide to MCP servers&lt;/a&gt; first.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seeing the dashboard is a separate grant from calling a model.&lt;/strong&gt; Bifrost ships three system roles, Admin, Developer and Viewer, carrying 42, 27 and 14 permissions, and you can build custom ones by toggling resource and operation pairs. On top of that sits data access control, which decides which rows a user sees at all: &lt;code&gt;own-data&lt;/code&gt;, &lt;code&gt;team-data&lt;/code&gt;, or &lt;code&gt;all-data&lt;/code&gt;. Being allowed to open the logs page and being allowed to see everybody's logs are two different grants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write the policy once, not once per person.&lt;/strong&gt; &lt;a href="https://docs.getbifrost.ai/enterprise/access-profiles" rel="noopener noreferrer"&gt;Access profiles&lt;/a&gt; let you define one policy and auto-issue a per-user virtual key from it, each with its own budget counter. Edit the template, propagate, and every key follows. Keys can rotate on a schedule from &lt;code&gt;1h&lt;/code&gt; up to &lt;code&gt;365d&lt;/code&gt;. This is the part that decides whether any of it survives the team getting bigger. If every new engineer needs a hand-written policy, somebody will quietly hand out a shared key by month three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit logs are signed.&lt;/strong&gt; Administrative events can be HMAC-signed and exported as JSON, JSON Lines, or syslog in the RFC 5424 format that SIEM tools ingest. Knowing who did something and being able to prove it later are the same job, and homegrown setups usually only manage the first half.&lt;/p&gt;

&lt;p&gt;Bifrost publishes its own benchmarks (roughly 20 microseconds of added latency at 5,000 requests per second), and those are vendor benchmarks run on a vendor harness, so measure your own. The point holds whichever gateway you use: the check belongs on the path, and not copy-pasted into fifteen codebases.&lt;/p&gt;

&lt;p&gt;If governance at this layer is the actual problem you are solving, I also went through &lt;a href="https://www.swapnoneel.site/blog/top-5-ai-agent-governance-platforms" rel="noopener noreferrer"&gt;the tools in this space&lt;/a&gt; in more detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where should you set the dial?
&lt;/h2&gt;

&lt;p&gt;The floor is not RBAC, and it is not ABAC either. The floor is being able to answer one question about your own system right now:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If a credential leaked this minute, what exactly could someone do with it, and how much could they spend before anyone noticed?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the honest answer is "everything, and I have no idea", the fix is not a bigger access control model. It is a first cut at all five dimensions, in this order: one identity per caller, then a spending cap on each one, then an expiry date, then a model or resource allowlist, then conditions. That order is deliberate, because knowing who called and capping what they spend buy you the most safety for the least work, and conditions buy you the least.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl5be20b0hjp3mg2g397n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl5be20b0hjp3mg2g397n.png" alt="Recommended order for setting granular access controls" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will not get to per-row, per-tool, per-hour granularity, and you should not try. Nobody is running the theoretically correct permission model. The teams doing well are the ones who pushed the dial two notches past a shared key and then actually maintained it.&lt;/p&gt;

&lt;p&gt;Anyway, I am curious where other people drew this line. If you have a permission setup that survived a team getting bigger, or one that collapsed into a shared key by month three, tell me in the comments, I want to hear the failure stories more than the success ones.&lt;/p&gt;

&lt;p&gt;You can find me on &lt;a href="https://x.com/swapnoneel123" rel="noopener noreferrer"&gt;X&lt;/a&gt;, or read the rest of my writing on &lt;a href="https://www.swapnoneel.site" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>ai</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Three Insights for Building Great Claude Skills</title>
      <dc:creator>Swapnoneel Saha</dc:creator>
      <pubDate>Tue, 08 Sep 2026 18:32:06 +0000</pubDate>
      <link>https://dev.to/swapnoneel123/three-insights-for-building-great-claude-skills-47o1</link>
      <guid>https://dev.to/swapnoneel123/three-insights-for-building-great-claude-skills-47o1</guid>
      <description>&lt;p&gt;Most Claude skills that never fire are not broken. Their description is.&lt;/p&gt;

&lt;p&gt;That one fact is behind almost every "why won't my skill trigger" post you'll find. Three things separate a working skill from one that sits there ignored: a description written like a routing rule, a &lt;code&gt;SKILL.md&lt;/code&gt; kept short on purpose, and pushing fragile steps into scripts instead of long instructions. Get those three right, and everything else gets much easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, what a Claude skill actually is
&lt;/h2&gt;

&lt;p&gt;A skill is a folder with a &lt;code&gt;SKILL.md&lt;/code&gt; file in it. At the top of that file, you have a small YAML header with a name and a description, followed by instructions written in markdown. That's the whole concept. Anthropic launched skills in October 2025 and &lt;a href="https://venturebeat.com/ai/anthropic-launches-enterprise-agent-skills-and-opens-the-standard" rel="noopener noreferrer"&gt;made the format an open standard&lt;/a&gt; in December, so Claude Code, the API, and claude.ai all use the exact same folder structure.&lt;/p&gt;

&lt;p&gt;The interesting part is how skills load. Claude doesn't read your entire skill upfront. When it starts up, it reads only the name and description of every installed skill, costing around 100 tokens each. Claude loads the main instructions only when it decides your skill fits what you asked for. And it reads extra helper files only when those instructions specifically point to them. Anthropic calls this &lt;a href="https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview" rel="noopener noreferrer"&gt;progressive disclosure&lt;/a&gt;. Two of the three insights below come down to working with this design instead of fighting it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgz1vqxln5j6jxssvavdi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgz1vqxln5j6jxssvavdi.png" alt="A folder reveals a skill’s metadata, instructions, and helper script loading in stages." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This means skills are cheap to keep around, costing you tokens only when they actually run. That single design choice shapes what makes a skill good or bad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Insight 1: Your description is a routing rule, not a summary
&lt;/h2&gt;

&lt;p&gt;Here is where people get confused. Your description isn't a friendly summary for humans browsing your repository. It is a routing rule Claude uses to pick your skill out of the pile.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9bmhnhds2rzhe73g61my.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9bmhnhds2rzhe73g61my.png" alt="Prompt paths enter a routing switch; precise triggers pass and vague descriptions stop." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Remember what Claude actually sees at startup: only the name and description of each skill. Nothing else. When you send a prompt, Claude decides whether to open your skill's full text based purely on that description. If the description is vague, Claude never reads the instructions. The best instructions in the world won't help if your skill never triggers.&lt;/p&gt;

&lt;p&gt;That's why a skill that won't trigger is almost never a code bug. It just means your description doesn't explain when to trigger using the words you actually type.&lt;/p&gt;

&lt;p&gt;A good description handles two jobs: what the skill does, and when Claude should run it. Write it in the third person (like "Reviews pull requests" instead of "I review" or "Review this"). Because Claude reads this inside its system prompt, mixing perspectives can confuse the model. You have up to 1,024 characters, so use that space for the exact phrases you would normally type.&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reviewing-pull-requests&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reviews&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pull&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;diffs&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;against&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;team's&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;checklist&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;flags&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;risky&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;changes.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Use&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;when&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;asks&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PR,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;diff,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;check&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;staged&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;changes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;before&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;merging."&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the description names the triggers out loud: "review a PR", "review a diff", "check staged changes". Compare that to "Helps with code review", which competes with everything Claude already knows about code review and loses.&lt;/p&gt;

&lt;p&gt;The name matters less than the description, but it has strict rules that catch people off guard. It must use only lowercase letters, numbers, and hyphens, capped at 64 characters. It cannot include the words "anthropic" or "claude". Anthropic recommends names ending in -ing (&lt;code&gt;reviewing-pull-requests&lt;/code&gt;, &lt;code&gt;analyzing-spreadsheets&lt;/code&gt;) to clearly show what action the skill takes. A name like &lt;code&gt;claude-pr-helper&lt;/code&gt; breaks two rules at once, while a generic name like &lt;code&gt;helper&lt;/code&gt; gives no useful information.&lt;/p&gt;

&lt;p&gt;There is real data behind this. One developer ran &lt;a href="https://medium.com/@ivan.seleznov1/why-claude-code-skills-dont-activate-and-how-to-fix-it-86f679409af1" rel="noopener noreferrer"&gt;650 activation trials&lt;/a&gt; on the same skills. They found that passive descriptions triggered far less reliably than direct instructions starting with "Use this skill when." While this isn't an official benchmark from Anthropic, the takeaway matches both the official documentation and real-world use: name the specific situation where Claude should trigger, not just what it can do.&lt;/p&gt;

&lt;p&gt;Also, keep your scope realistic. If it's too broad ("helps with frontend"), Claude will pick its own built-in knowledge instead. If it's too narrow ("fixes React 18 hydration errors in Next.js 14 app router"), it won't run if you phrase your prompt slightly differently. You are describing a general task, not a specific bug ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Insight 2: Progressive disclosure is a token budget you spend
&lt;/h2&gt;

&lt;p&gt;Once a skill triggers, its &lt;code&gt;SKILL.md&lt;/code&gt; body loads into the same context window as your conversation, your files, and every other skill's metadata. Anthropic makes a great point in their &lt;a href="https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices" rel="noopener noreferrer"&gt;authoring guide&lt;/a&gt;: the context window is shared space. Every token you waste in your instruction file is memory the model can't use for your actual work.&lt;/p&gt;

&lt;p&gt;This means your main skill file should be smaller than you might expect. The official recommendation is to keep &lt;code&gt;SKILL.md&lt;/code&gt; under 500 lines. Move anything longer into separate files that load only when Claude needs them. Loading happens in stages, and each stage has a different cost:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;When it loads&lt;/th&gt;
&lt;th&gt;Rough cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Name and description&lt;/td&gt;
&lt;td&gt;Always, at startup&lt;/td&gt;
&lt;td&gt;~100 tokens per skill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SKILL.md body&lt;/td&gt;
&lt;td&gt;When the skill triggers&lt;/td&gt;
&lt;td&gt;Under 5k tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundled files and scripts&lt;/td&gt;
&lt;td&gt;Only when referenced&lt;/td&gt;
&lt;td&gt;Nothing until read&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F49tkgx6zafxd3thsb00x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F49tkgx6zafxd3thsb00x.png" alt="Instruction pages narrow through a funnel so only the needed context reaches the working window." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That third row is the real secret. You can include a 2,000-line API guide, datasets, or code examples, and they cost zero tokens until Claude actually needs them. So the best approach is a lean &lt;code&gt;SKILL.md&lt;/code&gt; that acts like a table of contents, pointing Claude to larger files when necessary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reviewing-pull-requests/
  SKILL.md          # short: the workflow and when to read what
  checklist.md      # loaded only when reviewing
  examples.md       # loaded only when Claude wants a sample
  scripts/
    diff_stats.py   # executed, never loaded into context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The documentation highlights two practical rules here. First, keep linked files only one level deep and link them directly inside &lt;code&gt;SKILL.md&lt;/code&gt;. If you bury files inside subfolders, Claude often previews just the first 100 lines and misses the rest. Second, don't explain concepts Claude already understands. Anthropic uses a straightforward example: a good instruction to extract PDF text is about 50 tokens of code. A bad one uses 150 tokens because it starts by explaining what a PDF is. Claude already knows what a PDF is!&lt;/p&gt;

&lt;p&gt;Notice what I'm doing here, too. I'm not explaining what a token is or how context windows work. If you're building skills, you already understand those basics. That's the exact same mindset your skills need: only write what the model doesn't already know.&lt;/p&gt;

&lt;p&gt;I care about this point a lot because of my work building &lt;a href="https://anrl-site.vercel.app" rel="noopener noreferrer"&gt;ANRL&lt;/a&gt;. It's a compact data format created to cut token waste and context fragmentation, and it reduced formatting overhead by more than 40%. When you spend weeks fighting to save 40% on tokens, watching someone waste 3,000 tokens in &lt;code&gt;SKILL.md&lt;/code&gt; re-explaining JSON is painful. Keeping your skills lean isn't just about clean formatting—it protects the model's working memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Insight 3: Let code do the deterministic work
&lt;/h2&gt;

&lt;p&gt;The last one separates skills that feel reliable from skills that mostly work.&lt;/p&gt;

&lt;p&gt;If a step is deterministic (parse this file, validate this schema, sort these rows), do not write instructions asking Claude to be careful. Write a script and tell Claude to run it. When Claude runs a script through bash, only the output comes back into context, not the code. So a bundled &lt;code&gt;validate.py&lt;/code&gt; is cheaper than asking Claude to generate the same validation every time, and it does the exact same thing on every run instead of a slightly different thing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg06ndx00o8to85u67nqt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg06ndx00o8to85u67nqt.png" alt="A plan crosses a guarded script bridge, passes validation, and reaches a stable output." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Anthropic uses a great phrase for writing these helper scripts: "solve, don't defer." If a file might be missing, handle that check right inside the script. Don't let the script crash and force Claude to guess a fix, because improvised fixes break reliability. The same rule applies to unexplained settings: setting a 30-second timeout with an explanatory comment is much better than leaving a random number like &lt;code&gt;47&lt;/code&gt; that no one can explain later.&lt;/p&gt;

&lt;p&gt;The bigger idea is giving Claude freedom only when the task allows it. Anthropic's docs share a helpful comparison:&lt;br&gt;
Some tasks are like a narrow bridge with steep drops on both sides, like running database migrations in a strict order. For those, give Claude a single script and tell it not to alter the command. Other tasks are like an open field with no dangers, like reviewing code where the best feedback depends on what Claude finds. For those, give general guidelines and trust the model to choose the right path. Making mistakes here hurts in both directions: adding too many rigid rules in an open field makes the skill inflexible, while giving too little direction on a narrow bridge leads to errors.&lt;/p&gt;

&lt;p&gt;For bulk edits or risky actions, the documentation suggests a three-step pattern: plan, validate, and execute. Have Claude write its planned changes to a file first. Next, run a script that checks that plan for mistakes. Finally, apply the changes only if the check passes. It might sound like extra work, but it's the difference between catching an error in a safe file and catching it after it breaks your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  When one skill becomes twenty
&lt;/h2&gt;

&lt;p&gt;Those three insights help you build a solid individual skill. But developers rarely stop at just one. Once your team starts collecting dozens of skills, two quiet problems start showing up.&lt;/p&gt;

&lt;p&gt;First, skills can connect to external tools through MCP (if you haven't used MCP yet, check out my &lt;a href="https://www.swapnoneel.site/blog/what-is-an-mcp-server" rel="noopener noreferrer"&gt;beginner's guide to MCP servers&lt;/a&gt;). The easiest way to think about the difference: MCP provides the tool, while the skill teaches Claude how to use it for your specific workflow. But pay close attention to Anthropic's security warning: a skill can tell Claude to run tools in ways the author never mentioned. That's why they recommend checking every file inside any skill you didn't write yourself.&lt;/p&gt;

&lt;p&gt;Second, it's best practice to test skills across different models like Haiku, Sonnet, and Opus. A skill is only as reliable as the model running it. In a team environment, that means your skills end up calling different models across different API providers.&lt;/p&gt;

&lt;p&gt;Combine that with several teammates installing community skills from GitHub, and you aren't just managing simple markdown files anymore. You have unmonitored tool calls and model requests running across your team. That's an entirely different problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi8hkijfuymdbellxer1x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi8hkijfuymdbellxer1x.png" alt="A shelf of skills expands into a connected ecosystem inspected for hidden tool risk." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Do you need a gateway for your skills?
&lt;/h2&gt;

&lt;p&gt;If you're running just two skills on your laptop, you don't. Seriously, skip this section and go write your skills instead of setting up infrastructure. A gateway won't make an individual skill better, and it won't fix a bad description.&lt;/p&gt;

&lt;p&gt;A gateway starts to matter when you can no longer answer simple questions: Who is allowed to run which tool? Which skill triggered this request? Whose API budget is paying for it? When you hit that point, routing all that traffic through one place makes life much easier. That place is an AI gateway, and the one I keep returning to is &lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost (by Maxim AI)&lt;/a&gt;, an open-source AI gateway and control plane for LLMs, MCP tools, and agents (&lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;repo here&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjcuzh381b5f9rtjrg7ja.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjcuzh381b5f9rtjrg7ja.png" alt="A small setup runs directly while a growing team routes tools and models through one gateway." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two of its core features line up directly with the scaling problems above.&lt;/p&gt;

&lt;p&gt;To secure tool calls, Bifrost acts as an &lt;a href="https://docs.getbifrost.ai/mcp/filtering" rel="noopener noreferrer"&gt;MCP gateway with deny-by-default tool filtering&lt;/a&gt;. Without explicit permissions, zero tools are accessible. You define what to unlock across client settings, request headers, and virtual key configurations. This means a skill could ask for a filesystem MCP server, but its key can restrict execution strictly to &lt;code&gt;read_file&lt;/code&gt;, completely blocking destructive actions like &lt;code&gt;delete_file&lt;/code&gt;. You don't have to depend on plain text instructions in a prompt to keep your systems safe. (It even has an MCP Code Mode that runs tool workflows inside a sandbox instead of dumping every tool schema into your context, cutting token usage by over 92% across large tool sets.)&lt;/p&gt;

&lt;p&gt;To manage model access, Bifrost provides &lt;a href="https://docs.getbifrost.ai/features/governance" rel="noopener noreferrer"&gt;virtual keys and governance rules&lt;/a&gt;. Each key gets its own list of approved models (across 25+ providers and 10,000+ models), an automatic budget cap with reset periods, and custom rate limits. So when you want your team to test a skill across Haiku, Sonnet, and Opus, you give them one key with an allow-list and a spending cap instead of juggling five API keys in a spreadsheet. Provider fallback and retries are built in, so a skill doesn't fall over if one provider has an outage.&lt;/p&gt;

&lt;p&gt;Performance is where this actually holds up. If a gateway is slow, developers route around it. Bifrost is written in Go with memory pooling and pre-warmed connections, keeping added gateway overhead to about 11 microseconds at 5,000 requests per second on a single t3.xlarge instance. In Maxim's published benchmarks against LiteLLM (tested with 500 concurrent users on AWS), Bifrost delivered 9.5x higher throughput (424 req/s vs 44.84 req/s) and roughly 50x lower P50 latency (804 ms vs 38.65 s). Those are vendor benchmarks, so treat them as such, but the main point holds: the gateway manages your traffic without becoming the slow part.&lt;/p&gt;

&lt;p&gt;None of this is about writing an individual skill. It's just what helps once your skills work well and you start scaling up.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what actually makes a Claude skill great?
&lt;/h2&gt;

&lt;p&gt;It's not about making a giant &lt;code&gt;SKILL.md&lt;/code&gt; file. It's much simpler than that.&lt;/p&gt;

&lt;p&gt;Write your description like a clear routing rule in the third person, using the exact words you'd naturally type in a prompt. Keep the main instructions short and let extra reference files handle the heavy lifting, because context memory isn't free. And move fragile, repetitive tasks into code scripts so they run consistently every single time instead of guessing. Those three habits make the difference between a skill that runs reliably and one that sits forgotten in a folder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0mqczjiu9kscgq8x4ld4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0mqczjiu9kscgq8x4ld4.png" alt="Three rising habits—routing, lean instructions, and deterministic code—build a reliable skill." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using an AI gateway matters as well, but only later on. Don't worry about it until you have more skills and tools than you can easily keep track of in your head. You'll know when you cross that line. Until then, setting up a gateway is just extra work you don't need.&lt;/p&gt;

&lt;p&gt;If you've built skills that trigger reliably, I'd love to hear how you wrote your descriptions to make them work. That's still the area where most people spend the least effort. Come share your thoughts on &lt;a href="https://x.com/swapnoneel123" rel="noopener noreferrer"&gt;X&lt;/a&gt;, or check out my other posts at &lt;a href="https://www.swapnoneel.site" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Cut Time to First Token (TTFT) in LLM Apps</title>
      <dc:creator>Swapnoneel Saha</dc:creator>
      <pubDate>Fri, 04 Sep 2026 15:49:58 +0000</pubDate>
      <link>https://dev.to/swapnoneel123/how-to-cut-time-to-first-token-ttft-in-llm-apps-b0f</link>
      <guid>https://dev.to/swapnoneel123/how-to-cut-time-to-first-token-ttft-in-llm-apps-b0f</guid>
      <description>&lt;p&gt;Whenever you type something into an AI app like ChatGPT or Gemini, and hit enter, you have to stare at nothing for half a second, one second, and sometimes even more. Then the words start pouring out fast. The wait time that you always experience in between these two events is called time to first token, or TTFT, and it is the single thing your users actually feel.&lt;/p&gt;

&lt;p&gt;Time to first token (TTFT) is the gap between sending a request and the first token coming back. To cut it, shorten and cache your prompt so the model reads less, stream the reply, keep connections warm, and route around slow providers. The biggest lever is prompt length, since most of that wait is the model reading your input.&lt;/p&gt;

&lt;p&gt;Now let me show you where the time actually goes, and how we can fix this. So, let's begin!&lt;/p&gt;

&lt;h2&gt;
  
  
  What Time to First Token Actually Is
&lt;/h2&gt;

&lt;p&gt;When you send a prompt, the model does two very different jobs.&lt;/p&gt;

&lt;p&gt;First it reads everything you sent. Every token of your system prompt, your chat history, your retrieved documents, your question. It runs all of that through the network in one big forward pass and builds an internal memory of it. This step is called prefill, and nothing comes back to you while it runs.&lt;/p&gt;

&lt;p&gt;Then it starts writing, one token at a time. That second phase is where you see text stream in.&lt;/p&gt;

&lt;p&gt;TTFT covers the first phase plus everything around it: the network trip to the provider, any time your request spends waiting in a queue, and the prefill itself. Here is the part that matters. On any prompt that is not tiny, prefill dominates, and the more tokens you send, the more &lt;a href="https://clickhouse.com/resources/engineering/llm-inference-latency" rel="noopener noreferrer"&gt;prefill work&lt;/a&gt; the model does before it can start. A short prompt starts answering quickly. A prompt stuffed with 8,000 tokens of context makes the reader sit and watch the cursor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwbbzan004icvor2ke7cu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwbbzan004icvor2ke7cu.png" alt="Illustration of the path from a prompt to the first token" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Good Time to First Token?
&lt;/h2&gt;

&lt;p&gt;It depends on what the app is for.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://handbook.modular.com/llm-inference-basics/llm-inference-metrics" rel="noopener noreferrer"&gt;Modular LLM Inference Handbook&lt;/a&gt; puts it simply: a chatbot usually needs TTFT under 500 milliseconds to feel responsive, and a code-completion tool needs it below 100 milliseconds or it feels broken. A batch job that emails a summary once a day can take five seconds and nobody cares.&lt;/p&gt;

&lt;p&gt;So before you optimize anything, decide which of these you are. A chat UI and a background report have completely different budgets, and chasing 100ms on a nightly job is wasted effort.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgn4jgcpvibw4m8c9otbc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgn4jgcpvibw4m8c9otbc.png" alt="Illustration comparing TTFT targets for chat, code completion, and batch work" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fixes, Biggest Wins First
&lt;/h2&gt;

&lt;p&gt;Here is the order I actually work through. The early steps cost you almost nothing and pay back the most. The later ones matter, but only after the cheap wins are done. Do not skip to the bottom.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiueoiqvsei4npgygwcfr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiueoiqvsei4npgygwcfr.png" alt="Illustration of TTFT improvements ranked from measurement to routing" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Measure TTFT on its own, before you touch anything
&lt;/h3&gt;

&lt;p&gt;You cannot cut a number you are not looking at. And most teams are not looking at this one, because their logging records total response time and stops there. Total time hides TTFT completely, since a slow first token and a slow-typing model both show up as one big number.&lt;/p&gt;

&lt;p&gt;So split them. Log the time from request-sent to first-token-received as its own field, separate from the time to finish. Do it at the p50 and the p99, because the average will lie to you and the tail is where users rage-quit.&lt;/p&gt;

&lt;p&gt;This is not theory for me. At Zonko Labs I built the internal tool that captured the data logs for Luffy, an AI co-worker that lived in Slack, and turned them into reports on latency and probable slowdowns. The first thing that tool taught everyone was that our "the bot is slow" complaints were almost always a first-token problem, not a typing-speed problem. You only learn that if you measure the two apart.&lt;/p&gt;

&lt;p&gt;If your traffic runs through an AI gateway, you can track this without building custom timers in your app. For example, &lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; exports a metric called &lt;a href="https://docs.getbifrost.ai/features/observability/prometheus" rel="noopener noreferrer"&gt;&lt;code&gt;bifrost_stream_first_token_latency_seconds&lt;/code&gt;&lt;/a&gt;. It measures TTFT automatically across OpenAI, Anthropic, or any other provider, so you get clean p50 and p99 charts right out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Shorten the prompt
&lt;/h3&gt;

&lt;p&gt;This is the biggest lever you fully control, and it is the one no tool can pull for you.&lt;/p&gt;

&lt;p&gt;Prefill scales with input length, so every token you delete is prefill work the model never has to do. Look hard at what you are actually sending. Most apps ship a system prompt that grew over months and never got trimmed, a full chat history when the last four turns would do, and RAG retrieval that dumps ten documents when three answer the question.&lt;/p&gt;

&lt;p&gt;Cut the system prompt to what the model needs, not what makes you feel safe. Truncate or summarize old history instead of resending all of it. Retrieve fewer, better chunks. None of this is glamorous, and all of it moves TTFT more than the fancy stuff.&lt;/p&gt;

&lt;p&gt;Be honest about the tradeoff, though. Cutting context can cost you answer quality, so trim, then check your evals, then trim again. This is the one place where going too far actually hurts, so it earns the care.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Cache the stable part of your prompt
&lt;/h3&gt;

&lt;p&gt;Most of what you send is the same every single call. The system prompt does not change. The tool definitions do not change. Only the user's question at the end changes. Prefill does not know that, so it re-reads the whole thing every time, unless you turn on caching.&lt;/p&gt;

&lt;p&gt;There are two flavors, and they are not the same thing.&lt;/p&gt;

&lt;p&gt;Prompt caching, also called prefix caching, is offered by the model providers themselves. It stores the processed form of a stable prefix so the next call reuses that work instead of redoing it, which is what &lt;a href="https://llm-d.ai/blog/kvcache-wins-you-can-see" rel="noopener noreferrer"&gt;prefix caching&lt;/a&gt; buys you. Put the parts that never change at the very front of your prompt and the changing question at the end, so the cache has the longest possible prefix to hit.&lt;/p&gt;

&lt;p&gt;Semantic caching is the bigger swing. Instead of caching the prompt's prefill, it caches the whole answer, and serves it when a new question means the same thing as an old one. On a hit, you skip the model entirely, so TTFT collapses from "prefill a big prompt" to "look up a vector," which is milliseconds. I wrote a &lt;a href="https://www.swapnoneel.site/blog/what-is-semantic-caching" rel="noopener noreferrer"&gt;full explainer on semantic caching&lt;/a&gt; if you want the mechanics.&lt;/p&gt;

&lt;p&gt;Let me make the caching point concrete with a small test you can predict.&lt;/p&gt;

&lt;p&gt;Call A sends a 3,000-token system prompt that never changes, then a 40-token question at the end. Call B folds the same information into a fresh 3,000-token prompt that gets reworded on every request, then the same 40-token question. Both send roughly the same number of tokens. On the hundredth request, which one has the lower TTFT?&lt;/p&gt;

&lt;p&gt;Call A wins, and it is not close. Call A's 3,000-token prefix is identical each time, so after the first call it lives in the prompt cache and its prefill is skipped. Call B looks different to the cache every time, so it pays the full prefill on all one hundred calls. Same token count, wildly different first-token time, decided entirely by whether the front of your prompt holds still. That is the whole game with caching: keep the front stable.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Stream the response, and know its one limit
&lt;/h3&gt;

&lt;p&gt;Streaming sends each token to the user the instant it is generated, instead of waiting for the full answer. Turn it on. Every serious chat UI streams, and it is a one-line change in most SDKs.&lt;/p&gt;

&lt;p&gt;Now the honest part, because it trips people up. &lt;strong&gt;Does streaming reduce TTFT?&lt;/strong&gt; No. Streaming does not make the first token arrive any sooner, since prefill still has to finish first. What it changes is everything after that first token, and it changes how fast the whole thing &lt;em&gt;feels&lt;/em&gt;. People read a streaming reply as faster than a non-streaming one even when the total time is identical, which is why &lt;a href="https://codeant.ai/blogs/ai-first-token-latency" rel="noopener noreferrer"&gt;faster first tokens&lt;/a&gt; matter more than total speed.&lt;/p&gt;

&lt;p&gt;So streaming is a perception win, not a TTFT win, and the two stack. Streaming only pays off if TTFT is already short, because a three-second blank screen followed by a burst still reads as three seconds of nothing. Fix TTFT first, then stream to make the rest feel quick.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Keep the connection warm and close
&lt;/h3&gt;

&lt;p&gt;Some of your TTFT is pure plumbing, and it hides where nobody looks.&lt;/p&gt;

&lt;p&gt;Every fresh HTTPS connection pays for a TLS handshake before a single byte of your prompt moves. If your app opens a new connection per request, you are paying that tax every time. Reuse connections with keep-alive and connection pooling so the handshake happens once, not on every call.&lt;/p&gt;

&lt;p&gt;Distance costs you too. A request from a server in Mumbai to a model endpoint in Virginia spends real milliseconds just crossing the planet and back, twice, before prefill even starts. Pick a provider region near your users where you can, and cut the number of hops between your app and the model. If you are on a serverless setup, watch for cold starts, since a function spinning up from zero can add more delay than the model does.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Route around slow and rate-limited providers
&lt;/h3&gt;

&lt;p&gt;Here is the failure mode that ruins TTFT and has nothing to do with your prompt. The provider is having a bad minute.&lt;/p&gt;

&lt;p&gt;When a provider is overloaded, your request sits in its queue, and queue time is part of TTFT. When you hit a rate limit, you get a 429 and your client backs off and retries, which stacks seconds onto the first token before it ever appears. Neither of these is fixed by shortening your prompt, because the model never even started.&lt;/p&gt;

&lt;p&gt;The fix is to not be stuck with one provider. Send the request to the fastest healthy option, and when one starts failing or crawling, fall over to another automatically instead of retrying into the same wall. This is &lt;a href="https://www.swapnoneel.site/blog/what-is-adaptive-load-balancing" rel="noopener noreferrer"&gt;adaptive load balancing&lt;/a&gt; applied to model traffic, and it is the difference between one bad provider-minute taking down your p99 and your users never noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a Gateway Does This For You
&lt;/h2&gt;

&lt;p&gt;Let me be honest about what a gateway cannot do first.&lt;/p&gt;

&lt;p&gt;A gateway cannot shorten your prompt. It cannot make a provider's GPU run faster. If your app only calls one model and your prompt is already lean, you do not need a gateway for TTFT. You will get much bigger gains just by trimming your system prompt.&lt;/p&gt;

&lt;p&gt;Where an AI gateway helps is when you want to handle caching, retries, and monitoring across multiple services without rebuilding them in every backend repo. The gateway sits between your application and your model providers as a reverse proxy, so you wire those fixes once in your infrastructure instead of in your app code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Floc2kyys6pjispqljjzk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Floc2kyys6pjispqljjzk.png" alt="Illustration of an AI gateway coordinating caching, metrics, and fallbacks" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have used &lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt;, an open-source AI gateway built in Go, and it handles these levers in one place:&lt;/p&gt;

&lt;p&gt;First, it gives you &lt;strong&gt;TTFT metrics out of the box&lt;/strong&gt;. Measuring first-token latency manually usually means writing custom streaming wrappers in Python or Node.js to timestamp the first chunk. Bifrost measures &lt;code&gt;bifrost_stream_first_token_latency_seconds&lt;/code&gt; at the proxy layer for every request, giving you clean p50 and p99 charts across all your providers without extra client telemetry.&lt;/p&gt;

&lt;p&gt;Second, it provides &lt;a href="https://docs.getbifrost.ai/features/semantic-caching" rel="noopener noreferrer"&gt;semantic caching&lt;/a&gt;. If a user asks a question that matches a previous one, Bifrost serves the answer straight from cache in about 10 to 15 milliseconds. That turns an 800-millisecond prefill wait into almost nothing. Plus, cache writes happen asynchronously in the background, so cache misses do not add any latency to your stream.&lt;/p&gt;

&lt;p&gt;Third, it runs &lt;strong&gt;automatic fallbacks&lt;/strong&gt;. When a provider is overloaded or returns a 429 rate limit, your request sits in a queue and your TTFT spikes. With Bifrost, you can set fallback rules: if your primary model hangs or errors, it instantly shifts the request to a healthy backup provider instead of leaving your user staring at an empty screen.&lt;/p&gt;

&lt;p&gt;Finally, there is the question of &lt;strong&gt;proxy overhead&lt;/strong&gt;. Adding a gateway puts an extra hop in your network path. If that hop adds 30 or 50 milliseconds, it defeats the entire purpose of optimizing latency. Bifrost is written in Go and adds around 20 microseconds of overhead under load, meaning the hop itself is practically invisible.&lt;/p&gt;

&lt;p&gt;You still have to fix your prompts first. But once your prompts are clean, putting a gateway in front of your models lets you handle caching, fallbacks, and latency tracking through simple configuration instead of messy glue code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start Here
&lt;/h2&gt;

&lt;p&gt;If you do nothing else from this whole post, do three things, in this order.&lt;/p&gt;

&lt;p&gt;Measure TTFT as its own metric, so you stop flying blind. Shorten and cache your prompt, because that is where the biggest, cheapest wins live. Then stream, so the rest of the answer feels fast.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd49u0m6wsdsmpjjkqytw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd49u0m6wsdsmpjjkqytw.png" alt="Illustration of the three steps to reduce TTFT" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The truth most infrastructure posts bury is that for an app that calls an API, time to first token is mostly about prompt length and caching, not about exotic GPU tricks you will never touch. The GPU tricks are real, but they belong to the people running the models. Your levers are the prompt you send, the cache in front of it, and the route it takes. Those you own completely.&lt;/p&gt;

&lt;p&gt;Start by logging your p50 and p99 first-token time this week. I would bet the number surprises you, and I would bet a fat system prompt is the reason.&lt;/p&gt;

&lt;p&gt;If you have a TTFT horror story, or a fix that worked that I left out, tell me. I am &lt;a href="https://x.com/swapnoneel123" rel="noopener noreferrer"&gt;@swapnoneel123&lt;/a&gt; on X, and I write more of these on &lt;a href="https://www.swapnoneel.site" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>performance</category>
    </item>
    <item>
      <title>What Is an MCP Registry? Discovering &amp; Governing MCP Servers</title>
      <dc:creator>Swapnoneel Saha</dc:creator>
      <pubDate>Thu, 03 Sep 2026 17:01:44 +0000</pubDate>
      <link>https://dev.to/swapnoneel123/what-is-an-mcp-registry-discovering-governing-mcp-servers-2dd3</link>
      <guid>https://dev.to/swapnoneel123/what-is-an-mcp-registry-discovering-governing-mcp-servers-2dd3</guid>
      <description>&lt;p&gt;Most people consider an MCP registry as "npm for MCP servers." It is a nice line to explain the concept, but it is partially true and extremely half-baked.&lt;/p&gt;

&lt;p&gt;An MCP registry is a searchable catalog of MCP servers. It stores metadata, like a server's name, where to install it, and how a client should connect, so an AI agent or a developer can find servers to plug in. But here is the part the npm comparison hides: a registry is a discovery layer, not a trust layer. It tells you a server exists. It does not tell you the server is safe, or that your agents should be allowed to run it.&lt;/p&gt;

&lt;p&gt;That gap between "I found it" and "I trust it" is where teams get burned. Discovering MCP servers and governing MCP servers are two different jobs, and a registry only does the first one. So let's get both straight.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want to know more about what an MCP server is and how it works, I wrote a &lt;a href="https://www.swapnoneel.site/blog/what-is-an-mcp-server" rel="noopener noreferrer"&gt;beginner's guide to MCP servers&lt;/a&gt; first. This post picks up where that one left off.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What an MCP Registry Actually Is
&lt;/h2&gt;

&lt;p&gt;An MCP registry is a metadata catalog. It does not hold the server's code. It holds a small record that describes the server and points to wherever the code actually lives.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcefrhqor7m9l0cjfli72.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcefrhqor7m9l0cjfli72.png" alt="A searchable MCP registry catalog with server metadata cards." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This trips people up, so let me separate the two clearly. Your code lives on a package registry: npm, PyPI, Docker Hub. That is where the actual bits get downloaded from. The MCP registry sits one level up and just stores a pointer. For example, a &lt;code&gt;weather-mcp&lt;/code&gt; package can live on npm, and the MCP registry entry maps "weather v1.2.0" to &lt;code&gt;npm:weather-mcp&lt;/code&gt;. The registry is the index card. npm is the shelf.&lt;/p&gt;

&lt;p&gt;Each entry follows a standard shape called &lt;code&gt;server.json&lt;/code&gt;. It records the server's unique name, where to find it (an npm package name, or a remote server URL), how to run it (command-line args, environment variables), and some discovery data like a description and what the server can do. That is it. A registry is a stack of these &lt;code&gt;server.json&lt;/code&gt; cards, plus an API to search them.&lt;/p&gt;

&lt;p&gt;And that is genuinely useful. Before any of this, finding an MCP server meant scrolling GitHub READMEs and random Discord links. A registry turns that into one queryable list. But notice what a stack of index cards can and cannot do for you, because that is where the trouble starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Official MCP Registry
&lt;/h2&gt;

&lt;p&gt;The one most people mean by "the MCP registry" is the &lt;a href="https://registry.modelcontextprotocol.io" rel="noopener noreferrer"&gt;official MCP Registry&lt;/a&gt;, which launched in preview on &lt;a href="https://blog.modelcontextprotocol.io/posts/2025-09-08-mcp-registry-preview/" rel="noopener noreferrer"&gt;8 September 2025&lt;/a&gt;. It is open source, and it is backed by a serious group: Anthropic, GitHub, PulseMCP, Microsoft, and others.&lt;/p&gt;

&lt;p&gt;Its job is deliberately narrow. In the maintainers' own framing, it is a centralized metadata repository, and the metadata it stores is intentionally unopinionated. It does not rank servers, and it does not review or rate them. It just holds the &lt;code&gt;server.json&lt;/code&gt; records and serves them over a REST API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvkj4odc0zcuit2mxa3mi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvkj4odc0zcuit2mxa3mi.png" alt="The official MCP Registry shown as a shared metadata source." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the design choice that matters most, and almost nobody mentions it. The official registry is not really built for your app to read directly. It expects to be consumed by "downstream aggregators", which pull its data on a schedule (say, once an hour) and then layer their own curation, ratings, or search on top. The registry is the wholesale source of truth. The nice front-ends you actually browse are meant to be built by other people.&lt;/p&gt;

&lt;p&gt;And in practice, as of 2026, most MCP clients still do not read the registry directly as a built-in source. So for now you usually meet it through one of those aggregators, not through your agent going and querying it live.&lt;/p&gt;

&lt;p&gt;Two more things worth knowing early. First, the official registry does not host private servers, so an internal &lt;code&gt;mcp.acme-corp.internal&lt;/code&gt; server has no place there. Second, it is in preview, which means data can reset and the shape can still change. So treat it as a fast-moving foundation, not a finished product.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Registry vs Marketplace vs Gateway
&lt;/h2&gt;

&lt;p&gt;This is the question I see confused most, so let me draw the lines hard. These are three different things doing three different jobs.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;registry&lt;/strong&gt; is discovery in its rawest form. It answers one question: what servers exist, and how do I connect to them? It is a passive list.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;marketplace&lt;/strong&gt; is discovery made pleasant. Sites like &lt;a href="https://smithery.ai" rel="noopener noreferrer"&gt;Smithery&lt;/a&gt;, Glama, and mcp.so give you search, categories, install buttons, and community ratings. Increasingly they read from the official registry underneath and add their own polish on top. A marketplace is a registry with a nice storefront and an opinion.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;gateway&lt;/strong&gt; is something else entirely. It is not about finding servers, it is about controlling them at runtime. A gateway sits in the traffic path between your agents and your MCP servers, and it enforces the rules every time a call is made: who is allowed to use this tool, how often, at what cost, with what data. A registry is a directory. A gateway is a checkpoint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjujtawbghe3ufx7n2t90.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjujtawbghe3ufx7n2t90.png" alt="A registry directory, marketplace storefront, and runtime gateway checkpoint." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The one line to remember: a registry without a gateway gives you discovery but no runtime governance. Your agents can find tools, but nothing stops an unauthorized call once they connect. Hold onto that, because it is the seam the rest of this post walks through.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Discovery Actually Works
&lt;/h2&gt;

&lt;p&gt;So how does a client go from "I need a GitHub server" to a running connection? Through the registry API and the &lt;code&gt;server.json&lt;/code&gt; record, and the interesting part is how it proves a server is who it claims to be.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxy6p4mdhykuv6j4v0j2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxy6p4mdhykuv6j4v0j2x.png" alt="MCP server discovery from verified namespace to connection." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Server names use a reverse-DNS format: &lt;code&gt;io.github.username/server-name&lt;/code&gt;, or &lt;code&gt;com.example/server&lt;/code&gt;. That name is not decorative. It ties the server to a namespace that the publisher had to prove they own, through GitHub or DNS verification. If you publish under &lt;code&gt;io.github.swpn0neel/*&lt;/code&gt;, the registry made you show you actually control that GitHub account first. So a name is a small ownership claim, verified at publish time.&lt;/p&gt;

&lt;p&gt;Here is the piece that makes the whole thing extensible. The registry ships an &lt;a href="https://modelcontextprotocol.io/registry/about" rel="noopener noreferrer"&gt;open OpenAPI spec&lt;/a&gt;, which means anyone can stand up their own registry that speaks the exact same interface. A public marketplace can implement it. A company can implement a private one for internal servers. Any MCP client that already knows how to talk to the official registry can talk to those too, with no extra work. One shape, many registries.&lt;/p&gt;

&lt;p&gt;That design is genuinely clever, and it is also exactly why governance cannot live at this layer. A standard that lets anyone publish, and lets anyone run a compatible registry, is a standard optimized for reach, not for safety. Openness and vetting pull in opposite directions, and the registry picked openness on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovery Is the Easy Half
&lt;/h2&gt;

&lt;p&gt;So that is discovery, more or less solved. You have a standard catalog, verified namespaces, an API, and a way to run your own copy.&lt;/p&gt;

&lt;p&gt;But discovery was always the easy half of the title. The hard half is governing what those servers actually do once your agents start calling them, and that is a problem a catalog structurally cannot touch. Let me show you why with one concrete example.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz4az02iq3frlhjsrlfga.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz4az02iq3frlhjsrlfga.png" alt="A discovery catalog ending before runtime governance begins." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Registry Does Not Protect You From
&lt;/h2&gt;

&lt;p&gt;Say you tell your coding agent, "add the Slack MCP server." It searches, finds a package named &lt;code&gt;slack-mcp-server&lt;/code&gt;, sees an install command that looks completely official, and wires it in. Feels routine. Now walk that one action through each layer and watch where it does, and does not, get stopped.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq3ck5pb0l8brtywr3jt6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq3ck5pb0l8brtywr3jt6.png" alt="A verified registry entry beside a hidden security risk." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did the registry catch it?&lt;/strong&gt; No, and this is by design. The official registry &lt;a href="https://modelcontextprotocol.io/registry/about" rel="noopener noreferrer"&gt;delegates security scanning&lt;/a&gt; to the underlying package registries and to downstream aggregators. Its own job is namespace authentication and metadata hosting. So namespace verification proves the publisher owns the name they used. It does not prove the code behind that name is safe. A registry entry is a verified business card, not a background check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So typosquatting still works.&lt;/strong&gt; This is not hypothetical. Security researchers have already documented &lt;a href="https://www.speakeasy.com/resources/mcp-tool-poisoning" rel="noopener noreferrer"&gt;look-alike PyPI packages&lt;/a&gt; like &lt;code&gt;slack-mcp-server&lt;/code&gt; and &lt;code&gt;slack-mcp-server-v2&lt;/code&gt;, whose install commands look official but are not. A name that reads as legitimate is not the same as a source you vetted, and a catalog optimized for discovery will happily list both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the tool itself can lie.&lt;/strong&gt; The nastier class is tool poisoning: a malicious server hides instructions inside a tool's description, so when the model reads "what can you do?", it also reads "and quietly send recent messages to this URL." The model sees a normal tool. The registry saw a normal &lt;code&gt;server.json&lt;/code&gt;. Neither one inspected behavior, because inspecting behavior is not what either one does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then there is the rug pull.&lt;/strong&gt; A server can be perfectly clean the day it is approved and turn malicious three versions later. A registry records the new version. It does not re-judge it. This exact risk is why security researchers keep arguing that tool updates should &lt;a href="https://arxiv.org/pdf/2506.01333" rel="noopener noreferrer"&gt;trigger re-review&lt;/a&gt; instead of being trusted automatically.&lt;/p&gt;

&lt;p&gt;None of this means the registry is broken. It means the registry is doing its one job, discovery, and quietly handing you a job it was never built to do: deciding what to trust, and enforcing that decision on every call. That job has to live somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do You Need a Private MCP Registry?
&lt;/h2&gt;

&lt;p&gt;For a lot of teams, the first real answer to the trust problem is a private registry. And it genuinely helps, so let me be fair to it before I tell you where it stops.&lt;/p&gt;

&lt;p&gt;A private, internal registry is an allowlist. Instead of letting agents pull from the open catalog, your platform team curates a list of vetted servers, mixes in your own internal ones, and points every agent at that. Because it implements the same OpenAPI shape, your existing clients keep working. This is the model enterprise registry products (Kong's, Portkey's, and others) are built around, and for regulated or multi-team setups it is close to mandatory, because a community catalog was never meant to carry your compliance requirements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8r4azb2ts8ztrotkbki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8r4azb2ts8ztrotkbki.png" alt="A private registry as a curated internal MCP server allowlist." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So a private registry moves the trust decision to a place you control. That is a real upgrade over pulling from the open internet.&lt;/p&gt;

&lt;p&gt;But look closely at when it acts. A private registry governs discovery time: what a developer or agent is allowed to install and see. It does not govern runtime: what an approved server actually does on each individual call. Once an agent is connected to a vetted server, the registry is out of the loop. It cannot cap how much that agent spends, cannot say "this key may read files but never delete them," and cannot stop a poisoned tool description that slipped through your review. It curated the shelf. It is not standing at the counter.&lt;/p&gt;

&lt;p&gt;You still need something in the traffic path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Gateway Comes In
&lt;/h2&gt;

&lt;p&gt;Here is the honest part first, because it decides whether you should even read this section. If you are running one or two MCP servers you trust, you do not need a gateway. A registry, or even a hardcoded config, is plenty. A gateway earns its place when you have many servers, many agents, and real consequences if one of them misbehaves. And a gateway cannot vet server code for you either, so nothing here removes the need to review what you run.&lt;/p&gt;

&lt;p&gt;For a few months I was the first layer of testing for an AI co-worker that lived inside Slack, catching its mistakes before the real users did. The lesson that stuck with me is simple: you cannot govern what you only hear about after the fact. Governing has to happen at runtime, in the path, while the call is being made.&lt;/p&gt;

&lt;p&gt;What a gateway adds is the one thing a registry structurally cannot: enforcement on every call, at runtime. That is where &lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt;, an &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;open-source AI gateway&lt;/a&gt; built by Maxim AI, fits the picture. It can act as an &lt;a href="https://docs.getbifrost.ai/mcp/gateway" rel="noopener noreferrer"&gt;MCP gateway&lt;/a&gt;: you connect all your MCP servers to Bifrost, and your agents connect to Bifrost instead of to each server directly. One endpoint in front of everything, sitting exactly where the checks need to happen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifs86uh0q37jducspypi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifs86uh0q37jducspypi.png" alt="An MCP gateway controlling calls between agents and servers." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you are in that position, three things change.&lt;/p&gt;

&lt;p&gt;First, access stops being all-or-nothing. Bifrost filters tools per virtual key, so a given key only sees the tools you explicitly listed for it. You configure it plainly: a client named &lt;code&gt;filesystem&lt;/code&gt;, with &lt;code&gt;tools_to_execute&lt;/code&gt; set to &lt;code&gt;["read_file", "list_directory"]&lt;/code&gt;, and that key can do exactly those two things and nothing else. It is deny-by-default. The read-only agent literally cannot see a delete tool, so a poisoned or over-eager call has nothing to grab.&lt;/p&gt;

&lt;p&gt;Second, the token problem gets solved instead of tolerated. Remember how a registry lets you connect dozens of servers? Once you do, every request stuffs every tool's description into the model's context, and with 150-plus tools the model burns most of its budget just reading the menu. Bifrost's &lt;a href="https://docs.getbifrost.ai/mcp/code-mode" rel="noopener noreferrer"&gt;Code Mode&lt;/a&gt; fixes this by exposing four generic meta-tools and letting the model write code to discover and run what it needs on demand, instead of pre-loading every definition. In their own benchmark of 508 tools across 16 servers, that cut input tokens by up to 92.8%, from 75.1M tokens down to 5.4M, while keeping a perfect pass rate. That is a vendor benchmark on a vendor harness, so weigh it as one, but the mechanism is sound and the direction is not subtle.&lt;/p&gt;

&lt;p&gt;Third, control lives in one place. All your keys and connections sit behind the gateway instead of scattered across config files, and you can attach &lt;a href="https://docs.getbifrost.ai/features/governance/virtual-keys" rel="noopener noreferrer"&gt;budgets and rate limits&lt;/a&gt; to each virtual key, so an agent that goes haywire hits a spending cap instead of your invoice. This is the runtime governance the registry handed off. The registry told your agents what exists. The gateway decides what is allowed, every single time.&lt;/p&gt;

&lt;p&gt;So the two are not competitors. A registry and a gateway are the two halves the title promised, and they sit at different points in the flow: the registry at discovery, the gateway at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Do You Actually Need?
&lt;/h2&gt;

&lt;p&gt;Let me put it as plainly as I can, because the whole post comes down to one distinction.&lt;/p&gt;

&lt;p&gt;A registry answers "what exists?" A gateway answers "what is allowed?" You will eventually want both, and the mistake I see most is assuming that a shiny catalog, public or private, has solved the second question. It has not. It was never trying to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvhiq57fueu4ba0bk6m4d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvhiq57fueu4ba0bk6m4d.png" alt="A practical path from discovery to runtime governance." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So here is the path I would actually take. If you are just exploring, use the official MCP registry or a marketplace to find good servers, and keep the count small. If you are a team standing this up for real, add a private registry so you control what gets discovered in the first place. And the moment agents are making real calls with real credentials and real budgets, put a gateway in the traffic path, because that is the only layer that can enforce a decision at the moment the decision matters.&lt;/p&gt;

&lt;p&gt;Discovery is a catalog problem. Governance is a runtime problem. Solve them in the right places and MCP stops feeling scary. Try to solve governance with a directory, and you will find out the hard way that an index card never stopped anybody from doing anything.&lt;/p&gt;

&lt;p&gt;I run coding agents like Claude Code every day, and my own MCP config has quietly grown from one server to a small pile without me really deciding to. That drift is exactly how this bites you, one harmless-looking addition at a time. So if you have hit the same sprawl, I would genuinely like to hear how you are handling it. Drop a comment, or come find me on &lt;a href="https://x.com/swapnoneel123" rel="noopener noreferrer"&gt;X&lt;/a&gt; and let's compare setups.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>webdev</category>
      <category>ai</category>
      <category>security</category>
    </item>
    <item>
      <title>What Is an MCP Server? A Beginner's Guide</title>
      <dc:creator>Swapnoneel Saha</dc:creator>
      <pubDate>Wed, 02 Sep 2026 18:48:39 +0000</pubDate>
      <link>https://dev.to/swapnoneel123/what-is-an-mcp-server-a-beginners-guide-305k</link>
      <guid>https://dev.to/swapnoneel123/what-is-an-mcp-server-a-beginners-guide-305k</guid>
      <description>&lt;p&gt;An MCP server is just a small program that gives an AI model access to tools and data it cannot reach on its own.&lt;/p&gt;

&lt;p&gt;In plain words, it is a bridge. Through one shared standard called the Model Context Protocol, it lets an AI read your files, query a database, or call an API without you having to write custom glue code every single time. It can be a tiny script running on your laptop, or a service running in the cloud.&lt;/p&gt;

&lt;p&gt;None of this is as complicated as the acronym makes it sound. And it is worth getting right, because MCP is quickly becoming the default way AI agents interact with the real world. So, let's dive in!&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is an MCP Server
&lt;/h2&gt;

&lt;p&gt;An MCP server is a program that exposes three things to an AI model: tools it can run, data it can read, and prompt templates it can reuse. Simply put, the model connects to the server, asks "what have you got?", and then uses whatever comes back. Strip away the jargon and an MCP server is a standard adapter between one AI and one outside system.&lt;/p&gt;

&lt;p&gt;The "server" word trips people up, so let me kill that confusion first. It does not mean a rack in a data center. A filesystem MCP server can be a tiny script on your own laptop. It is called a server only because it serves requests, the same way your local dev server does when you run &lt;code&gt;npm run dev&lt;/code&gt;. Small program, answers questions, done.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1byxqr8e49h575iwempp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1byxqr8e49h575iwempp.png" alt="A small program running on a laptop opens a compact drawer of tools, readable data, and reusable prompts." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that is really the core of what people mean by MCP server meaning: it is the thing on the other end of the connection that knows how to do actual work, like reading a file or querying a database, and knows how to describe that work in a format the model understands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where MCP Came From and Why It Caught On
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol is an open standard for connecting AI models to outside tools and data. Anthropic &lt;a href="https://www.anthropic.com/news/model-context-protocol" rel="noopener noreferrer"&gt;released it in November 2024&lt;/a&gt;, and within a year it went from a niche idea to something Claude, ChatGPT, Cursor, and VS Code all speak.&lt;/p&gt;

&lt;p&gt;Here is the problem it fixed. Before MCP, if you wanted an AI to use some tool, you wrote the glue yourself, every single time. A couple of years back at Keploy I built a RAG chatbot over their docs, wiring the vector embeddings and the retrieval together by hand. That was one data source and one integration, fully custom. Now picture doing that again for GitHub, then Slack, then your database, then for a different model that expects a different shape. It does not scale, and everyone was rebuilding the same connectors.&lt;/p&gt;

&lt;p&gt;The official docs describe MCP as &lt;a href="https://modelcontextprotocol.io/introduction" rel="noopener noreferrer"&gt;a USB-C port for AI&lt;/a&gt;. One connector shape, and anything can plug into anything. You build an MCP server once, and every client that speaks the protocol can use it. That is the entire reason it caught on so fast: it turned a pile of one-off integrations into one standard everyone could share.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdojewl5j5fu98gzgh3yb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdojewl5j5fu98gzgh3yb.png" alt="Incompatible custom connectors resolve into a row of identical standard plugs." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Does an MCP Server Actually Work?
&lt;/h2&gt;

&lt;p&gt;An MCP server works through three roles: a host, a client, and a server. The host is the app you are actually using, like Claude Desktop or Cursor. For each server it wants to talk to, the host spins up one client, and that client keeps a dedicated line open to one MCP server. Messages go back and forth as &lt;a href="https://www.jsonrpc.org/specification" rel="noopener noreferrer"&gt;JSON-RPC 2.0&lt;/a&gt;, which is a plain, boring, well-understood format, and that is a good thing.&lt;/p&gt;

&lt;p&gt;The server can offer three kinds of thing, and it is worth knowing them by name:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt;: actions the model can run, like "read this file", "run this query", or "create this GitHub issue".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources&lt;/strong&gt;: data the model can read for context, like a file's contents or a database schema. Read-only, no side effects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts&lt;/strong&gt;: ready-made templates for common tasks, so the model does not start from scratch every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flow itself is simple. The client connects, then asks the server to list what it has (a &lt;code&gt;tools/list&lt;/code&gt; call), then runs one of them (a &lt;code&gt;tools/call&lt;/code&gt;). In JSON it looks about 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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What can you do?"&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;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tools/list"&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Okay, do this one."&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;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.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;"id"&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;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tools/call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&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;"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;"arguments"&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;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kolkata"&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;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjmu8tsl3mdvf13g2aavo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjmu8tsl3mdvf13g2aavo.png" alt="A client inside an application exchanges a capability menu, tool call, and result with a server." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One more thing worth knowing early: a server runs in one of two places. A local server talks over &lt;code&gt;stdio&lt;/code&gt; (standard input and output), which is just two programs on the same machine passing text back and forth, no network involved. A remote server talks over HTTP, so it can live anywhere and serve lots of clients at once. Same protocol either way, which is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Server vs API: What's the Difference
&lt;/h2&gt;

&lt;p&gt;This is the question I get most, so let me be direct. An MCP server usually sits on top of a normal API. It is not a replacement for one. The difference is who the thing is built for.&lt;/p&gt;

&lt;p&gt;A regular API is built for a developer who already read the docs and hardcoded the exact calls. It assumes someone knows the endpoints ahead of time. An MCP server is built for a model that shows up knowing nothing, asks what is available, and figures out which tool to use on its own. That runtime discovery is the whole trick, and a plain API does not do it.&lt;/p&gt;

&lt;p&gt;So when people ask about MCP vs API, the honest answer is that most MCP servers are a thin wrapper around an API you could have called yourself. What MCP adds is a standard way to describe the tools, so any model can find them and use them without a human wiring each call. The API does the work. The MCP server makes that work legible to an AI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff4z8fcn2qfyng9uhr9bg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff4z8fcn2qfyng9uhr9bg.png" alt="An API executes fixed calls; an MCP wrapper makes those same capabilities discoverable to an AI." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Can You Actually Do With One
&lt;/h2&gt;

&lt;p&gt;Plenty, and you probably already have the pieces. The most common MCP servers connect a model to everyday systems: a filesystem server for reading and writing files, a GitHub server for issues and pull requests, a Postgres server for querying a database, a Slack server for messages. Anthropic keeps a set of &lt;a href="https://github.com/modelcontextprotocol/servers" rel="noopener noreferrer"&gt;reference servers&lt;/a&gt; on GitHub, and most companies now ship an official one for their own product.&lt;/p&gt;

&lt;p&gt;I run Claude Code every day, and that is where most people meet their first MCP server. You bolt one onto a coding agent or a desktop chat app, and suddenly it can touch your files, search the web, or open a pull request without you copy-pasting anything. The MCP tools it offers just show up as things the model can now do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk5edu92gtx19brfig7so.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk5edu92gtx19brfig7so.png" alt="Four practical capabilities: edit files, work with repositories, query data, and send messages." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pattern is always the same. Find a server for the system you care about, point your client at it, and the model gains a new skill. One server is genuinely a five-minute setup, and it feels a little bit like magic the first time an agent edits a real file on your disk!&lt;/p&gt;

&lt;h2&gt;
  
  
  Do You Need an MCP Gateway
&lt;/h2&gt;

&lt;p&gt;Here is the honest part first: if you are running one or two MCP servers, you do not need a gateway. It solves a problem you do not have yet, so skip this section and go play with a server instead.&lt;/p&gt;

&lt;p&gt;But things change once the count goes up. Say you have got ten servers wired into your agents. Three problems show up fast. First, every request dumps the full tool list of every server into the model's context, so with 150-plus tools the model burns most of its budget just reading the menu. Second, each server has its own keys and auth, scattered across config files with no single place to rotate them. And third, there is no shared way to say who is allowed to call what, or to cap spending. That is tool sprawl, and it turns a neat setup into a liability.&lt;/p&gt;

&lt;p&gt;An MCP gateway fixes this by sitting right in the middle. Think of it like a power strip. Instead of plugging ten different cables into ten wall sockets, your AI agent connects to the gateway once. The gateway handles the rest.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl4107emyz3timcqn3ve.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl4107emyz3timcqn3ve.png" alt="Growing server connections share one controlled gateway with centralized credentials, permissions, and budgets." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is where &lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt;, an &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;open-source AI gateway&lt;/a&gt; built by Maxim AI, comes in. It can &lt;a href="https://docs.getbifrost.ai/mcp/gateway" rel="noopener noreferrer"&gt;act as an MCP gateway&lt;/a&gt;, tying all your servers together so your AI talks to one neat endpoint instead of juggling ten.&lt;/p&gt;

&lt;p&gt;It also fixes that token problem in a really clever way. Remember how 100 tools eat up your context? Normally, the model has to re-read every single tool's description on every prompt just to know what's on the menu. That burns tokens fast.&lt;/p&gt;

&lt;p&gt;Bifrost fixes this using &lt;a href="https://docs.getbifrost.ai/mcp/code-mode" rel="noopener noreferrer"&gt;Code Mode&lt;/a&gt;. Instead of dumping every tool definition into the prompt, it gives the model four generic tools and lets it write code to find and run what it needs on demand. In their benchmarks, that cut input tokens by up to 92.8%. That is not a small difference, that is basically your whole bill! Very similar to the cut-costs-at-the-gateway idea I talked about with &lt;a href="https://www.swapnoneel.site/blog/what-is-semantic-caching" rel="noopener noreferrer"&gt;semantic caching&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;On top of that, you get one central place for safety and control. All your API keys live in one spot instead of being scattered across random config files. You can set &lt;a href="https://docs.getbifrost.ai/features/governance/virtual-keys" rel="noopener noreferrer"&gt;per-key tool filtering and budgets&lt;/a&gt;, meaning you decide exactly which agent can use which tools and how much it can spend. And by default, tools do not run automatically unless you explicitly turn that on. That last part matters a lot, because an agent running tools silently without your permission is a bad day waiting to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Start
&lt;/h2&gt;

&lt;p&gt;You do not need to read a massive spec or write complex code to get started with MCP. The whole idea is small once the acronym stops scaring you: it is just a standard plug that lets your AI reach outside tools.&lt;/p&gt;

&lt;p&gt;If you want to try it out today, do not overthink it. Pick an app you already use, like Claude Desktop, Cursor, or Claude Code. Then grab just one pre-built server, like the filesystem or GitHub server. Wiring it into your config file takes about five minutes.&lt;/p&gt;

&lt;p&gt;Once it is running, give the model a real prompt: ask it to inspect a local folder or check a repo. The moment you see an agent reach outside its chat box and touch a real file on your machine, the whole protocol clicks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfgkcti6rc3vx7ojy4pz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfgkcti6rc3vx7ojy4pz.png" alt="Choose an app, connect one filesystem server, and complete one real task before expanding." width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And my advice would be to keep it tiny. You do not need a gateway or fancy routing when you are only running one or two servers. Play with one first, see how it feels, and only put a gateway like Bifrost in front when you have a dozen servers and your token bill starts climbing.&lt;/p&gt;

&lt;p&gt;If you set up your first MCP server after reading this, I would genuinely like to hear how it went. Drop it in the comments, or come say hi on &lt;a href="https://x.com/swapnoneel123" rel="noopener noreferrer"&gt;X&lt;/a&gt; and we can chat about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions (FAQs)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is an MCP server in simple terms?&lt;/strong&gt; It is a small program that gives an AI model a set of tools and data through a shared standard, so the model can read a file, query a database, or call an API without custom glue code. Think of it as a standard adapter between one AI and one outside system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between an MCP client and an MCP server?&lt;/strong&gt; They are the two ends of one connection. The server offers tools, data, and prompts. The client, created by an app like Claude Desktop, connects to the server and uses what it offers. One app can run many clients, one per server it talks to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkpair6erwwqme1azp965.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkpair6erwwqme1azp965.png" alt="A compact field guide clarifies the adapter, client/server relationship, permissions, and building a small server." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are MCP servers safe to use?&lt;/strong&gt; They are as safe as how you run them. A single local server you trust is low risk. The danger shows up when you run many unvetted servers with their own credentials and let an agent run tools automatically, which is exactly why keeping auto-execution off and routing servers through a gateway with tool filtering is worth doing once you scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you build your own MCP server?&lt;/strong&gt; You use one of the official SDKs, define a few tools with their inputs, and expose them over &lt;code&gt;stdio&lt;/code&gt; or HTTP. Start tiny, with one or two tools, get it working with a client you already use, and grow from there instead of trying to wrap an entire API on day one.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Fixing Claude Rate Limit Errors with an AI Gateway</title>
      <dc:creator>Swapnoneel Saha</dc:creator>
      <pubDate>Wed, 26 Aug 2026 18:04:49 +0000</pubDate>
      <link>https://dev.to/swapnoneel123/fixing-claude-rate-limit-errors-with-an-ai-gateway-5dk5</link>
      <guid>https://dev.to/swapnoneel123/fixing-claude-rate-limit-errors-with-an-ai-gateway-5dk5</guid>
      <description>&lt;p&gt;Ever had a &lt;code&gt;429&lt;/code&gt; from Claude that just would not clear, no matter how patiently you backed off?&lt;/p&gt;

&lt;p&gt;Yeah. That one was never a rate limit.&lt;/p&gt;

&lt;p&gt;A Claude rate limit error is an HTTP &lt;code&gt;429&lt;/code&gt; carrying &lt;code&gt;"type": "rate_limit_error"&lt;/code&gt;, and it means your organization crossed a per-minute request ceiling, a per-minute token ceiling, or its monthly spend cap. Which one decides the fix, and the response body tells you which before you write any retry code.&lt;/p&gt;

&lt;p&gt;And the difference matters far more than it sounds, because two of those three conditions get strictly worse the moment you retry them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five failures behind one Claude rate limit error
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rate_limit_error&lt;/code&gt; is not one condition. Anthropic returns the same string for several different situations, and the correct response may be to wait a few seconds or to change a setting that retrying cannot fix.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe5joinddh3t7dxjgioei.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe5joinddh3t7dxjgioei.png" alt="Diagnostic flow for five Claude API failure types" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://platform.claude.com/docs/en/api/errors" rel="noopener noreferrer"&gt;Claude API errors reference&lt;/a&gt; documents them separately, so you can tell them apart from the response alone.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What it actually is&lt;/th&gt;
&lt;th&gt;Does retrying help?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;429&lt;/code&gt; + &lt;code&gt;rate_limit_error&lt;/code&gt; + a &lt;code&gt;retry-after&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;A real per-minute limit. You went too fast.&lt;/td&gt;
&lt;td&gt;Yes, after &lt;code&gt;retry-after&lt;/code&gt; seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;429&lt;/code&gt; + &lt;code&gt;rate_limit_error&lt;/code&gt; + &lt;code&gt;details.error_code: enforced_spend_limit_reached&lt;/code&gt;, and no &lt;code&gt;retry-after&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Your usage tier's monthly spend cap&lt;/td&gt;
&lt;td&gt;No. Access returns at 00:00 UTC on the 1st&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;400&lt;/code&gt; + &lt;code&gt;invalid_request_error&lt;/code&gt;, message starting &lt;code&gt;You have reached your specified API usage limits&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A spend limit you set yourself in the Console&lt;/td&gt;
&lt;td&gt;No. Raise or remove your own limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;429&lt;/code&gt; right after a sharp traffic ramp&lt;/td&gt;
&lt;td&gt;An acceleration limit, not your steady-state limit&lt;/td&gt;
&lt;td&gt;Partly. Ramp up gradually instead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;529&lt;/code&gt; + &lt;code&gt;overloaded_error&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Anthropic is busy. Nothing to do with you.&lt;/td&gt;
&lt;td&gt;Yes, but the queue is global&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the one that trips people up most. The Claude overloaded error gets searched more than any specific 429 phrasing, and almost every guide lumps it in with rate limits. It isn't yours. A &lt;code&gt;529&lt;/code&gt; means the API is temporarily overloaded across all users, so no amount of tier upgrading or key rotation on your side moves it.&lt;/p&gt;

&lt;p&gt;The second row is easy to misdiagnose. It looks identical to a normal rate limit at the status-code level, the official SDKs will happily auto-retry it, and every retry fails. The tell is the missing &lt;code&gt;retry-after&lt;/code&gt; header plus that &lt;code&gt;error_code&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So before anything else, log the full error body. Not &lt;code&gt;err.status&lt;/code&gt;. The body.&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="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;msg&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-sonnet-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;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hi&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;except&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;APIStatusError&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                          &lt;span class="c1"&gt;# 429
&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;e&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;headers&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;retry-after&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# None means spend cap
&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;e&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="c1"&gt;# the error_code lives here
&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                           &lt;span class="c1"&gt;# req_018Ee... for support
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;retry-after&lt;/code&gt; check on that second line is doing almost all of the diagnostic work. Present means wait. Absent on a &lt;code&gt;429&lt;/code&gt; means stop retrying and go look at billing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually counts against your Claude rate limits
&lt;/h2&gt;

&lt;p&gt;Now, the per-minute limits. There are three of them running at once, per model, and you can trip any one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3tnu0xx83flpiei2xx6f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3tnu0xx83flpiei2xx6f.png" alt="Claude rate limit token buckets and prompt caching" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RPM&lt;/strong&gt;, requests per minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ITPM&lt;/strong&gt;, input tokens per minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OTPM&lt;/strong&gt;, output tokens per minute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are enforced per model class, so Sonnet traffic and Haiku traffic draw from separate buckets and can run at full speed at the same time. Worth knowing: on the current lineup, Sonnet 5 has its own bucket while Sonnet 4.6 and 4.5 share a combined one, and the same split applies to Opus 5 versus the Opus 4.x family. Pinning half your traffic to an older Sonnet does not double your Sonnet 5 headroom.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://platform.claude.com/docs/en/api/rate-limits" rel="noopener noreferrer"&gt;rate limits documentation&lt;/a&gt; also says something that should change how you think about bursts. The limiter is a token bucket, which means capacity refills continuously instead of resetting on a clean minute boundary. A 60 RPM limit behaves much more like 1 request per second than like 60 free requests at the top of every minute. So if you fan out 60 concurrent calls at 12:00:00 you will get rate limited, even though your average for that minute was exactly at the ceiling.&lt;/p&gt;

&lt;p&gt;I hit this exact shape of problem building &lt;a href="https://scholarian.vercel.app" rel="noopener noreferrer"&gt;Scholarian&lt;/a&gt;, which fetched and ranked over 10,000 academic papers. The moment you write a loop that fans out one model call per document, your average throughput looks perfectly fine on a dashboard and your p99 latency (the slowest 1% of calls) is a wall of 429s, because nothing in that loop is pacing itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one number most people get wrong
&lt;/h3&gt;

&lt;p&gt;For most Claude models, only uncached input tokens count against ITPM.&lt;/p&gt;

&lt;p&gt;Specifically: &lt;code&gt;input_tokens&lt;/code&gt; and &lt;code&gt;cache_creation_input_tokens&lt;/code&gt; count, and &lt;code&gt;cache_read_input_tokens&lt;/code&gt; does not. So a cache hit is free as far as your rate limit is concerned, and it is billed at a reduced rate on top of that. (Claude Haiku 3.5 is the exception and does count cache reads.)&lt;/p&gt;

&lt;p&gt;Do the arithmetic on that, because it is not a small effect. Say you are on the Start tier with 2,000,000 ITPM on Sonnet 5, and you are running a coding agent that ships roughly 180,000 tokens of context per turn.&lt;/p&gt;

&lt;p&gt;Without caching, 2,000,000 divided by 180,000 is about &lt;strong&gt;11 turns per minute&lt;/strong&gt; for your entire organization.&lt;/p&gt;

&lt;p&gt;With prompt caching where 170,000 of those tokens are cache reads, only 10,000 counts against ITPM. That is &lt;strong&gt;200 turns per minute&lt;/strong&gt; off the exact same limit! Anthropic's own worked example frames it the same way, and it is why they publish your cache hit rate on the Usage page right next to the rate limit charts.&lt;/p&gt;

&lt;p&gt;One nuance people miss: &lt;code&gt;cache_creation_input_tokens&lt;/code&gt; does count. Writing the cache costs you full ITPM once, and reading it is free. So a workload that keeps rebuilding the cache because the prefix keeps shifting gets the worst of both.&lt;/p&gt;

&lt;p&gt;Output is simpler. OTPM is measured on tokens actually produced, in real time, and &lt;code&gt;max_tokens&lt;/code&gt; never factors into it. There is genuinely no rate limit penalty for setting a generous &lt;code&gt;max_tokens&lt;/code&gt;, so stop shaving it down for that reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headers you should be reading instead of guessing
&lt;/h2&gt;

&lt;p&gt;Every response, including the successful ones, carries your current standing. And most people never look at them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffri34c0gmiht98fr2wsa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffri34c0gmiht98fr2wsa.png" alt="Monitoring Claude rate-limit response headers" width="800" height="450"&gt;&lt;/a&gt;&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;-sS&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null https://api.anthropic.com/v1/messages &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$ANTHROPIC_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;"anthropic-version: 2023-06-01"&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":"claude-sonnet-5","max_tokens":16,
       "messages":[{"role":"user","content":"hi"}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that and you get back a block of &lt;code&gt;anthropic-ratelimit-*&lt;/code&gt; headers. The ones worth wiring into a metric:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;anthropic-ratelimit-requests-remaining&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;anthropic-ratelimit-input-tokens-remaining&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;anthropic-ratelimit-output-tokens-remaining&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;the matching &lt;code&gt;-reset&lt;/code&gt; fields, which are RFC 3339 timestamps rather than durations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two gotchas here. The &lt;code&gt;remaining&lt;/code&gt; values are rounded to the nearest thousand, so treat them as a gauge and not as an accountant. And the generic &lt;code&gt;anthropic-ratelimit-tokens-*&lt;/code&gt; triplet reports whichever limit is currently most restrictive, which means the number can jump between input and output accounting between requests without anything being wrong.&lt;/p&gt;

&lt;p&gt;Identify the failure and read the remaining-capacity headers before changing retry logic. Backoff cannot fix a spend cap, and tuning it first wastes time.&lt;/p&gt;

&lt;p&gt;At one of my previous roles I built an internal tool that pulled the data logs off an AI product and turned them into latency and slowdown reports, and the useful part was never the clever part. It was just having the numbers somewhere you could look at them without reproducing the bug first. Same idea here. Export those three headers as gauges and most rate limit debugging stops being detective work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrying correctly, and where retrying stops helping
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgcolh0nckwd3kwh0ef46.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgcolh0nckwd3kwh0ef46.png" alt="Retry backoff with jitter and hard ceilings" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rule in plain English: if the response gave you a &lt;code&gt;retry-after&lt;/code&gt;, wait exactly that long. If it did not, double your wait each attempt and add randomness so your workers do not all wake up at the same instant.&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;random&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="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;max_retries&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="c1"&gt;# we handle it ourselves
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&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;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;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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;APIStatusError&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;e&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="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="mi"&gt;529&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;hinted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&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;headers&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;retry-after&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;e&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;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;hinted&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;  &lt;span class="c1"&gt;# spend cap, so retrying is pointless
&lt;/span&gt;            &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hinted&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;hinted&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;min&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="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&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;wait&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="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exhausted retries&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;The jitter multiplier on the last line is the bit people skip, and it is the bit that matters at concurrency. Without it, 50 workers that all got rate limited at the same moment will all retry at the same moment, and you get a thundering herd that reproduces the original 429 on a fixed schedule forever.&lt;/p&gt;

&lt;p&gt;Also notice &lt;code&gt;max_retries=0&lt;/code&gt; at the top. The official SDKs retry twice by default with their own backoff, honoring &lt;code&gt;retry-after&lt;/code&gt;. That default is fine for a script. It is not fine when you are also running your own retry loop, because the two layers multiply and you quietly end up with 3x the attempts you thought you configured.&lt;/p&gt;

&lt;p&gt;But here is the honest limit of all of this, and it is why the post does not end here.&lt;/p&gt;

&lt;p&gt;Retries are a queueing strategy. They smooth out bursts against a ceiling you are near. They do nothing at all when you are structurally above the ceiling, because every retry is just another request competing for the same bucket. If your steady-state demand is 3,000,000 input tokens per minute and your limit is 2,000,000, no backoff curve in the world fixes that. You need more ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup is done, now the part that actually raises the ceiling
&lt;/h2&gt;

&lt;p&gt;Everything above happens inside one API key against one endpoint. Reading the error properly and then caching aggressively will genuinely resolve most Claude rate limit errors, and if you are a solo developer on one key, honestly stop reading here and go turn on prompt caching. You will get more out of that one afternoon than out of any infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzzk1yyuceuqbn0e324l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzzk1yyuceuqbn0e324l.png" alt="Four independent sources of Claude API capacity" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But if you are past that point, the interesting fact is that "Claude capacity" is not one pool. It is four, and they are metered by different organizations running different quota systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four separate places Claude capacity lives
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. The direct Claude API, by usage tier.&lt;/strong&gt; Start, Build, Scale, then Custom. Each tier carries both per-minute limits and a monthly spend cap, and as of mid-2026 those caps sit at 500USD on Start, 1,000USD on Build, and 200,000USD on Scale. Tiers move up automatically as you build usage history, and you can also request an increase from the Console.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Amazon Bedrock.&lt;/strong&gt; Claude on Bedrock runs against &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/quotas.html" rel="noopener noreferrer"&gt;AWS service quotas&lt;/a&gt;, which are per-account and per-region and adjustable through the Service Quotas console. These have no relationship to your Anthropic tier at all. Bedrock even splits its own quotas across two inference endpoints, tracked separately for the same underlying model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Google's platform.&lt;/strong&gt; Claude models run there through the model garden against Google Cloud quotas, again on a completely separate meter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Priority Tier.&lt;/strong&gt; Anthropic sells committed capacity with its own &lt;code&gt;anthropic-priority-*&lt;/code&gt; headers and its own bucket, sitting alongside your standard limits rather than replacing them.&lt;/p&gt;

&lt;p&gt;The consequence is the interesting part. An organization pinned at 100% of its direct-API ITPM might have an entirely idle Bedrock quota sitting in two regions. The capacity exists, and it is already paid for. The problem is that your application has one base URL and one key, so it cannot reach any of it.&lt;/p&gt;

&lt;p&gt;One clarification before moving on, because this is the most common mix-up of all. If your error reads &lt;code&gt;API Error: rate limit reached&lt;/code&gt; inside Claude Code on a Pro or Max plan, none of this section applies to you, because that is a subscription meter and not the API. Two different systems, two different clocks, and I wrote about &lt;a href="https://www.swapnoneel.site/blog/claude-pro-usage-limits" rel="noopener noreferrer"&gt;Claude Pro usage limits&lt;/a&gt; separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a gateway changes the math
&lt;/h2&gt;

&lt;p&gt;This is the job an &lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;AI gateway&lt;/a&gt; does, which you will also see called an LLM gateway or an LLM proxy. It is a proxy that speaks every provider's API shape, sits between your code and the model, and holds the routing decisions your application shouldn't have to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjgxccsp0dbkrtjsmsbur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjgxccsp0dbkrtjsmsbur.png" alt="AI gateway routing across keys and providers" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me lead with what it cannot do, because that part gets oversold constantly. A gateway does not raise Anthropic's ceiling. Your Start tier is still your Start tier. If you run one Anthropic key through a gateway and change nothing else, you will hit exactly the same 429 at exactly the same token count, plus a few microseconds of hop.&lt;/p&gt;

&lt;p&gt;What it changes is how many buckets a single request can reach, and how fast it gives up on a bad one. &lt;a href="https://docs.getbifrost.ai/overview" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; is Maxim AI's high-performance, &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;open-source AI gateway&lt;/a&gt; that unifies access to 20+ providers through a single OpenAI-compatible API. Its &lt;a href="https://docs.getbifrost.ai/features/retries-and-fallbacks" rel="noopener noreferrer"&gt;retry and fallback layer&lt;/a&gt; distinguishes upstream failures from credential failures: it retries &lt;code&gt;5xx&lt;/code&gt; and network errors against the same key, rotates keys for &lt;code&gt;429&lt;/code&gt;, &lt;code&gt;401&lt;/code&gt;, &lt;code&gt;402&lt;/code&gt;, and &lt;code&gt;403&lt;/code&gt; responses, and moves to the next provider only after the current provider's retry budget is exhausted. That behavior maps directly to the different Claude failure modes in this post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pooling keys so one bucket is not the whole story
&lt;/h3&gt;

&lt;p&gt;Rate limits are set at the organization level, so a second organization with its own key is a second bucket. A gateway can treat several keys as one logical pool.&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;"providers"&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;"anthropic"&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;"keys"&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;"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;"primary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"env.ANTHROPIC_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"models"&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;"*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight"&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.7&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;"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;"overflow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"env.ANTHROPIC_API_KEY_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;"models"&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;"*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight"&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.3&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;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;Traffic splits 70/30 by weight. But the part that matters for 429s is what happens on failure. Bifrost rotates keys when the failure is bound to the credential rather than to the request, which it defines as &lt;code&gt;429&lt;/code&gt;, &lt;code&gt;401&lt;/code&gt;, &lt;code&gt;403&lt;/code&gt; and &lt;code&gt;402&lt;/code&gt;. A rate limited key gets marked used for that cycle and the request carries on against another key. Auth and billing failures mark the key dead for the remainder of that request. Once every key has been tried, the set resets and a fresh weighted round begins.&lt;/p&gt;

&lt;p&gt;Key selection is scored rather than round robin, using recent error rates, latency, and observed rate limit hits, with weights recomputed on a short interval and penalties decaying once a key recovers. That is &lt;a href="https://www.swapnoneel.site/blog/what-is-adaptive-load-balancing" rel="noopener noreferrer"&gt;adaptive load balancing&lt;/a&gt; applied to credentials instead of servers, and it matters here because a key that just returned a &lt;code&gt;429&lt;/code&gt; is a bad destination for the next 30 seconds specifically, and not permanently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Falling back across channels, not just keys
&lt;/h3&gt;

&lt;p&gt;Key pooling only helps if you have more keys. Falling back across the four capacity pools helps even when you don't.&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;"anthropic/claude-sonnet-5"&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="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;"..."&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;"fallbacks"&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="s2"&gt;"bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"vertex/claude-sonnet-4-5@20250929"&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;Same request, same Anthropic model family, three completely independent quota systems. If the direct API is capped, the call lands on Bedrock's per-region quota, which knows nothing about your Anthropic tier.&lt;/p&gt;

&lt;p&gt;The execution detail worth knowing is that each provider in the chain gets its own full retry budget. A primary at &lt;code&gt;max_retries: 3&lt;/code&gt; with two fallbacks also at 3 means up to 12 attempts on one call, so set your client timeout with that in mind. &lt;a href="https://docs.getbifrost.ai/features/retries-and-fallbacks" rel="noopener noreferrer"&gt;Retries and fallbacks&lt;/a&gt; documents the backoff as &lt;code&gt;min(initial × 2^attempt, max) × jitter(0.8-1.2)&lt;/code&gt;, with defaults of 500ms initial and a 5000ms cap.&lt;/p&gt;

&lt;p&gt;And a real gotcha, stated plainly because it will bite you: &lt;code&gt;max_retries&lt;/code&gt; defaults to 0. Retries are off until you turn them on.&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;"network_config"&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;"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;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"retry_backoff_initial"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"retry_backoff_max"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&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;h3&gt;
  
  
  Two more things that buy headroom
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Caching at the gateway instead of per service.&lt;/strong&gt; If four services ask the same question, an application-level cache inside each one misses four times. A gateway cache hits three of them, and every hit is a request that never touches your ITPM at all. Bifrost's version only engages when a request carries an &lt;code&gt;x-bf-cache-key&lt;/code&gt; header, defaults to a 5 minute time-to-live and a 0.8 cosine similarity threshold, and can run in exact-match mode with no embedding provider at all. That threshold is the whole product, and I went into why in a post on &lt;a href="https://www.swapnoneel.site/blog/what-is-semantic-caching" rel="noopener noreferrer"&gt;semantic caching&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stopping one tenant from eating the org bucket.&lt;/strong&gt; Your organization limit is shared, so one runaway batch job starves the interactive traffic sitting behind it. Governance limits let you cap consumption per virtual key before the request ever reaches Anthropic, in tokens or in requests, with calendar-aligned reset periods running from &lt;code&gt;1m&lt;/code&gt; up to &lt;code&gt;1Y&lt;/code&gt;. When a cap trips, the gateway reports the breach with its own code instead of a vague 429: &lt;code&gt;token_limited&lt;/code&gt; or &lt;code&gt;request_limited&lt;/code&gt; on a &lt;code&gt;429&lt;/code&gt;, &lt;code&gt;budget_exceeded&lt;/code&gt; on a &lt;code&gt;402&lt;/code&gt;, and &lt;code&gt;403&lt;/code&gt; for a blocked model or provider. The &lt;a href="https://docs.getbifrost.ai/features/governance" rel="noopener noreferrer"&gt;governance docs&lt;/a&gt; carry the full mapping.&lt;/p&gt;

&lt;p&gt;That is a real difference in debuggability. "Your batch job hit its token cap" is a fix you can act on in a minute. "Anthropic said 429" is an afternoon.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pointing Claude Code at it
&lt;/h3&gt;

&lt;p&gt;Since a lot of people arriving at this error are inside a coding agent rather than a Python service, the integration is two environment variables. Bifrost exposes an Anthropic-compatible endpoint, so the client never knows anything changed.&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;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8080/anthropic
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dummy-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The path is &lt;code&gt;/anthropic&lt;/code&gt;, and not &lt;code&gt;/v1/anthropic&lt;/code&gt;. And the key can be a placeholder because the real credentials live inside the gateway.&lt;/p&gt;

&lt;p&gt;Gateway traffic is billed per token against API keys. It uses a separate meter from Pro or Max subscriptions, not a larger subscription allowance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a gateway will not fix
&lt;/h2&gt;

&lt;p&gt;Four things, because a post that only lists the wins is a brochure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fauxl6crqc7hdkz3hl8ik.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fauxl6crqc7hdkz3hl8ik.png" alt="Limits an AI gateway cannot solve" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It does not help with a &lt;code&gt;529&lt;/code&gt;. An overloaded error is Anthropic-wide capacity, so a fallback to Bedrock might dodge it, but no amount of key pooling on the direct API will.&lt;/p&gt;

&lt;p&gt;It does not make a cross-provider fallback behave identically. Sonnet 5 on the direct API and Sonnet 4.5 on Bedrock are not the same model, and if your prompts are tuned tightly or you lean on a specific tool-calling shape, your fallback path needs its own evals. A fallback that silently produces worse answers is arguably worse than a clean 429.&lt;/p&gt;

&lt;p&gt;It does not fix a workload that is simply too large. If you need 5x your current ceiling permanently, the answer is a tier increase or committed capacity, and routing is the bridge that gets you there without downtime in the meantime.&lt;/p&gt;

&lt;p&gt;And the vendor benchmarks are vendor benchmarks. Bifrost's page claims 20 microseconds of added latency at 5,000 requests per second, and a 50x advantage on that same p99 number over LiteLLM. Those are Maxim's own numbers on Maxim's own harness, and you should treat them the way you treat every published benchmark, which is as a reason to go run your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you should actually do, in order
&lt;/h2&gt;

&lt;p&gt;Ordered by what each one costs you, cheapest first. Stop at the first one that fixes it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fap1us1416it1ujunxxrb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fap1us1416it1ujunxxrb.png" alt="Ordered checklist for resolving Claude rate limits" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One.&lt;/strong&gt; Log the full error body and check for &lt;code&gt;retry-after&lt;/code&gt;. If it is missing on a &lt;code&gt;429&lt;/code&gt;, you have a spend cap and no retry strategy will ever help you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two.&lt;/strong&gt; Export the three &lt;code&gt;anthropic-ratelimit-*-remaining&lt;/code&gt; headers as gauges. You cannot tune what you cannot see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three.&lt;/strong&gt; Turn on prompt caching and check your hit rate on the Usage page. This is the single biggest win available and it costs you an afternoon, because cached reads do not count against ITPM on most models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four.&lt;/strong&gt; Add &lt;code&gt;retry-after&lt;/code&gt;-aware backoff with jitter, and make sure you are not stacking it on top of the SDK's own retries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five.&lt;/strong&gt; Move anything non-interactive to the Message Batches API, which has its own separate rate limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Six.&lt;/strong&gt; Request a tier increase. It is free and it is slow, so start it before you need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seven.&lt;/strong&gt; Only now, put a gateway in front and pool keys or channels. This is real infrastructure with real operational cost, and it is worth it once you have genuinely run out of ceiling, and mostly not before.&lt;/p&gt;

&lt;p&gt;The reason I would put it dead last rather than first is that most 429s I have run into were not capacity problems at all. They were a cache that never got turned on, or a retry loop with no jitter, or a spend cap that everybody kept retrying into. Fix those and the ceiling stops being the constraint.&lt;/p&gt;

&lt;p&gt;If you have hit a &lt;code&gt;429&lt;/code&gt; shape that doesn't fit any of the five rows in that table, drop it in the comments, I'd genuinely like to see it. And if you want more of this sort of thing, I'm on &lt;a href="https://x.com/swapnoneel123" rel="noopener noreferrer"&gt;X&lt;/a&gt; and I write everything up first on &lt;a href="https://www.swapnoneel.site" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>Claude Pro Usage Limits Explained and How to Work Around Them</title>
      <dc:creator>Swapnoneel Saha</dc:creator>
      <pubDate>Tue, 25 Aug 2026 17:05:36 +0000</pubDate>
      <link>https://dev.to/swapnoneel123/claude-pro-usage-limits-explained-and-how-to-work-around-them-2lki</link>
      <guid>https://dev.to/swapnoneel123/claude-pro-usage-limits-explained-and-how-to-work-around-them-2lki</guid>
      <description>&lt;p&gt;In March 2026, Claude Max subscribers started watching a 5-hour window drain in 90 minutes. One person reported going from 21% used to 100% used on a single prompt, and everybody assumed it was a bug.&lt;/p&gt;

&lt;p&gt;It wasn't. Anthropic's Thariq Shihipar &lt;a href="https://x.com/trq212/status/2037254607001559305" rel="noopener noreferrer"&gt;posted on X&lt;/a&gt; and said that they're adjusting the 5 hour session limits during peak hours to manage growing demand, affecting roughly 7% of users.&lt;/p&gt;

&lt;p&gt;Claude Pro meters you on two clocks at once: a rolling 5-hour session window and a weekly cap, shared across claude.ai, Claude Code and the desktop app. Neither one is a message counter. Both are token meters, and what drains them fastest is not how much you ask, it is how much you make Claude re-read.&lt;/p&gt;

&lt;p&gt;Once you read the meter properly, most of the "limits" problem becomes context management, and context is something you control.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Claude Pro's usage limits, exactly?
&lt;/h2&gt;

&lt;p&gt;Two windows, running at the same time, and you can hit either one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1g5lle3op3imamaxoej.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1g5lle3op3imamaxoej.png" alt="Two usage clocks share one allowance" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;5-hour session window&lt;/strong&gt; starts with your first message and rolls. It is not a clock that ticks at the top of the hour, and it is not tied to a calendar day. Send a message at 2pm and that window closes at 7pm, whether you sent one message in it or four hundred.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;weekly window&lt;/strong&gt; sits on top of that and resets at a fixed time assigned to your account. This one is the real ceiling for heavy users, because you can stay inside every 5-hour window all week and still run out on a Thursday.&lt;/p&gt;

&lt;p&gt;Both windows are shared. &lt;a href="(https://support.claude.com/en/articles/11145838-using-claude-code-with-your-pro-or-max-plan)"&gt;Anthropic's docs&lt;/a&gt; are explicit that usage across claude.ai, Claude Code and Claude Desktop counts toward the same pool. So the hour you spent arguing with Claude about your resume in the browser is the same hour you don't get in your terminal later.&lt;/p&gt;

&lt;p&gt;And here is the part that annoys everyone, me included: &lt;strong&gt;Anthropic publishes no token number for any plan.&lt;/strong&gt; Not for Pro, not for Max. Pro is described as more usage than free, and Max is sold as 5 times or 20 times more usage than Pro. Any blog quoting you an exact message count for Pro is guessing.&lt;/p&gt;

&lt;p&gt;The plans themselves, as of August 2026: Pro is 17USD per month billed annually or 20USD per month billed monthly, and Max starts at &lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;100USD per month&lt;/a&gt;. Same model access on both. The only thing you buy with Max is a bigger number on the same two clocks.&lt;/p&gt;

&lt;p&gt;There is one more distinction worth holding onto, because it changes what you do when you get blocked. A message saying you hit your &lt;strong&gt;session limit&lt;/strong&gt; or &lt;strong&gt;weekly limit&lt;/strong&gt; is plan-wide, and switching models with &lt;code&gt;/model&lt;/code&gt; will not save you. A message saying you hit your &lt;strong&gt;Opus limit&lt;/strong&gt; or &lt;strong&gt;Sonnet limit&lt;/strong&gt; is model-specific, and switching to a different model family genuinely does keep you working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your limit drains faster than your typing
&lt;/h2&gt;

&lt;p&gt;Two developers, same Pro plan, same afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer A&lt;/strong&gt; opens a fresh Claude Code session, asks 40 short questions about a small file, and closes it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer B&lt;/strong&gt; opens one session at 9am, works on and off all day across three unrelated tasks without ever clearing, and sends 12 messages total.&lt;/p&gt;

&lt;p&gt;Developer B burns more of the plan. &lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;Anthropic's cost documentation&lt;/a&gt; explains why:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Claude Code sends your full conversation with every request, and each time Claude uses tools it sends another request carrying that batch of tool results.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So a one-line question at 4pm, in a session that has been open since 9am, draws usage for the entire conversation sitting behind it. Every file that got read, every tool result, every previous answer. Twelve messages carrying a whole day of accumulated context beats 40 messages carrying almost nothing, every single time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdtanop2d10i3frgp53l3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdtanop2d10i3frgp53l3.png" alt="Context weight drains the meter" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three more things burn the meter while you are not looking, and none of them are obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache misses.&lt;/strong&gt; Prompt caching is what keeps a long session affordable, because re-read history gets billed at the cached rate instead of the full one. But the cache has a lifetime. On a subscription it is one hour, and your first message after a longer break misses it and reprocesses your entire context at full price. That is the real answer to "why did one question cost me so much".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extended thinking.&lt;/strong&gt; It's on by default, thinking tokens bill as output tokens, and the default budget can run to tens of thousands of tokens per request depending on the model. You are paying for reasoning on tasks that may not need any.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent teams.&lt;/strong&gt; If you have them switched on, they use roughly &lt;strong&gt;7 times&lt;/strong&gt; more tokens than a normal session when teammates run in plan mode, because every teammate carries its own full context window. Five agents is five conversations, and not one.&lt;/p&gt;

&lt;p&gt;I have some personal skin in this part. I have been building &lt;a href="https://anrl-site.vercel.app" rel="noopener noreferrer"&gt;ANRL&lt;/a&gt;, an AI-native representation language with a Rust parser-compiler, specifically because delimiter overhead and context fragmentation waste an absurd share of a context window. It cuts delimiter token overhead by over 40% in my benchmarks. Spending months on that problem is what made me stop reading the limit as a quota and start reading it as a bill for context I chose to carry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did Claude cut me off after one question?
&lt;/h2&gt;

&lt;p&gt;Because that one question was not one question.&lt;/p&gt;

&lt;p&gt;If you resumed a large session after a long break, you paid a cache miss on the whole history. If a scheduled task or a background job fired while the session sat idle, it sent your full context along with it. If you ran &lt;code&gt;/compact&lt;/code&gt;, that operation itself reads the entire conversation it is summarizing, so compacting a huge context is itself a huge request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbbcgmg6d14whjde8tn8j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbbcgmg6d14whjde8tn8j.png" alt="A cache miss reopens the whole history" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tell is always the same: the size of the request has almost nothing to do with the size of what you typed.&lt;/p&gt;

&lt;h2&gt;
  
  
  When do Claude usage limits reset?
&lt;/h2&gt;

&lt;p&gt;The 5-hour window resets 5 hours after your first message in that window, rolling. There is no fixed reset hour, so the practical move is just to notice when you started.&lt;/p&gt;

&lt;p&gt;The weekly window resets at a fixed time tied to your account rather than a global Monday. When you actually get blocked, the error message tells you the reset time, and that is the only authoritative source you have.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F26tngnpf9m9xikwtxdsv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F26tngnpf9m9xikwtxdsv.png" alt="Rolling and weekly resets" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Worth knowing: Claude Code v2.1.234 and later can wait out a limit and resume the interrupted task automatically once the window resets. It lives in &lt;code&gt;/rate-limit-options&lt;/code&gt;, and it beats sitting there refreshing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you see where your usage actually went?
&lt;/h2&gt;

&lt;p&gt;This is the step almost everybody skips, and it is the one that changes behaviour.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;/usage&lt;/code&gt; inside Claude Code. On a paid plan it shows plan usage bars plus a breakdown that is genuinely useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Attribution&lt;/strong&gt;, meaning how much of your recent usage went to skills, subagents, plugins and each individual MCP server, as a percentage of the total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior flags&lt;/strong&gt;, which name the behaviour costing you the most whenever one crosses 10% of recent usage, so "long context" or "cache misses" gets called out by name instead of you having to guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loops&lt;/strong&gt;, the heaviest scheduled tasks that ran recently, with per-run token counts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr0b7au6jepla4qfbryge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr0b7au6jepla4qfbryge.png" alt="A diagnostic view of usage" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;d&lt;/code&gt; or &lt;code&gt;w&lt;/code&gt; to flip between the last 24 hours and the last 7 days.&lt;/p&gt;

&lt;p&gt;Then run &lt;code&gt;/context&lt;/code&gt; to see what is sitting in your context window right now, and &lt;code&gt;/insights&lt;/code&gt; for a report on how you actually work, written out to &lt;code&gt;~/.claude/usage-data/report.html&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One honest caveat. These numbers are computed from local session history on that machine, so usage from your other devices and from claude.ai is not in there. Don't treat the bars as your true remaining balance.&lt;/p&gt;

&lt;p&gt;I built something adjacent to this on a contract a while back, an internal tool that captured an AI product's logs and turned them into reports on latency and probable slowdowns (keeping it vague here, can't say much more than that). The lesson transferred completely: teams argue about model quality for weeks and never once look at the token accounting, which usually takes an afternoon and explains most of the pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you actually work around Claude Pro's usage limits?
&lt;/h2&gt;

&lt;p&gt;Let me be blunt about one thing before the list. &lt;strong&gt;Nothing here raises your subscription cap.&lt;/strong&gt; There is no flag and no proxy that makes Anthropic hand you more of a plan you didn't buy, and anybody selling you a "bypass" is selling you something that doesn't exist.&lt;/p&gt;

&lt;p&gt;What you can do is make each token buy more work, and give yourself somewhere to go when you get blocked. Both are real, and together they are worth more than a plan upgrade.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd87fulu71hdm9g11jpnf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd87fulu71hdm9g11jpnf.png" alt="Ways to stretch one plan" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Clear between unrelated tasks
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;/clear&lt;/code&gt; does more work than anything else on this list, because it attacks the long-context problem directly. Switching from a bug fix to writing docs? Clear. Use &lt;code&gt;/rename&lt;/code&gt; first so you can &lt;code&gt;/resume&lt;/code&gt; that session later if you need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Match the model to the job
&lt;/h3&gt;

&lt;p&gt;Sonnet handles most coding work and costs meaningfully less than Opus. Keep Opus for architecture calls and multi-step reasoning, and switch with &lt;code&gt;/model&lt;/code&gt; mid-session rather than committing at the start. For simple subagents, set &lt;code&gt;model: haiku&lt;/code&gt; in the subagent config.&lt;/p&gt;

&lt;p&gt;Leaving Opus as your default all day is the most common way people burn a weekly cap without noticing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Turn the thinking budget down when you don't need it
&lt;/h3&gt;

&lt;p&gt;Lower the effort level with &lt;code&gt;/effort&lt;/code&gt;, disable thinking in &lt;code&gt;/config&lt;/code&gt; for simple work, or cap it with an environment variable:&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;MAX_THINKING_TOKENS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line says: give me at most 8,000 tokens of reasoning per request. On adaptive-reasoning models the budget is ignored, so use effort levels there instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Shrink what loads at session start
&lt;/h3&gt;

&lt;p&gt;Your &lt;code&gt;CLAUDE.md&lt;/code&gt; is loaded into context at the start of every single session, so a 600-line file is a tax on unrelated work all week. Anthropic's own guidance is to keep it under 200 lines and move specialised instructions into skills, which load only when invoked.&lt;/p&gt;

&lt;p&gt;Same idea for MCP servers. Run &lt;code&gt;/mcp&lt;/code&gt; and switch off the ones you aren't using, and prefer CLI tools like &lt;code&gt;gh&lt;/code&gt; or &lt;code&gt;aws&lt;/code&gt; where they exist, since those add nothing to context until Claude actually runs them.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Push verbose work out of the main conversation
&lt;/h3&gt;

&lt;p&gt;Two mechanisms, same principle: keep the noisy stuff out of the conversation that gets re-sent every turn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subagents&lt;/strong&gt; for running tests and chewing through logs. The verbose output stays in the subagent's context and only a summary comes back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hooks&lt;/strong&gt; for preprocessing. A &lt;code&gt;PreToolUse&lt;/code&gt; hook that greps a 10,000-line log for &lt;code&gt;ERROR&lt;/code&gt; before Claude ever sees it takes that context from tens of thousands of tokens down to hundreds.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Stop paying for cache misses
&lt;/h3&gt;

&lt;p&gt;The cache lifetime is one hour on a subscription. So the pattern that hurts is: work for an hour, go to a meeting for 90 minutes, come back and drop a one-liner into the same session.&lt;/p&gt;

&lt;p&gt;Either come back inside the hour, or start fresh instead of resuming. On Pro and Max, Claude Code will offer to resume a large session from a summary rather than the full history, and you should take it.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Understand what usage credits really cost
&lt;/h3&gt;

&lt;p&gt;You can switch on usage credits and keep working past your cap, billed at standard API rates. Fine. But there's a trap in the small print that nobody mentions: &lt;strong&gt;once you are drawing on usage credits, the prompt cache lifetime drops from one hour to five minutes!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the moment you go over your cap, your long sessions get dramatically more expensive per message, and not just marginally. You can choose the TTL yourself to keep the one-hour lifetime, and if you use credits at all, you should.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Give yourself a second lane
&lt;/h3&gt;

&lt;p&gt;The seven above make one plan go further. This one is different, because it gives you somewhere to fail over to when the plan is genuinely spent.&lt;/p&gt;

&lt;p&gt;Claude is available from Anthropic directly, from Amazon Bedrock and from Google Vertex AI, and each of those has &lt;strong&gt;its own independent rate limits&lt;/strong&gt;. Your subscription cap has no bearing whatsoever on your Bedrock quota. So "I'm blocked until Tuesday" is really a routing problem wearing a costume, and routing problems have known solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an AI gateway does about this, and what it can't
&lt;/h2&gt;

&lt;p&gt;An AI gateway is a proxy that sits between your tools and the model providers you use. &lt;a href="https://docs.getbifrost.ai/overview" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; is Maxim AI's high-performance, &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;open-source AI gateway&lt;/a&gt; that unifies access to 20+ providers through a single OpenAI-compatible API. For usage limits, the relevant features are ordered &lt;a href="https://docs.getbifrost.ai/features/retries-and-fallbacks" rel="noopener noreferrer"&gt;provider fallbacks&lt;/a&gt; and &lt;a href="https://docs.getbifrost.ai/features/governance/virtual-keys" rel="noopener noreferrer"&gt;virtual keys&lt;/a&gt; with their own budgets and rate limits. Together, they create a separately metered API route for overflow traffic, but they do not increase the Claude Pro subscription allowance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A gateway cannot raise your Claude Pro cap.&lt;/strong&gt; Your subscription is an OAuth relationship between Claude Code and Anthropic, and no proxy in the world changes that arithmetic. Worse, the moment you point Claude Code at a gateway you are authenticating with API keys, which means that traffic bills per token and doesn't touch your subscription at all. Different cost model, and you should walk into it knowingly.&lt;/p&gt;

&lt;p&gt;Which is exactly why it works as an overflow lane. It's a separate meter, and not a bigger one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftrh5me76p8fp2lajoeaa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftrh5me76p8fp2lajoeaa.png" alt="A separate gateway overflow lane" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pointing Claude Code at it is two lines in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;, or &lt;code&gt;%USERPROFILE%\.claude\settings.json&lt;/code&gt; on Windows, which is where mine lives:&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="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ANTHROPIC_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8080/anthropic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ANTHROPIC_AUTH_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-virtual-key"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The path is &lt;code&gt;/anthropic&lt;/code&gt; and not &lt;code&gt;/v1/anthropic&lt;/code&gt;, which is the mistake everyone makes exactly once. From there, &lt;code&gt;/model openai/gpt-5.5&lt;/code&gt; or &lt;code&gt;/model vertex/claude-haiku-4-5&lt;/code&gt; switches providers mid-session, and the harness never knows anything changed.&lt;/p&gt;

&lt;p&gt;The gateway adds four controls that help manage the separate API meter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failover that fires on the error you care about.&lt;/strong&gt; &lt;a href="https://docs.getbifrost.ai/features/retries-and-fallbacks" rel="noopener noreferrer"&gt;Fallback chains&lt;/a&gt; are declared as an ordered list, and every provider in the chain gets its own full retry budget:&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="nl"&gt;"fallbacks"&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="s2"&gt;"anthropic/claude-sonnet-4-6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"bedrock/anthropic.claude-sonnet-4-6-v1: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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detail I like here is how a 429 is handled. Hitting a rate limit does not immediately jump you to the next provider. It rotates to another key &lt;strong&gt;within&lt;/strong&gt; the same provider first, and only falls through once that provider's retries are exhausted. Backoff doubles on each retry until it hits a ceiling, with a bit of randomness thrown in so a thousand clients don't all retry on the same tick. In practice that is &lt;code&gt;min(initial × 2^attempt, max) × jitter&lt;/code&gt;, defaulting to 500ms initial and 5,000ms maximum, so a provider having a bad minute doesn't get hammered on the way out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Budgets that refuse instead of warn.&lt;/strong&gt; &lt;a href="https://docs.getbifrost.ai/features/governance/virtual-keys" rel="noopener noreferrer"&gt;Virtual keys&lt;/a&gt; are scoped credentials carrying their own budgets and rate limits. You give the coding agent one key and the side project another, and when a budget is exhausted the request gets refused with a real status code rather than quietly costing you money. This is the piece Anthropic's own plan structure genuinely does not give an individual.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching, so repeated work stops being paid work.&lt;/strong&gt; A cache hit is a completion you never pay for, in tokens or in seconds. I went through the mechanics, the threshold trap and the realistic hit rates in &lt;a href="https://www.swapnoneel.site/blog/what-is-semantic-caching" rel="noopener noreferrer"&gt;what semantic caching actually is&lt;/a&gt;, including why the 95% figure quoted everywhere is not a hit rate at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Numbers you can act on.&lt;/strong&gt; Every request through the gateway is logged with tokens, cost, latency and provider. Same accounting &lt;code&gt;/usage&lt;/code&gt; gives you, except it survives across machines and covers the tools that aren't Claude Code.&lt;/p&gt;

&lt;p&gt;If you want the routing side in more depth, I wrote up &lt;a href="https://www.swapnoneel.site/blog/what-is-adaptive-load-balancing" rel="noopener noreferrer"&gt;adaptive load balancing&lt;/a&gt; separately. And the overhead question has a real answer: Bifrost adds tens of microseconds per request at 5,000 requests per second, which is nothing next to a model call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Claude Max worth it, or should you just use the API?
&lt;/h2&gt;

&lt;p&gt;Depends on one number, and you can go get it today.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;/usage&lt;/code&gt;, press &lt;code&gt;w&lt;/code&gt;, and look at what share of your weekly cap you actually consumed. If you regularly finish the week under 70%, Max is not your problem and better context habits will get you the rest. Upgrading to fix a habit is an expensive way to avoid typing &lt;code&gt;/clear&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you are hitting the weekly wall by Wednesday every week, then it's a real capacity problem and you have two honest options. Max at 100USD per month buys you 5 times the same plan, predictably, and it's the right answer if your usage is steady. Pay-per-token through a gateway has no ceiling at all, but also no floor, so it's the right answer if your usage is spiky.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgigkcomnchiiieqyorpd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgigkcomnchiiieqyorpd.png" alt="Subscription versus metered API" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For reference on the pay-per-token side, Anthropic's enterprise deployments average around 13USD per developer per active day and stay under 30USD per active day for 90% of users. On steady daily use, that math does not favour the API. On three heavy days a month, it does.&lt;/p&gt;

&lt;p&gt;My own answer, and yours might be different: subscription for the daily driver, gateway for the overflow and for everything that isn't Claude Code.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing about the limits themselves
&lt;/h2&gt;

&lt;p&gt;They move, and this year they have mostly moved in your favour.&lt;/p&gt;

&lt;p&gt;On 6 May 2026, Anthropic &lt;a href="https://www.anthropic.com/news/higher-limits-spacex" rel="noopener noreferrer"&gt;doubled Claude Code's 5-hour rate limits&lt;/a&gt; across Pro, Max, Team and Enterprise, and removed the peak-hours reduction that caused the March mess. That came attached to a compute deal for more than 300 megawatts of new capacity, over 220,000 GPUs.&lt;/p&gt;

&lt;p&gt;Then weekly Claude Code limits went 50% higher as a promotion from 13 May, originally through 19 August 2026, and later extended to 31 August. Anthropic's own wording about making it permanent was careful: they hope to, but capacity may be tight.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5v2zbphau4l0v37lh9lj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5v2zbphau4l0v37lh9lj.png" alt="Capacity grows, but boosts can expire" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So treat any specific limit you read about, including the ones in this post, as a snapshot. The mechanism is stable. The numbers are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually do
&lt;/h2&gt;

&lt;p&gt;Nothing exotic, and it took about a week to settle into.&lt;/p&gt;

&lt;p&gt;Claude Code stays on the Pro subscription, because that is the best price per unit of work I can get anywhere. I &lt;code&gt;/clear&lt;/code&gt; between unrelated tasks, which I resisted for a long time and was wrong about. Sonnet is my default and Opus is a deliberate choice, and not a setting I forgot to change. And when something feels slow or expensive, I open &lt;code&gt;/usage&lt;/code&gt; and read the behavior flags instead of guessing, which is the habit that actually stuck.&lt;/p&gt;

&lt;p&gt;Underneath the harnesses that aren't Claude Code, my traffic goes through the gateway with a fallback rule, so one provider having a bad day doesn't end my afternoon. I also keep a second subscription with a different vendor, which is less a strategy than an admission that a single meter is a single point of failure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwn62ohb4vxkkrudikkw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwn62ohb4vxkkrudikkw.png" alt="A repeatable limit-management workflow" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start with &lt;code&gt;/usage&lt;/code&gt; this week. Find your biggest behavior flag and fix that before you spend a rupee on a bigger plan. If it says long context, you have a &lt;code&gt;/clear&lt;/code&gt; habit to build. If it says cache misses, you have a scheduling problem. &lt;code&gt;/usage&lt;/code&gt; tells you which habit to change first.&lt;/p&gt;

&lt;p&gt;And if you have found a genuinely clever way to stretch a Pro plan that isn't on this list, I want to hear it in the comments. I write more about LLM infrastructure and building with AI over at &lt;a href="https://www.swapnoneel.site" rel="noopener noreferrer"&gt;swapnoneel.site&lt;/a&gt;, and I'm on &lt;a href="https://x.com/swapnoneel123" rel="noopener noreferrer"&gt;X (swapnoneel123)&lt;/a&gt; if you want to argue about any of this.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Top 5 AI Agent Governance Platforms in 2026</title>
      <dc:creator>Swapnoneel Saha</dc:creator>
      <pubDate>Sat, 22 Aug 2026 12:58:20 +0000</pubDate>
      <link>https://dev.to/swapnoneel123/top-5-ai-agent-governance-platforms-in-2026-4i3c</link>
      <guid>https://dev.to/swapnoneel123/top-5-ai-agent-governance-platforms-in-2026-4i3c</guid>
      <description>&lt;p&gt;Most teams shopping for AI agent governance in 2026 end up buying a dashboard, and then find out six months later that their actual problem was an agent holding a database credential with no spending cap on it.&lt;/p&gt;

&lt;p&gt;AI agent governance platforms decide what your autonomous agents are allowed to do and how much they can spend doing it. The category splits into five layers, and no single product covers all five well. The five that matter in 2026 are &lt;a href="https://github.com/maximhq/bifrost/" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt;, Microsoft Agent 365, Zenity, Arthur AI, and Credo AI.&lt;/p&gt;

&lt;p&gt;I have ranked them by the thing most lists skip: how much each one can actually &lt;strong&gt;stop&lt;/strong&gt;, in real time, before a bad action lands. Not how good the report looks afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI agent governance actually means
&lt;/h2&gt;

&lt;p&gt;Let's start from first principles, because "governance" is a word that has been stretched until it means almost nothing.&lt;/p&gt;

&lt;p&gt;Traditional software governance assumes a human is somewhere in the loop. A person logs in, a person clicks the button, and the audit log records which person did it. Permissions attach to that person.&lt;/p&gt;

&lt;p&gt;An AI agent breaks that assumption in a very specific way. The agent holds the credential, decides on its own which tool to call, and calls it a few hundred times while you are asleep. There is no click to attribute.&lt;/p&gt;

&lt;p&gt;So governing an agent means controlling five separate things, and they really are separate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identity.&lt;/strong&gt; Which agent is this, who owns it, and what is it allowed to be?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization.&lt;/strong&gt; Which models, tools, data and systems can it reach?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spend.&lt;/strong&gt; How many dollars and how many tokens can it consume before it gets cut off?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior.&lt;/strong&gt; Was the input a prompt injection, was the output leaking a card number, was the answer just confidently wrong?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence.&lt;/strong&gt; Can you reconstruct, six weeks later, exactly what it did and why?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Miss any one of those and you don't have governance, you have a partial view with a nice chart on top of it.&lt;/p&gt;

&lt;p&gt;Gartner polled more than 3,400 organizations investing in agentic AI and predicted that &lt;a href="https://www.gartner.com/en/newsroom/press-releases/2025-06-25-gartner-predicts-over-40-percent-of-agentic-ai-projects-will-be-canceled-by-end-of-2027" rel="noopener noreferrer"&gt;over 40% of agentic AI projects&lt;/a&gt; will be canceled by the end of 2027, naming escalating costs and inadequate risk controls as two of the three reasons. Notice that both of those are governance failures and not model failures. The model was fine. Nobody had put a limit on it.&lt;/p&gt;

&lt;p&gt;And the readiness gap is not subtle. In a Deloitte survey of 3,235 IT and business leaders across 24 countries, only 21% said their organization has a mature governance model in place for agentic AI. So four out of five teams are running agents on trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five control points, walked through one incident
&lt;/h2&gt;

&lt;p&gt;This is the part I wish the other lists on this topic did, so let me do it here.&lt;/p&gt;

&lt;p&gt;Take one concrete setup. A customer support agent running on Claude Sonnet, wired to two MCP servers: one for Linear so it can file tickets, and one Postgres connection so it can look up an order. A support engineer built it in an afternoon. It works fine.&lt;/p&gt;

&lt;p&gt;Now walk through what goes wrong, as four separate events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event one.&lt;/strong&gt; The Postgres MCP server got registered with a connection string that has write access, because that was the string already sitting in the engineer's &lt;code&gt;.env&lt;/code&gt; file. Nobody noticed, because the agent never tried to write anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event two.&lt;/strong&gt; A customer pastes a support message that contains an instruction addressed to the agent, telling it to look up recent orders for a different email address. This is item one on the &lt;a href="https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/" rel="noopener noreferrer"&gt;OWASP Top 10 for Agentic Applications&lt;/a&gt;, published in December 2025, and this is the boring, non-exotic version of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event three.&lt;/strong&gt; The agent complies, and because it is trying to be helpful, it iterates. It runs the same lookup pattern 4,000 times over a two-hour window, each call carrying a large system prompt plus the full retrieved context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event four.&lt;/strong&gt; Somebody finds out the next morning, from a bill.&lt;/p&gt;

&lt;p&gt;So which layer catches which event?&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;identity&lt;/strong&gt; layer catches event one, and only event one. If the agent has an identity with declared scopes, then a read-only agent holding a write-capable credential is a visible contradiction, and you can alert on it. But identity has nothing at all to say about a malicious support message.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;behavior&lt;/strong&gt; layer, meaning guardrails, catches event two. A prompt injection classifier sitting in front of the model can flag that instruction and refuse the turn. But guardrails run per call, and they are stateless. Four thousand individually-innocent calls are four thousand clean passes.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;evidence&lt;/strong&gt; layer, meaning tracing and observability, sees all four events, perfectly, in full detail, after they have already happened. That is not a criticism of it, that is the definition of the layer. I wrote a whole post on &lt;a href="https://www.swapnoneel.site/blog/ai-observability-explained" rel="noopener noreferrer"&gt;what you should capture on every agent call&lt;/a&gt; and I still think tracing is the first thing you should turn on. It is just not a control.&lt;/p&gt;

&lt;p&gt;The only layer that can stop event three while it is happening is the layer that every single call physically passes through, counting tokens as they go, with the authority to return a 402 on call number 900.&lt;/p&gt;

&lt;p&gt;That layer is the gateway. Which is exactly why it is number one on this list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now, the five AI agent governance platforms
&lt;/h2&gt;

&lt;p&gt;That is the map. Five control points, and one hard rule underneath all of it: a tool can only enforce what it sits in front of.&lt;/p&gt;

&lt;p&gt;So here are the five, ordered by how much of the request path each one owns, with what it actually does and where it runs out of road.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Bifrost, governance on the request path
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; is Maxim AI's open-source AI gateway, written in Go, and it is at number one here because it is the only entry on this list that sits inside the call. Every model request and every MCP tool call passes through it, which makes it the only one that can refuse.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8lhiygmhmdkdjfz71qwm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8lhiygmhmdkdjfz71qwm.png" alt="Bifrost Governance Model" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The governance model is a three-tier hierarchy: &lt;strong&gt;customer&lt;/strong&gt;, then &lt;strong&gt;team&lt;/strong&gt;, then &lt;strong&gt;virtual key&lt;/strong&gt;. A virtual key is the thing your application actually authenticates with, and it is the primary governance entity. One looks roughly 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;"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;"vk-001"&lt;/span&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;"support-agent-prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sk-bf-*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_active"&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;"provider_configs"&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;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"anthropic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"weight"&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.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"allowed_models"&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;"claude-sonnet-4-5"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key_ids"&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;"anthropic-primary"&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;"team_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;"team-support-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&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-12-01T00: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;Read that config out loud as a sentence and you get the whole point of the layer! This agent may talk to Anthropic, may use exactly one model, may use exactly one of your provider API keys, belongs to the support team, and stops working in December whether anyone remembers it or not.&lt;/p&gt;

&lt;p&gt;Three parts of this are worth pulling out properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Budgets that cascade
&lt;/h3&gt;

&lt;p&gt;Budgets attach independently at each of the three tiers, and they get checked cumulatively. The virtual key's budget is checked, then the team's, then the customer's, and the request only proceeds if all of them still have room.&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;"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;"budget-support-vk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"virtual_key_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;"vk-001"&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_limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reset_duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1M"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"current_usage"&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.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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reset durations run from one minute up to one year (&lt;code&gt;1m&lt;/code&gt;, &lt;code&gt;1h&lt;/code&gt;, &lt;code&gt;1d&lt;/code&gt;, &lt;code&gt;1w&lt;/code&gt;, &lt;code&gt;1M&lt;/code&gt;, &lt;code&gt;1Q&lt;/code&gt;, &lt;code&gt;1Y&lt;/code&gt;), and there is a calendar-aligned mode that resets on real UTC month and quarter boundaries instead of a rolling window. That sounds like a tiny detail until you try to reconcile a rolling 30-day agent spend against a finance team's calendar month.&lt;/p&gt;

&lt;p&gt;There is also a budget override, with an effective limit that equals the base budget plus the override amount, granted either for a fixed number of cycles or forever. That primitive exists because the alternative, in every organization I have ever seen, is somebody quietly raising the real limit at 2am and never lowering it back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate limits that count tokens, and not just requests
&lt;/h3&gt;

&lt;p&gt;Rate limits live only at the virtual key tier, and they are two fully independent counters:&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;"token_max_limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token_reset_duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1h"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_max_limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_reset_duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1m"&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 separation is the thing that would have caught event three in the walkthrough. A requests-per-minute cap does nothing against an agent making a modest number of enormous calls. A token cap with its own independent reset window is the control that actually maps to how agents burn money.&lt;/p&gt;

&lt;p&gt;And the failures come back as honest HTTP semantics, which matters more than it sounds: &lt;strong&gt;402&lt;/strong&gt; for a budget exhausted, &lt;strong&gt;429&lt;/strong&gt; for a rate limit hit, &lt;strong&gt;403&lt;/strong&gt; for an inactive key, an expired key, or a blocked model. Your client code already knows how to handle those. You are not parsing an error string to find out what happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP tools as a governed surface
&lt;/h3&gt;

&lt;p&gt;This is the part that almost nothing else in the category does yet.&lt;/p&gt;

&lt;p&gt;Bifrost's MCP gateway treats tools the way it treats models, so tool filtering is deny-by-default at three levels. The client config sets which tools exist at all, per-request headers (&lt;code&gt;x-bf-mcp-include-clients&lt;/code&gt;, &lt;code&gt;x-bf-mcp-include-tools&lt;/code&gt;) narrow that set for a single call, and virtual key filtering overrides both. A key with no MCP configuration gets no MCP tools at all, period! You can also bundle a curated set of tools from several different servers into one MCP Tool Group and attach that group to a key.&lt;/p&gt;

&lt;p&gt;Now go back to event one in the walkthrough. A write-capable Postgres tool that the support agent's key was never granted is a tool the agent cannot even see, regardless of what is sitting in anybody's &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;For the behavior layer, Bifrost does not try to build its own classifiers. It plugs into AWS Bedrock Guardrails, Azure AI Content Safety, Google Model Armor and Patronus AI, and it emits OpenTelemetry out of the box. It also does &lt;a href="https://www.swapnoneel.site/blog/what-is-adaptive-load-balancing" rel="noopener noreferrer"&gt;adaptive load balancing&lt;/a&gt; with provider failover, semantic caching, and multi-node clustering, which is the reliability half of the same control plane.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The numbers.&lt;/strong&gt; Maxim's own benchmark puts the added latency in the tens of microseconds (the repository claims under 15 microseconds per request at 5,000 requests per second, the product page says 20), with 3.3 GB peak memory, and against LiteLLM it claims 9.5 times the throughput and 54 times faster latency at the 99th percentile, meaning the slowest one request in a hundred. Treat vendor benchmarks as vendor benchmarks, but the architectural reason behind them is real: this is a Go binary in your request path, and not a Python process. The core is Apache 2.0 on &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; at around 7.4k stars, and you can have it running with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @maximhq/bifrost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it runs out of road.&lt;/strong&gt; Bifrost governs the call, and it does not discover agents. If a team stands up an agent that talks to OpenAI directly and skips the gateway, Bifrost will never know that agent exists. So it is simultaneously the strongest enforcement point available and the easiest one to route around, which means it only works if you make it the only path out. Also, enterprise RBAC, SSO and audit logs sit in the commercial tier and not in the open-source core.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick it if&lt;/strong&gt; you want a control that can actually say no, and you are willing to make gateway egress mandatory.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Microsoft Agent 365 with Entra Agent ID
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.microsoft.com/en-us/microsoft-agent-365" rel="noopener noreferrer"&gt;Microsoft Agent 365&lt;/a&gt; is the most serious attempt yet at solving layer one properly.&lt;/p&gt;

&lt;p&gt;The idea underneath it is Entra Agent ID, and it is a genuinely good idea: every agent gets a first-class identity in the directory, the same way every employee has one. Once an agent is a directory object, everything Microsoft already built for humans starts applying to it. Conditional Access policies. Least-privilege scoping. Purview for data classification, Defender for threat detection, Intune for endpoint controls. Agent 365 then adds the registry and the admin hub on top, so you get a real inventory of the agent fleet, including the agents somebody spun up in Copilot Studio and never told you about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphbudyr5vznh08tlpt66.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphbudyr5vznh08tlpt66.jpg" alt="Microsoft Agent 365" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That registry is the answer to the exact problem a gateway cannot solve. Agent sprawl is a discovery problem before it is an enforcement problem, and you cannot govern an agent you have not found yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it runs out of road.&lt;/strong&gt; The gravity is real. Coverage is deepest for Copilot Studio and Azure AI Foundry agents and for agents on managed endpoints, and it thins out fast for a Python agent your ML team runs in a container on another cloud. It also does not do token budgets in any meaningful sense. It will tell you an agent exists and what it may touch, and it will not stop that agent at dollar 4,000.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick it if&lt;/strong&gt; you are already an Entra shop, which honestly means pick it anyway, because identity is the one layer you cannot fake.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Zenity, posture and runtime action validation
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://zenity.io" rel="noopener noreferrer"&gt;Zenity&lt;/a&gt; comes at this from the security side rather than the platform side, and its strength is the part of your estate that nobody has an inventory of.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7uou7k3plv6txnsywav.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7uou7k3plv6txnsywav.png" alt="Zenity" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It discovers agents living inside Copilot Studio, Power Platform, AWS Bedrock and Google Vertex AI, then keeps watching what those agents touch and share. The framing is AI Security Posture Management, and the genuinely useful mechanic is buildtime policy: it flags an over-permissioned connector before the agent ships, which is the cheapest possible moment to catch event one from the walkthrough. Gartner named Zenity a 2025 Cool Vendor in Agentic AI TRiSM (their acronym for trust, risk and security management), and that low-code coverage is why.&lt;/p&gt;

&lt;p&gt;So why does the low-code angle matter this much? Well, because that is where the agents you don't know about actually live. An engineer's Python agent is at least in a repository somewhere. A business analyst's Copilot Studio agent with access to a SharePoint site full of contracts is nowhere you are currently looking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it runs out of road.&lt;/strong&gt; It is a security product, so it governs risk and not cost. There is no budget primitive here and no token accounting at all. And its discovery is strongest in exactly the places Microsoft's is strongest, so if you already have Agent 365, the overlap deserves a hard look before you pay for both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick it if&lt;/strong&gt; a lot of your agents are being built by people who are not engineers.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Arthur AI, guardrails and evaluations without a framework tax
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://arthur.ai" rel="noopener noreferrer"&gt;Arthur AI&lt;/a&gt; launched its Agent Discovery and Governance platform in December 2025, and the thing I find genuinely interesting about it is that it is framework-neutral in a category that mostly is not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frf7yavj5ctnp7powdtgq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frf7yavj5ctnp7powdtgq.jpg" alt="Arthur AI" width="798" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Discovery works across four vectors: OpenTelemetry streams, MCP server monitoring, network-layer analysis, and the platform APIs of Vertex AI, Bedrock and Azure AI Foundry. And because the tracing is built on OpenTelemetry and OpenInference rather than a proprietary SDK, it works with LangChain, LlamaIndex, raw OpenAI and Anthropic calls, and whatever your team decides to pick next quarter.&lt;/p&gt;

&lt;p&gt;On top of that it runs two things that are worth keeping separate in your head. Runtime guardrails, which intercept before and after the model call for PII, prompt injection and toxicity. And continuous evaluations against live production traffic, which is the part that catches hallucination and quality drift, the failures that never throw an error and never show up in a latency graph.&lt;/p&gt;

&lt;p&gt;That second distinction is the one I care most about, and it comes from doing the work by hand and getting it half right. On a contract with a startup building an AI co-worker that lives in Slack (keeping it a bit vague, can't reveal much more than this lol), I was the first layer of internal testing, and I built an internal tool that captured the product's data logs and generated reports on latency and probable slowdowns. It was useful, and the core users got a better product out of it. But it told me when the thing was slow, and it never once told me when the thing was confidently wrong. Those are two different systems, and I had only built one of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it runs out of road.&lt;/strong&gt; Same ceiling as any observability-first product: it sees everything and blocks a narrow slice of it. The guardrails are per-call, so the accumulation problem from event three stays unsolved, and there is no spend primitive here either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick it if&lt;/strong&gt; your stack is heterogeneous and output quality is your real risk, rather than permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Credo AI, the compliance program of record
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.credo.ai" rel="noopener noreferrer"&gt;Credo AI&lt;/a&gt; is on this list for a reason that has nothing to do with runtime, and I would rather be blunt about that than pretend it belongs in the same bucket as the other four.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl167d3aq6kn3rg17enan.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl167d3aq6kn3rg17enan.png" alt="Credo AI" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Its Policy Packs translate regulation into something an engineering team can actually act on, mapping the EU AI Act, ISO 42001 and the NIST AI Risk Management Framework down into concrete requirements. It keeps an AI Agent Registry of what is in production, plus GAIA, an assistant for the governance workflow itself. When a regulator or an enterprise customer's security review asks what your agents do and which controls apply to them, this is the layer that produces the answer.&lt;/p&gt;

&lt;p&gt;And the timing here is worth getting right, because a lot of content on this topic is now stale. The Digital Omnibus on AI &lt;a href="https://www.gibsondunn.com/eu-ai-act-omnibus-agreement-postponed-high-risk-deadlines-and-other-key-changes/" rel="noopener noreferrer"&gt;entered into force on 27 July 2026&lt;/a&gt;, and it deferred the high-risk obligations for standalone Annex III systems all the way out to 2 December 2027, and to 2 August 2028 for AI embedded in regulated products. So the deadline a lot of teams panic-bought a platform for has moved.&lt;/p&gt;

&lt;p&gt;What did not move is Article 50. The transparency duties landed on 2 August 2026, and those catch every chatbot and every piece of synthetic content, which in practice means they catch most agent deployments regardless of risk tier. The high-risk regime got 16 more months. The disclosure regime is live right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it runs out of road.&lt;/strong&gt; Credo AI cannot block a request. It produces policy and evidence, and if you buy it expecting enforcement, you have made the exact purchasing mistake this whole post is about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick it if&lt;/strong&gt; you sell into regulated industries, or your legal team has started asking questions in writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What none of these five actually do
&lt;/h2&gt;

&lt;p&gt;Time for the honest part, because a list that ends on five recommendations and no caveats is just an ad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent-to-agent calls are mostly ungoverned.&lt;/strong&gt; When agent A hands a task off to agent B, whose budget does it hit, whose identity does it carry, and does B's guardrail see the original user's prompt or A's rewritten version of it? Inter-agent communication is item seven on the OWASP agentic list, and I have not seen anybody solve it convincingly yet. Gateways get closest, because that hop is still an HTTP call, and "closest" is doing a lot of work in that sentence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory is a governance hole.&lt;/strong&gt; An agent that writes a poisoned fact into its long-term store carries that fact past every per-call guardrail forever, because on every future call it is not an injection anymore, it is just context. Nothing on this list governs writes to agent memory the way it governs writes to a database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent identity is not portable.&lt;/strong&gt; Entra Agent ID is real and it is good, and it is also Microsoft's. There is no cross-vendor standard yet, so an agent's identity in your directory means nothing to a partner's system. We are roughly where federated human identity was before SAML showed up.&lt;/p&gt;

&lt;p&gt;And the buying mistake, which is the whole reason I ordered this list the way I did. A team worried about a runaway bill buys a compliance platform, and a team worried about a regulator buys a tracing tool. Both walk away with a real product doing a real job, and neither one has touched the thing that wakes them up at 3am.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is an AI gateway enough on its own for agent governance?&lt;/strong&gt; For spend, authorization and tool access, yes, and it is the strongest control you can get for those three. For discovery and identity, no. A gateway cannot see an agent that does not route through it, so pair it with an identity or discovery layer, and make gateway egress mandatory at the network level rather than by policy document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need an AI agent governance platform for a small team?&lt;/strong&gt; You need two of the five control points from day one, and neither of them is a purchase. Put every model call behind one gateway with a per-key spending cap, and turn tracing on. My own side project Scholarian has fetched and ranked over 10,000 papers across 250-plus search sessions, and the reason I care about per-key caps is not enterprise policy, it is that a retry loop in a research pipeline is perfectly capable of spending real money overnight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between AI agent governance and AI observability?&lt;/strong&gt; Observability tells you what happened, governance decides what is allowed to happen in the first place. They get sold together because tracing is how you prove a policy was enforced, but a trace has no authority. If your platform can produce a beautiful timeline of an incident and could not have prevented that incident, you bought observability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where do guardrails fit into all this?&lt;/strong&gt; Guardrails are the behavior layer, and they run per call, screening inputs for injection and outputs for leakage or toxicity. They are necessary, and they are stateless, which is why they miss failures made out of many individually-fine calls. Run them at the gateway rather than inside each application, so that one policy update covers every agent instead of six repositories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the EU AI Act require an agent governance platform?&lt;/strong&gt; No regulation names a product. But the Article 50 transparency duties are in force as of 2 August 2026, and the deferred high-risk regime still expects risk management, logging and human oversight when it lands in December 2027. Those obligations are much cheaper to meet if the logging was there from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would I actually pick?
&lt;/h2&gt;

&lt;p&gt;If I could only run one thing, it is the gateway, and that is Bifrost. Put every model call and every MCP tool call behind it, then set a token budget with its own reset window on every virtual key and deny tools by default. It is the only layer on this list that can turn a policy into a refusal, it is Apache 2.0 so the argument costs you nothing but an afternoon, and at microsecond-scale overhead you are not trading latency for control.&lt;/p&gt;

&lt;p&gt;Then add identity second, and not fifth. Agent 365 if you are on Entra, which most enterprises already are. Discovery is the one problem a gateway genuinely cannot solve for you, and most of the rest of this list is a refinement of a control you would already have.&lt;/p&gt;

&lt;p&gt;And here's my bet for the next 18 months: this category does not stay five layers wide. Identity and posture will collapse into whoever already owns your directory, evaluations will fold into observability, and the thing that survives as a separate purchase is the request path, because that is the only place enforcement is physically possible. If I turn out to be wrong about that, I'll happily write the follow-up.&lt;/p&gt;

&lt;p&gt;That's all from my side, folks. If you are running agents in production and you have found a control that actually stopped something, I want to hear about it, so drop it in the comments. And if you want more of this, I write about AI infrastructure regularly on &lt;a href="https://x.com/swapnoneel123" rel="noopener noreferrer"&gt;X&lt;/a&gt; and on &lt;a href="https://www.swapnoneel.site" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>What Is Semantic Caching, and Where It Quietly Breaks</title>
      <dc:creator>Swapnoneel Saha</dc:creator>
      <pubDate>Wed, 19 Aug 2026 17:39:37 +0000</pubDate>
      <link>https://dev.to/swapnoneel123/what-is-semantic-caching-and-where-it-quietly-breaks-514o</link>
      <guid>https://dev.to/swapnoneel123/what-is-semantic-caching-and-where-it-quietly-breaks-514o</guid>
      <description>&lt;p&gt;Two people open your support chatbot within the same minute. One types &lt;code&gt;How do I reset my password?&lt;/code&gt; and the other types &lt;code&gt;i forgot my password, how do i get a new one&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Same question, same answer, two full model calls, and your Redis cache stores both as separate keys without ever hitting on either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic caching&lt;/strong&gt; stores past LLM responses and serves them to new queries that mean the same thing, even when the words are completely different. It works by turning every query into a vector, searching for the nearest stored vector, and returning that cached answer if the similarity clears a threshold you set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your existing cache does nothing for LLM traffic
&lt;/h2&gt;

&lt;p&gt;Every cache you have ever written works on exact equality. You take the request, hash it, look up the hash, and either the bytes match or they don't. Redis and Memcached both work this way, and so does the HTTP layer sitting in front of them. It works brilliantly.&lt;/p&gt;

&lt;p&gt;It works because the traffic it was designed for is machine-generated. &lt;code&gt;GET /api/users/42&lt;/code&gt; is always spelled the same way by the same client, every time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jeu8z2sgsj1g54k16lr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jeu8z2sgsj1g54k16lr.png" alt="Exact matching misses equivalent questions" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Human language is not like that. People can ask for a refund policy in many ways, and a hash function treats each wording as a different key. One extra space, one lowercase letter, one "please" at the end, and you get a completely different key.&lt;/p&gt;

&lt;p&gt;So your hit rate on natural language collapses to nearly zero, and you go on paying for the same answer over and over.&lt;/p&gt;

&lt;p&gt;Semantic caching fixes the matching function instead of the cache. The storage stays boring. What changes is that you stop asking "are these two strings identical" and start asking "are these two strings close enough in meaning".&lt;/p&gt;

&lt;p&gt;The difficult part is deciding what counts as &lt;strong&gt;close&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How semantic caching works
&lt;/h2&gt;

&lt;p&gt;The rule in plain English:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Turn the question into a point in space. Look for the nearest point we have already answered. If it is near enough, reuse that answer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In practice, it takes five steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A query comes in.&lt;/li&gt;
&lt;li&gt;You send it to an embedding model, which returns a vector of floats.&lt;/li&gt;
&lt;li&gt;You search your vector store for the nearest stored vector, using cosine similarity.&lt;/li&gt;
&lt;li&gt;If the best match scores above your threshold, you return the stored response and never call the model at all. That's a cache hit.&lt;/li&gt;
&lt;li&gt;If nothing clears the threshold, you call the model, return the real answer, and write the query vector plus the response into the store with an expiry time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fscfpabis1jjf01mxg69u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fscfpabis1jjf01mxg69u.png" alt="The five-step semantic caching pipeline" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In code it is almost insultingly short:&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;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.92&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                         &lt;span class="c1"&gt;# step 2
&lt;/span&gt;    &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nearest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# step 3
&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                     &lt;span class="c1"&gt;# step 4
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;                  &lt;span class="c1"&gt;# cache hit, zero model tokens
&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;# step 5, cache miss
&lt;/span&gt;    &lt;span class="n"&gt;store&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;vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what step 4 is really doing. It is taking a floating point number and using it to decide whether a human being gets a fresh answer or a recycled one. There is no other logic in this system, no parsing and no intent classification, nothing else that ever looks at what was actually asked.&lt;/p&gt;

&lt;p&gt;If you want the mechanics of what &lt;code&gt;store.nearest&lt;/code&gt; is doing underneath, I built one of these from scratch, cosine similarity and the HNSW graph and all, in my post on &lt;a href="https://www.swapnoneel.site/blog/build-vector-database-from-scratch" rel="noopener noreferrer"&gt;building a vector database from scratch&lt;/a&gt;. The short version is that it is an approximate nearest neighbour search, so it is fast, and it is also allowed to be a little bit wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The similarity threshold is the whole product
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fozy9b0qio9cwi7gesn26.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fozy9b0qio9cwi7gesn26.png" alt="One threshold, two opposing failure modes" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Consider three pairs of queries and how an embedding model scores them.&lt;/p&gt;

&lt;p&gt;Pair one. &lt;code&gt;What is your refund policy?&lt;/code&gt; and &lt;code&gt;Can I get my money back?&lt;/code&gt; Different words, same intent. This one &lt;strong&gt;should&lt;/strong&gt; score high, and it does.&lt;/p&gt;

&lt;p&gt;Pair two. &lt;code&gt;Show me the sales numbers for Q1 2025&lt;/code&gt; and &lt;code&gt;Show me the sales numbers for Q3 2024&lt;/code&gt;. Almost identical strings, completely different answers. You would want this to score low. It does not. It scores extremely high, because most of the tokens are shared and the embedding barely notices which quarter you asked about.&lt;/p&gt;

&lt;p&gt;Pair three. &lt;code&gt;Is this drug safe for pregnant patients?&lt;/code&gt; and &lt;code&gt;Is this drug not safe for pregnant patients?&lt;/code&gt; One word apart, opposite meaning.&lt;/p&gt;

&lt;p&gt;Embedding models are often poor at handling negation. A &lt;a href="https://arxiv.org/html/2608.10216" rel="noopener noreferrer"&gt;validity audit published in August 2026&lt;/a&gt; tested 9 encoder configurations and found that negation and antonym pairs score &lt;strong&gt;above&lt;/strong&gt; genuinely similar pairs on every model tested, at average cosines of &lt;strong&gt;0.93 to 0.999&lt;/strong&gt;. In the production system that paper audits, flipping an instruction from "withhold the study drug" to "administer the study drug" scored &lt;strong&gt;0.9608&lt;/strong&gt;, and the safety gate that existed specifically to catch that never fired.&lt;/p&gt;

&lt;p&gt;The reversed instruction scored higher than most legitimate paraphrases would.&lt;/p&gt;

&lt;p&gt;A threshold like 0.92 does not cleanly separate "same question" from "different question". What it often separates is &lt;strong&gt;surface form&lt;/strong&gt;, and surface form is not meaning. Two sentences that share a grammatical frame and differ in one date, one entity, or one negation will sit above almost any threshold you are willing to set.&lt;/p&gt;

&lt;p&gt;That's the trap. Raise the threshold to 0.98 and you kill your hit rate, because honest rephrasings stop matching. Lower it to 0.85 and you start serving Q3 2024's numbers to someone asking about Q1 2025. There is no single number that fixes both, because the failure is in the measurement and not in the cutoff.&lt;/p&gt;

&lt;p&gt;What helps is refusing to let similarity be the only gate. Partition the cache by anything the embedding is bad at holding: user, tenant, model, and any structured parameter your queries carry. If dates and IDs are pulled out into the cache key instead of being left sitting inside the prose, the embedding never gets a chance to blur them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semantic caching vs prompt caching vs KV caching
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjkhh3wr7lia2pkd4dynv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjkhh3wr7lia2pkd4dynv.png" alt="Three caching layers at different depths" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These three names get used interchangeably online, but they refer to different layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KV caching&lt;/strong&gt; lives inside the GPU. During inference the model computes key and value tensors for every token in your context, and the KV cache keeps them around so the next token does not need to recompute attention over everything before it. This is always on, you do not configure it, and it is the reason generation gets faster after the first token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt caching&lt;/strong&gt;, sometimes called prefix caching, is what OpenAI and Anthropic sell you at the API level. It reuses those KV tensors across requests when two requests share a common prefix. So if you send a 4,000 token system prompt on every call, the provider can skip recomputing it and charges you less for those tokens. Important detail: it matches on &lt;strong&gt;exact prefix bytes&lt;/strong&gt;, so two prompts that mean the same thing but start differently will miss it entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic caching&lt;/strong&gt; sits in front of the model, in infrastructure you control. It stores whole request and response pairs and matches on meaning. When it hits, you save 100% of the call, because the model is never invoked.&lt;/p&gt;

&lt;p&gt;You can use all three together. A request can try the semantic cache first, fall through to the provider's prompt cache on a miss, and only then pay for full inference. Semantic caching is the outer layer, so it is also the one you control.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a realistic cache hit rate?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wcix9fspjnxzdjazh61.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wcix9fspjnxzdjazh61.png" alt="Real-world hits are a minority of requests" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The 95% figure quoted in semantic-caching marketing is easy to misread.&lt;/p&gt;

&lt;p&gt;You will see 95% quoted constantly. Trace that number back and it almost never refers to hit rate. It refers to &lt;strong&gt;match accuracy&lt;/strong&gt;, meaning the cached response was correct 95% of the time it was served. Those are entirely different claims, and the second one tells you nothing about how much money you saved.&lt;/p&gt;

&lt;p&gt;Actual production numbers are much lower. A &lt;a href="https://dev.to/gauravdagde/llm-semantic-caching-the-95-hit-rate-myth-and-what-production-data-actually-shows-8ga"&gt;breakdown of real deployment data&lt;/a&gt; puts typical hit rates at &lt;strong&gt;20 to 45%&lt;/strong&gt;, with Portkey seeing around 20% on retrieval-augmented workloads and an EdTech platform hitting about 45% on student question-and-answer traffic. Open-ended chat sits at 10 to 20% because its queries repeat less often.&lt;/p&gt;

&lt;p&gt;Academic results land in a similar band. The &lt;a href="https://arxiv.org/pdf/2411.05276" rel="noopener noreferrer"&gt;GPT Semantic Cache paper&lt;/a&gt; reports cutting API calls by up to &lt;strong&gt;68.8%&lt;/strong&gt;, but that is on query categories picked for repetition, which is exactly the workload where this technique looks its best.&lt;/p&gt;

&lt;p&gt;Even a 20% hit rate can matter. On a $5,000 monthly bill, that is $1,000 saved. A cache hit returns in under 5 milliseconds instead of the 2 to 5 seconds a real completion takes, so the speed difference may matter even more than the cost.&lt;/p&gt;

&lt;p&gt;Set expectations using your own traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What semantic caching costs you to run
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8fwahwaw4vu6fnhlacgn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8fwahwaw4vu6fnhlacgn.png" alt="Every request pays the semantic lookup toll" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You pay for an embedding call and a vector search on &lt;strong&gt;every request&lt;/strong&gt;, including the 60 to 80% that miss. Embeddings are cheap compared to a chat completion, so the money side is fine. The latency is the thing to watch, because you have just added a network round trip to the front of every request in your system, including all the ones the cache cannot help with.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://keploy.io" rel="noopener noreferrer"&gt;Keploy&lt;/a&gt; I built a retrieval-augmented chatbot over their documentation using vector embeddings, and docs traffic is close to the best case for this technique. People ask the same twenty questions in fifty phrasings, forever. Even there, you trade a small cost on every request for a larger saving on the requests that hit. Measure that ratio before assuming the cache pays off.&lt;/p&gt;

&lt;p&gt;Then there is staleness. Your cache does not know your prices changed on Tuesday. The stored answer is a frozen snapshot of what the model said, plus whatever context it was given at the time, and it keeps being served until its expiry time runs out. Short expiry times are safer and hit less. Long ones are the opposite. Pick deliberately.&lt;/p&gt;

&lt;p&gt;Multi-turn conversations are worse. A follow-up like "and what about the second one?" embeds to almost nothing useful, because the meaning lives in the previous four messages and not in that sentence. The safer approach is to stop caching after a few turns of history.&lt;/p&gt;

&lt;p&gt;And you now operate a vector store. That is one more thing to size, monitor, and pay for.&lt;/p&gt;

&lt;h2&gt;
  
  
  One cache, many tenants, and the leak nobody plans for
&lt;/h2&gt;

&lt;p&gt;This is the part that turns semantic caching from a performance feature into a security decision, and it is why I would not hand-roll one at the application layer in an enterprise setting.&lt;/p&gt;

&lt;p&gt;A semantic cache with one global namespace returns the nearest previous response across every user in it. Not the nearest response &lt;em&gt;belonging to that user&lt;/em&gt;. The nearest one, period.&lt;/p&gt;

&lt;p&gt;Picture two customers of the same SaaS product asking structurally similar questions about their own account data. Their prompts embed within 0.93 cosine of each other because they are the same question about different companies, and the cache hands one customer the other's cached answer. Nothing errors or logs a violation. It looks like a successful cache hit, making the leak hard to detect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwsotlhxsvm0yrky97y93.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwsotlhxsvm0yrky97y93.png" alt="A shared cache can cross tenant boundaries" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is a subtler version too. Even with no wrong response served, cache hits are dramatically faster than misses, and that timing difference is observable from outside. Somebody probing your API can learn which questions have already been asked by other tenants just by watching time to first token.&lt;/p&gt;

&lt;p&gt;The fix is not clever, it is structural. The cache key has to be partitioned by a tenant identifier resolved from something you trust, meaning the API key, a virtual key, or a signed token claim. Never from the request body, because the request body belongs to the attacker. And the lookup has to be scoped to that namespace so a cross-tenant match is not merely unlikely, it is unreachable.&lt;/p&gt;

&lt;p&gt;This is why I prefer to handle semantic caching once at the gateway instead of rebuilding tenant isolation inside every service.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should you not use semantic caching?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F318wp496sschu36wzkyd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F318wp496sschu36wzkyd.png" alt="Some workloads should bypass semantic caching" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anything that must be current.&lt;/strong&gt; Live inventory, account balances, order status. A stale answer here is worse than a slow one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anything where the parameters matter more than the phrasing.&lt;/strong&gt; Analytical queries over dates, IDs, and entities are precisely where embeddings blur the thing you needed preserved. If you cannot pull those values out into the cache key, skip it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anything high-stakes and low-volume.&lt;/strong&gt; Medical, legal, financial advice. The negation problem described above can cause serious harm in those domains. And if your volume is low, you were not saving much anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long open-ended conversations.&lt;/strong&gt; A 10 to 20% hit rate while adding latency to 100% of requests is a bad trade.&lt;/p&gt;

&lt;p&gt;Where it does earn its place: support bots, docs assistants, FAQ layers, onboarding flows, internal knowledge search, and any product where a large user base asks a small set of questions in a lot of different ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Bifrost does semantic caching at the gateway
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.getbifrost.ai/overview" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; is Maxim AI's high-performance, &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;open-source AI gateway&lt;/a&gt; that unifies access to 20+ providers through a single OpenAI-compatible API. Because semantic caching runs at the gateway, one cache policy is applied before requests reach any configured model provider.&lt;/p&gt;

&lt;p&gt;Its &lt;a href="https://docs.getbifrost.ai/features/semantic-caching" rel="noopener noreferrer"&gt;semantic cache&lt;/a&gt; combines exact hash matching with vector similarity search, supports per-request TTL and threshold overrides, and keeps cache entries separate by model and provider by default.&lt;/p&gt;

&lt;p&gt;Bifrost uses two cache layers.&lt;/p&gt;

&lt;p&gt;Every request first goes through a direct hash lookup. If the prompt is byte-identical to something already cached, it returns immediately with zero embedding overhead, which matters because you just skipped the round trip that would otherwise tax every request in the system. Only on a direct miss does it embed the query and run the similarity search. So the cheap path stays cheap, and the expensive path only runs when it might actually pay off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyspil8wub276z4bz7hcz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyspil8wub276z4bz7hcz.png" alt="Direct and semantic cache layers at the gateway" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching only activates when a request carries a cache key&lt;/strong&gt;, passed as an &lt;code&gt;x-bf-cache-key&lt;/code&gt; header. There is no global-namespace mode for you to accidentally ship. If you want per-tenant isolation, the tenant identifier goes in that header, and cross-tenant matches then cannot happen because those entries are not in the same partition. Requiring a cache key makes isolation mandatory rather than optional.&lt;/p&gt;

&lt;p&gt;Its other defaults are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;threshold&lt;/code&gt; defaults to &lt;strong&gt;0.8&lt;/strong&gt; for semantic hits, overridable per request with &lt;code&gt;x-bf-cache-threshold&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ttl&lt;/code&gt;, the time to live on an entry, defaults to &lt;strong&gt;5 minutes&lt;/strong&gt;. This reduces stale answers, and you can override it per request.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;conversation_history_threshold&lt;/code&gt; defaults to &lt;strong&gt;3&lt;/strong&gt;, which means it stops caching once a conversation runs past 3 messages.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cache_by_model&lt;/code&gt; and &lt;code&gt;cache_by_provider&lt;/code&gt; are both on by default, so a cached GPT answer never gets served to a Claude request.&lt;/li&gt;
&lt;li&gt;The vector store is pluggable across Redis or Valkey, Weaviate, Qdrant, and Pinecone, so you are not forced into adopting a new database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every response carries a &lt;code&gt;cache_debug&lt;/code&gt; block with &lt;code&gt;cache_hit&lt;/code&gt;, &lt;code&gt;hit_type&lt;/code&gt; (direct or semantic), the actual &lt;code&gt;similarity&lt;/code&gt; score, and a &lt;code&gt;cache_id&lt;/code&gt;. The similarity score helps diagnose a bad match, while &lt;code&gt;cache_id&lt;/code&gt; lets you remove the affected entry or clear its partition through the API.&lt;/p&gt;

&lt;p&gt;Every cache hit avoids the completion cost. At the gateway, the cache sits next to budgets, virtual keys, and routing. The same layer that chooses &lt;em&gt;which&lt;/em&gt; provider gets a request can first decide whether the request needs a provider at all. I cover the routing side separately in &lt;a href="https://www.swapnoneel.site/blog/what-is-adaptive-load-balancing" rel="noopener noreferrer"&gt;what adaptive load balancing actually is&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Bifrost adds under 100 microseconds of overhead at 5,000 requests per second, which is negligible next to the embedding call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should you do first?
&lt;/h2&gt;

&lt;p&gt;Do not start by building the cache.&lt;/p&gt;

&lt;p&gt;Start by measuring how repetitive your traffic is. Take a week of your logs, embed the queries, and count how many land within 0.92 of an earlier one. That gives you an upper bound on the possible hit rate. A week of your own traffic will tell you more than a benchmark based on someone else's workload.&lt;/p&gt;

&lt;p&gt;If that number comes back at 30% or better, turn semantic caching on at your gateway. Partition the cache key by tenant from day one and keep the expiry short. For the first few weeks, watch the similarity scores on cache hits instead of trusting the threshold.&lt;/p&gt;

&lt;p&gt;If it comes back at 8%, the workload is too varied for semantic caching to save much.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flwzz9uz8e0dazpi297z9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flwzz9uz8e0dazpi297z9.png" alt="Measure repetition before turning caching on" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have run a semantic cache in production and watched it serve something it should not have, tell me about it in the comments. Those failures are the useful stories.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>ai</category>
      <category>performance</category>
    </item>
    <item>
      <title>AI Observability Explained: What It Is and How It Works</title>
      <dc:creator>Swapnoneel Saha</dc:creator>
      <pubDate>Tue, 18 Aug 2026 06:25:25 +0000</pubDate>
      <link>https://dev.to/swapnoneel123/ai-observability-explained-what-it-is-and-how-it-works-487</link>
      <guid>https://dev.to/swapnoneel123/ai-observability-explained-what-it-is-and-how-it-works-487</guid>
      <description>&lt;p&gt;Traditional monitoring rests on one quiet assumption that nobody ever writes down: the same input gives you the same output. Something breaks, you replay the request, you watch it break again, you fix it.&lt;/p&gt;

&lt;p&gt;Now send the same request to a model twice. You get two different answers, and neither one of them threw an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI observability&lt;/strong&gt; is the practice of recording what happened inside an AI system on every request: the prompt, the model version, tokens, cost, latency, tool calls, and a judgement of whether the output was any good. Monitoring tells you the service is up. Observability tells you why it answered that way.&lt;/p&gt;

&lt;p&gt;That gap is the whole story here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your current monitoring stack misses all of this
&lt;/h2&gt;

&lt;p&gt;Your existing setup is watching for crashes. Status codes, error rates, p99 latency, memory. All of it is designed around the idea that a broken thing looks broken.&lt;/p&gt;

&lt;p&gt;An AI feature failing looks nothing like that. It returns HTTP 200 in 900ms, with grammatically perfect prose that happens to be wrong, or that quietly ignored the document you retrieved for it, or that called the refund tool when the user only asked a question.&lt;/p&gt;

&lt;p&gt;Your dashboard sees a healthy service, because by every measure it has, the service is healthy.&lt;/p&gt;

&lt;p&gt;And there are whole categories of failure your stack has no field for. It has nowhere to put "this response cost 14 cents", or "the model version changed under us last Tuesday", or "the retrieved context was garbage". Those are not infrastructure facts, and standard telemetry was never built to carry them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F72i1wisqwyz7zwvurepu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F72i1wisqwyz7zwvurepu.png" alt="Why standard monitoring misses AI quality failures" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Something has to hold those fields instead, which is the entire reason this tooling exists. &lt;a href="https://docs.getbifrost.ai/overview" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; is Maxim AI's high-performance, &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;open-source AI gateway&lt;/a&gt; that unifies access to 20+ providers through a single OpenAI-compatible API. Its position between the application and the model providers makes it useful for observability: every model request and response passes through the same layer, regardless of provider. Bifrost's &lt;a href="https://docs.getbifrost.ai/features/observability/default" rel="noopener noreferrer"&gt;built-in observability&lt;/a&gt; records the input, output, provider, model, token usage, cost, latency, errors, and status for each call, then makes those fields searchable in a live dashboard. Those are the details standard HTTP telemetry misses even when the request returns &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one AI request actually looks like when you trace it
&lt;/h2&gt;

&lt;p&gt;This is the part that made it click for me, so let me walk through a real shape.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://keploy.io" rel="noopener noreferrer"&gt;Keploy&lt;/a&gt; I built a retrieval-augmented chatbot over their documentation, using vector embeddings, so developers could ask a question instead of hunting through pages. A single question to something like that is not one operation. It is a chain, and a trace is just that chain written down.&lt;/p&gt;

&lt;p&gt;One request breaks into spans, where a &lt;strong&gt;span&lt;/strong&gt; is one step with its own start time, end time, inputs and outputs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user's question comes in and opens the root span.&lt;/li&gt;
&lt;li&gt;The question gets embedded into a vector. That is a span, with its own model and its own cost.&lt;/li&gt;
&lt;li&gt;The vector search runs and returns, say, five chunks of documentation. That is a span, and the important bit is that it records &lt;em&gt;which&lt;/em&gt; five chunks came back.&lt;/li&gt;
&lt;li&gt;Those chunks get stuffed into a prompt template along with the chat history.&lt;/li&gt;
&lt;li&gt;The model call goes out. This span carries the model name and version, the temperature, the prompt tokens, the completion tokens, the cost in dollars, the total latency, and the time to first token.&lt;/li&gt;
&lt;li&gt;If the model calls a tool, every one of those is its own child span too.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now here is why anybody bothers with all that plumbing.&lt;/p&gt;

&lt;p&gt;When the bot gives a bad answer, you do not have to guess. You open the trace and look at step 3. If the vector search pulled back five irrelevant chunks, your problem is chunking or embeddings, and the model did nothing wrong. If the search pulled back exactly the right documentation and the model still answered from thin air, your problem is the prompt.&lt;/p&gt;

&lt;p&gt;Two completely different fixes, and without the trace you cannot tell them apart. All you have is "the bot said something dumb", which is the single most useless bug report in the world.&lt;/p&gt;

&lt;h2&gt;
  
  
  The things worth capturing on every call
&lt;/h2&gt;

&lt;p&gt;You will notice I have not called this section "the three pillars of observability". Everyone else writing about this does, and I dropped it on purpose, because logs, metrics and traces is a framing built for deterministic systems and it has no slot at all for "was the answer any good".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3s0utdi83ubyijnvze1x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3s0utdi83ubyijnvze1x.png" alt="The telemetry worth capturing on every AI call" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So here is the actual list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The full prompt and the response&lt;/strong&gt;, as they really went over the wire, after every template and system message got assembled. Not the template, the final text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model, version and parameters.&lt;/strong&gt; Providers ship silent updates. If you cannot say which exact version answered a request, you cannot explain last month's regression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens in, tokens out, and cost in dollars&lt;/strong&gt; per request, attributed to a user or a feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency, split into total time and time to first token.&lt;/strong&gt; Those two numbers feel completely different to a user, and one can get worse while the other improves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool calls, retries and fallbacks.&lt;/strong&gt; Which key was tried, what failed, what it fell back to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A trace ID that ties the whole chain together&lt;/strong&gt;, and ideally a session or user identifier so you can reconstruct a full conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A quality score&lt;/strong&gt;, attached after the fact. More on that next.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built one of these myself earlier this year, an internal tool at a contract role that captured an AI product's logs and turned them into reports on latency and probable slowdowns (keeping it vague on purpose, cannot say much more than that). The honest takeaway was not that the tool was clever. It was that a team can ship for months on vibes, and the moment somebody puts the per-request numbers on a screen, problems nobody previously had words for suddenly have words.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you measure quality when there is no right answer?
&lt;/h2&gt;

&lt;p&gt;Well, you do not measure it the way you measure a unit test, because there is no expected string to compare against.&lt;/p&gt;

&lt;p&gt;The industry has mostly settled on three overlapping things. &lt;strong&gt;LLM-as-judge&lt;/strong&gt;, where you send the input and output to a second model with a rubric and it scores relevance or faithfulness or tone. It is imperfect, and it is far better than nothing. &lt;strong&gt;Human annotation&lt;/strong&gt; on a sample, which is slow, expensive, and still the ground truth everything else gets calibrated against. And &lt;strong&gt;implicit user signals&lt;/strong&gt;, like thumbs, edits and retries, which are noisy but free.&lt;/p&gt;

&lt;p&gt;Run those continuously and you get &lt;strong&gt;drift detection&lt;/strong&gt;, which is just the same score measured over time. When your faithfulness score drops 8 points over two weeks and nobody deployed anything, something moved underneath you, and that is usually the model provider.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhkiusbk7ev7rfgmtj6c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhkiusbk7ev7rfgmtj6c.png" alt="Three ways to measure AI output quality" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the half most teams skip, and there are numbers on it. In LangChain's &lt;a href="https://www.langchain.com/state-of-agent-engineering" rel="noopener noreferrer"&gt;2026 State of Agent Engineering report&lt;/a&gt;, which surveyed 1,300+ practitioners, 89% said they had observability running on their agents while only 52% were running evaluations. So most people are recording what happened and still have no systematic opinion on whether it was good.&lt;/p&gt;

&lt;p&gt;Which is also why I keep saying you cannot test an AI feature the way you test code. I went into that failure mode properly in my post on &lt;a href="https://www.swapnoneel.site/blog/testing-ai-coding-agents" rel="noopener noreferrer"&gt;testing AI coding agents&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI observability actually lives in your stack
&lt;/h2&gt;

&lt;p&gt;Two choices here, and you can do both.&lt;/p&gt;

&lt;p&gt;You can instrument your application directly, wrapping every model call in your own code. That gives you the most context, because your code knows what the user was doing. It also means every service, every language and every framework has to be instrumented separately, and someone has to keep it consistent.&lt;/p&gt;

&lt;p&gt;Or you put it in the gateway. If all your model traffic already goes through one proxy, that proxy sees every request and every response by definition, and you get telemetry for services you never touched.&lt;/p&gt;

&lt;p&gt;And the reason you can do both without doubling the work is that there is finally a shared standard. The &lt;a href="https://opentelemetry.io/blog/2026/genai-observability/" rel="noopener noreferrer"&gt;OpenTelemetry GenAI semantic conventions&lt;/a&gt; define agreed attribute names for exactly this, like &lt;code&gt;gen_ai.request.model&lt;/code&gt;, &lt;code&gt;gen_ai.usage.prompt_tokens&lt;/code&gt; and &lt;code&gt;gen_ai.usage.cost&lt;/code&gt;. Emit those and your AI spans slot into the same traces as the rest of your system, in whatever backend you already pay for.&lt;/p&gt;

&lt;p&gt;Bifrost is a reasonable thing to look at here, since it does both halves. It records inputs, outputs, tokens, cost and status for every call into SQLite or Postgres with a dashboard on top, and it exports OpenTelemetry spans using those GenAI conventions plus native Prometheus counters like &lt;code&gt;bifrost_input_tokens_total&lt;/code&gt; and &lt;code&gt;bifrost_cost_total&lt;/code&gt;. The logging runs in background goroutines, which is why &lt;a href="https://docs.getbifrost.ai/features/observability" rel="noopener noreferrer"&gt;its documentation&lt;/a&gt; puts the added overhead under 0.1ms per request.&lt;/p&gt;

&lt;p&gt;That last detail is the pattern to steal, whichever tool you end up picking. Telemetry gets emitted off the hot path, after the response is already on its way back to the user. Observability that slows down the thing it observes gets switched off within a week.&lt;/p&gt;

&lt;p&gt;The routing side of that same gateway is worth knowing about too, and I covered it in my post on &lt;a href="https://www.swapnoneel.site/blog/what-is-adaptive-load-balancing" rel="noopener noreferrer"&gt;adaptive load balancing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs you to run
&lt;/h2&gt;

&lt;p&gt;Now the uncomfortable part, because none of this is free.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78xs5rtn8pt5huvu3cyl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78xs5rtn8pt5huvu3cyl.png" alt="The storage, privacy, and attention costs of observability" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage adds up fast.&lt;/strong&gt; You are storing full prompts and full responses, and prompts got long. A retrieval app can easily push 8,000 tokens of context per call. At real traffic that is a serious volume of text, and this is where sampling comes in: keep 100% of errors and slow requests, keep a small percentage of the healthy ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your prompts contain user data.&lt;/strong&gt; Every support chat, every uploaded document, every email a user pasted in. The moment you log all of it, your observability store is now a system holding personal data, with all the retention and access rules that implies. Redact at the point of capture, not later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And someone has to actually look at it.&lt;/strong&gt; This is the one that quietly kills the whole effort. The traces get collected, the dashboard gets built, nobody opens it, and six months later it is a very expensive write-only database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is AI observability the same as LLM monitoring?&lt;/strong&gt; Close, and monitoring is the narrower one. Monitoring tracks known metrics like uptime, latency and error rate, and answers "is it working". Observability keeps enough per-request detail that you can answer questions you had not thought of yet, like "why did this one user get that answer". In practice most tools sell both under one name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need OpenTelemetry for this?&lt;/strong&gt; No, but it is the sensible default in 2026. The GenAI semantic conventions mean your AI spans use the same attribute names everywhere, so you can change vendors without reinstrumenting, and your model calls appear inside the same traces as your database queries. Note that parts of the spec are still marked experimental, so pin your versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between observability and evals?&lt;/strong&gt; Evals are the measurement, observability is the pipe. Evals score whether an output was good; observability captures the request, the context, the cost and the trace so the score has something to attach to. You can run evals offline in CI against a fixed dataset, but you can only run them on real traffic if the traffic is being recorded.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what should you actually do?
&lt;/h2&gt;

&lt;p&gt;If you have an AI feature in production right now and you cannot pull up the exact prompt, the model version and the cost of a request from last Tuesday, that is the gap, and it is worth a day of your week.&lt;/p&gt;

&lt;p&gt;Start with capture, and not with dashboards. Get every request logged with its prompt, response, model, tokens, cost and a trace ID, put it wherever you already look at data, and give it two weeks. You will find something. Everyone does. Quality scoring, drift alerts and per-feature cost budgets are worth adding later, but every one of them sits on top of the boring capture layer, so there is no point doing them first.&lt;/p&gt;

&lt;p&gt;If your model calls already go through a gateway, turn on the telemetry it ships with before you write any of this yourself. That is the single highest-value hour available to you here, and it is mostly a config change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0jqt2fv5869zcc74abon.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0jqt2fv5869zcc74abon.png" alt="Capture first, then add quality, drift, and cost controls" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is my read on it, and your setup might look nothing like mine. If you have built this kind of tracing yourself, or you have had an AI observability bill genuinely surprise you, drop it in the comments, I would like to hear how it went.&lt;/p&gt;

&lt;p&gt;You can find me on &lt;a href="https://x.com/swapnoneel123" rel="noopener noreferrer"&gt;X&lt;/a&gt; where I post about most of what I am building, and the rest of my writing lives at &lt;a href="https://www.swapnoneel.site" rel="noopener noreferrer"&gt;swapnoneel.site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>What is Adaptive Load Balancing, and Why AI Needs It</title>
      <dc:creator>Swapnoneel Saha</dc:creator>
      <pubDate>Mon, 17 Aug 2026 17:53:52 +0000</pubDate>
      <link>https://dev.to/swapnoneel123/what-is-adaptive-load-balancing-and-why-ai-needs-it-440f</link>
      <guid>https://dev.to/swapnoneel123/what-is-adaptive-load-balancing-and-why-ai-needs-it-440f</guid>
      <description>&lt;p&gt;Five identical servers sitting behind one load balancer, each getting exactly one-fifth of the requests. So why is one of them pinned at 90% CPU while another one sits half idle?&lt;/p&gt;

&lt;p&gt;Well, because an equal share of requests is not an equal share of work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adaptive load balancing&lt;/strong&gt; is a routing strategy that picks a destination using live health signals like error rate, latency and utilization, instead of a fixed rotation. The balancer keeps scoring every backend while traffic flows, shifts weight toward the ones behaving well, and pulls weight away from the ones going bad.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a load balancer?
&lt;/h2&gt;

&lt;p&gt;A load balancer is just a thing sitting in front of your servers, deciding which one gets the next request. That's it.&lt;/p&gt;

&lt;p&gt;The simplest version is round robin. Request 1 goes to server A, request 2 to server B, request 3 to server C, then back to A again. It's a rotation, and it never once looks at what is actually happening inside those servers.&lt;/p&gt;

&lt;p&gt;Static algorithms like this quietly assume two things: every request costs the same, and every server has the same capacity right now. Both assumptions survive about five minutes of real production traffic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmm73n9wcp18ckzhurdhv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmm73n9wcp18ckzhurdhv.png" alt="Equal turns can still create unequal work" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Think of a supermarket. Round robin is the sign saying "next customer to the next till, in order." Adaptive is a floor manager who watches which till is genuinely moving, spots the one stuck behind a price check, and sends people elsewhere.&lt;/p&gt;

&lt;p&gt;Same queue, very different Saturday.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does an adaptive load balancer decide where to send a request?
&lt;/h2&gt;

&lt;p&gt;It collects signals, turns them into a score per route, and turns those scores into weights.&lt;/p&gt;

&lt;p&gt;Three signals do most of the work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Error rate.&lt;/strong&gt; Is this backend returning failures? Usually the heaviest signal, and usually time-decayed, so a spike from ten minutes ago stops dominating the decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency.&lt;/strong&gt; How slow is it right now, both against its peers and against its own recent baseline? A route that always takes 2 seconds is fine. A route that usually takes 200ms and is now taking 2 seconds is in trouble.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Utilization.&lt;/strong&gt; How much of its capacity is already committed, so that no single fast route gets hammered into becoming a slow one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those collapse into one number per route, and a higher number means a bigger share of the traffic.&lt;/p&gt;

&lt;p&gt;And the weights are not recalculated per request, because that would drop real work onto the hot path. They get recalculated on a background loop, and each incoming request simply reads the numbers that were computed a moment ago.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5v9bekad6tumykkshzuf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5v9bekad6tumykkshzuf.png" alt="Live signals become route weights" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.getbifrost.ai/overview" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; is Maxim AI's high-performance, &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;open-source AI gateway&lt;/a&gt; that unifies access to 20+ providers through a single OpenAI-compatible API. That makes it a useful case study here: the gateway sees provider, model, and API-key routes from one layer, which is the full decision surface its adaptive balancer needs. The &lt;a href="https://docs.getbifrost.ai/enterprise/adaptive-load-balancing" rel="noopener noreferrer"&gt;documented implementation&lt;/a&gt; scores routes on error penalty, token-aware latency, utilization, and recovery momentum. It recalculates weights every 5 seconds, monitors both provider and key performance, and keeps route selection under 10 microseconds on the hot path.&lt;/p&gt;

&lt;p&gt;That provider-and-key view matters because a provider can remain healthy while one API key is throttled or exhausted. A balancer that only watches provider health would miss that failure and keep sending traffic to a route that cannot serve it.&lt;/p&gt;

&lt;p&gt;The other half of the mechanism is state. A good adaptive balancer doesn't just have a dial, it has an opinion about what each route currently is: healthy, degraded, failed, or recovering.&lt;/p&gt;

&lt;p&gt;And the recovering state is the one people forget. A route that failed does not get cut off forever, it gets a thin trickle of live traffic so the balancer can find out when it is better. Without that, your balancer is just a fancy circuit breaker that never closes again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adaptive load balancing vs round robin and least connections
&lt;/h2&gt;

&lt;p&gt;Each static strategy fails in a different way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Round robin&lt;/strong&gt; rotates blindly. Fine when every server and every request is genuinely identical, which is basically never.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weighted round robin&lt;/strong&gt; lets you say "server A is beefier, give it double." Better, but you set those weights by hand, based on what was true when you deployed. It has no idea what is true at 3am during a traffic spike.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Least connections&lt;/strong&gt; picks whichever server has the fewest open connections. This one is genuinely dynamic, and it is a solid default. But an open connection is a rough proxy for load, since one connection doing heavy work counts exactly the same as one connection idling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Least response time&lt;/strong&gt;, usually implemented with an exponentially weighted moving average of latency, gets close to adaptive. It measures the thing you actually care about.&lt;/p&gt;

&lt;p&gt;Adaptive load balancing is the version that stops relying on any single number. It combines errors, latency and capacity, keeps a health state per route, and has explicit behavior for pulling a route out and easing it back in.&lt;/p&gt;

&lt;p&gt;Does the extra machinery pay off? Envoy's own benchmark for its Peak EWMA policy puts it at a 99.9% success rate under a 1-second timeout, against 99% for least-loaded and 95% for round robin (&lt;a href="https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/contrib/load_balancing_policies/peak_ewma/peak_ewma" rel="noopener noreferrer"&gt;Envoy docs&lt;/a&gt;). That gap between adaptive and round robin is made entirely of user-visible failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an old routing idea matters again
&lt;/h2&gt;

&lt;p&gt;Adaptive load balancing has been used in networking gear and reverse proxies for years. For a normal web app behind NGINX, least connections is often enough.&lt;/p&gt;

&lt;p&gt;AI traffic changes that because it breaks the assumptions behind the older algorithms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why LLM traffic makes static load balancing fall apart?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqwpbhamjdib7xoqguev4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqwpbhamjdib7xoqguev4.png" alt="Large token loads expose static routing limits" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four things go wrong at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requests are wildly different sizes.&lt;/strong&gt; A "summarize this sentence" call and a "read these 40 pages and reason about them" call hit the same endpoint, and one of them costs a hundred times more. When I built &lt;a href="https://scholarian.vercel.app" rel="noopener noreferrer"&gt;Scholarian&lt;/a&gt;, a deep-research pipeline over academic papers, it ended up fetching and ranking over 10,000 papers across 250+ search sessions. Some sessions were one cheap query. Some were a long chain of very expensive ones. A rotation cannot tell those apart, so it cheerfully fires the expensive one at the route that is already drowning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The limits are not counted in requests.&lt;/strong&gt; Model providers rate-limit you on requests per minute &lt;em&gt;and&lt;/em&gt; tokens per minute, and it is usually the token ceiling you hit first. So a balancer counting requests is watching the wrong meter, and you find out about it through a 429 error in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The backends are not yours.&lt;/strong&gt; You cannot SSH into OpenAI. There is no CPU graph, no memory reading, nothing except the latency and error rate you observe from outside. Observed behavior is the only signal you have, and observed behavior is exactly what adaptive balancing runs on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every API key is its own bottleneck.&lt;/strong&gt; Rate limits are per key, so teams end up holding several keys per provider. Now you are not balancing across servers anymore, you are balancing across a grid of providers and keys, each with separate limits and separate health.&lt;/p&gt;

&lt;h2&gt;
  
  
  What adaptive load balancing looks like inside an AI gateway?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqa5gm5t6jdk0cdoixsjd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqa5gm5t6jdk0cdoixsjd.png" alt="Provider selection followed by API key selection" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The gateways handling this properly split the decision in two.&lt;/p&gt;

&lt;p&gt;The first level picks the provider and model for a request, based on live capacity and error rates across all of them. The second level picks which API key inside that provider actually gets used, weighted by how each individual key is performing.&lt;/p&gt;

&lt;p&gt;That two-level shape matters because the failures are different at each level. A provider goes down for everybody at once. A single key just quietly hits its own token ceiling while its siblings are perfectly fine. One balancer trying to handle both would be making the wrong call half the time.&lt;/p&gt;

&lt;p&gt;I went through the practical side of this in my post on &lt;a href="https://www.swapnoneel.site/blog/bifrost-for-enterprises" rel="noopener noreferrer"&gt;Bifrost's enterprise features&lt;/a&gt;, where the adaptive routing sits right next to audit logs and guardrails. Worth a read if you want the version with an actual dashboard in front of you.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should not reach for adaptive load balancing
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc7ilhdmpxvllvd27omo1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc7ilhdmpxvllvd27omo1.png" alt="Sparse signals and weak capacity call for caution" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need real telemetry before you need adaptive routing.&lt;/strong&gt; At a contract role earlier this year I built an internal tool that captured our AI product's logs and generated reports on latency and probable slowdowns, and the uncomfortable lesson was that most of the wins came from simply &lt;em&gt;seeing&lt;/em&gt; the numbers. Half the routing problems people want an adaptive balancer to solve turn out to be one bad prompt template or one undersized instance, and a dashboard finds those faster than an algorithm hides them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low traffic means no signal.&lt;/strong&gt; Scoring on error rate and latency needs enough requests per window to mean anything. At 5 requests a minute, an adaptive balancer is mostly reacting to noise, and reacting to noise is worse than not reacting at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can paper over a capacity problem.&lt;/strong&gt; If every route is degraded, adaptive balancing will smoothly and confidently send you to the least-bad option, forever, while the real answer was "add capacity" or "get off the free tier."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it is one more moving part.&lt;/strong&gt; More state, more tuning, one more thing to reason about at 3am. If round robin across two identical boxes is working for you, keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the right routing strategy
&lt;/h2&gt;

&lt;p&gt;If you run a plain web service on infrastructure you control, least connections is fine and adaptive load balancing is over-engineering.&lt;/p&gt;

&lt;p&gt;If you are routing to model providers, it is not optional anymore. You are balancing across backends you cannot inspect, with limits measured in tokens, with per-key ceilings, and with failure modes that arrive as a slow degradation instead of a clean crash. A fixed rotation has no mechanism to even notice that. Use a gateway that scores routes on live behavior and moves the traffic for you, and go spend that attention on your product instead. And also, if what Bifrost is doing intrigued you, you can easily &lt;a href="https://www.getmaxim.ai/bifrost/book-a-demo" rel="noopener noreferrer"&gt;book a demo&lt;/a&gt;, and see how it fits in your organization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx8u8niajvw5kek1yhbtm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx8u8niajvw5kek1yhbtm.png" alt="Simple services can stay simple; AI traffic needs adaptation" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is my take, and your setup might look nothing like mine. If you have built this kind of routing yourself, or you have watched an adaptive balancer make a genuinely stupid decision, drop it in the comments, I want to hear it.&lt;/p&gt;

&lt;p&gt;You can find me on &lt;a href="https://x.com/swapnoneel123" rel="noopener noreferrer"&gt;X&lt;/a&gt; where I post about most of what I am building, and the rest of my writing lives at &lt;a href="https://www.swapnoneel.site" rel="noopener noreferrer"&gt;swapnoneel.site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>webdev</category>
      <category>ai</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Bifrost for Enterprises: Audit Logs, MCP Tool Groups, Adaptive Routing, and Guardrails</title>
      <dc:creator>Swapnoneel Saha</dc:creator>
      <pubDate>Sun, 09 Aug 2026 15:41:05 +0000</pubDate>
      <link>https://dev.to/swapnoneel123/bifrost-for-enterprises-adaptive-routing-guardrails-and-much-more-4mlf</link>
      <guid>https://dev.to/swapnoneel123/bifrost-for-enterprises-adaptive-routing-guardrails-and-much-more-4mlf</guid>
      <description>&lt;p&gt;&lt;a href="https://docs.getbifrost.ai/overview" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; is Maxim AI's high-performance, &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;open-source AI gateway&lt;/a&gt; that unifies access to 20+ providers through a single OpenAI-compatible API. &lt;a href="https://docs.getbifrost.ai/enterprise/overview" rel="noopener noreferrer"&gt;Bifrost Enterprise&lt;/a&gt; is a strict superset of that gateway, adding high-availability clustering, adaptive load balancing, identity and access controls, audit logs, guardrails, and private deployment options.&lt;/p&gt;

&lt;p&gt;Those additions matter when several teams share the gateway. Routing rules, access policies, MCP tool permissions, and administrative changes can be managed and audited in one layer instead of being implemented separately in every AI application.&lt;/p&gt;

&lt;p&gt;After writing about the open-source gateway in my previous two posts, I asked the Bifrost team for access to the Enterprise version. They gave me limited access to the paid features for free. This article covers what I tested and whether those features make sense for personal and enterprise use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transparency through Audit Logs
&lt;/h2&gt;

&lt;p&gt;So, before creating any new Enterprise configuration, I wanted to see whether Bifrost could actually tell me what was happening behind the scenes. Audit Logs tell you exactly that. It’s different from the regular LLM logs because they show the requests going through the gateway, while Audit Logs focus on changes and administrative activity inside Bifrost. So if someone creates a virtual key, changes a routing rule, updates a guardrail, or modifies the cluster configuration, this is where we should be able to find it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcgnv1ejfcvkgmrohejyj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcgnv1ejfcvkgmrohejyj.png" alt="Bifrost dashboard showing the Audit Logs interface." width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a personal setup, audit logs may be more detail than you need. In a company, they give security and platform teams a record of who changed shared routing, access, and guardrail configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are MCP Tool Groups?
&lt;/h2&gt;

&lt;p&gt;In my &lt;a href="https://www.swapnoneel.site/blog/deep-dive-into-bifrost" rel="noopener noreferrer"&gt;previous blog&lt;/a&gt;, I already mentioned how I connected the MCP Gateway to OpenCode using the Virtual Key, which enabled Bifrost to expose all of my configured MCP tools through that one endpoint.&lt;/p&gt;

&lt;p&gt;But that also raised a question: do I really want every harness to have access to every tool? So, this time I decided to try MCP Tool Groups.&lt;/p&gt;

&lt;p&gt;The idea is pretty simple. We can create a group of selected MCP tools and attach that group to a virtual key. OpenCode already uses my dedicated Enterprise virtual key, so I can control the tools available to it without changing the rest of my MCP setup.&lt;/p&gt;

&lt;p&gt;In the previous blog, you might have seen that I’ve used the Context7 MCP server. It provided access to two tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;resolve-library-id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;query-docs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, while creating the tool group, I decided to drop the &lt;code&gt;query-docs&lt;/code&gt; and kept only the &lt;code&gt;resolve-library-id&lt;/code&gt; activated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fre84bfyifr07hwij3hl5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fre84bfyifr07hwij3hl5.png" alt="Bifrost dashboard showing the 'Edit Tool Group' panel" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, under Associations, I attached the group only to my opencode-enterprise virtual key. I didn't attach it to any teams, customers, providers, or other keys.&lt;/p&gt;

&lt;p&gt;The OpenCode MCP configuration itself didn't need much change. It was already pointing to Bifrost's remote MCP endpoint:&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;"mcp"&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;"bifrost"&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;"remote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://bifrost-enterprise.agitracker.io/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&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;"oauth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&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;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer {file:./bifrost-virtual-key}"&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;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;I kept the virtual key inside a separate local file, so it never had to be pasted into the configuration or committed to Git.&lt;/p&gt;

&lt;p&gt;After restarting OpenCode, I used a deliberately small prompt,&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Use the Context7 MCP to resolve the React library. Reply with only the returned library ID.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffncld2oo6svgypq11g2t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffncld2oo6svgypq11g2t.png" alt="Bifrost MCP logs dashboard showing metrics" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The request executed successfully, as you can see in the above screenshot.&lt;/p&gt;

&lt;p&gt;So, is this feature useful? The idea is excellent, especially when different coding harnesses should have access to different MCP tools. And, it gives you a central place to manage tool access instead of duplicating MCP configuration across every client.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Adaptive routing?
&lt;/h2&gt;

&lt;p&gt;The next feature I wanted to try was Adaptive Routing. The idea behind it is quite useful, especially if you are running several models, providers, or API keys through the same Bifrost gateway.&lt;/p&gt;

&lt;p&gt;Normally, requests are distributed using fixed weights. For example, if two Gemini keys have the same weight, Bifrost can send roughly half of the traffic to each one. The problem is that fixed weights do not know whether one key has become slower, started returning errors, or hit a rate limit.&lt;/p&gt;

&lt;p&gt;Adaptive Routing tries to solve that automatically.&lt;/p&gt;

&lt;p&gt;Bifrost monitors the latency, error rate, success rate, and utilization of each available route. It then recalculates their weights every few seconds. And, a healthy and faster route receives more traffic, while a failing or slow route receives less. Bifrost still sends a small amount of traffic to recovering routes so it can detect when they become healthy again.&lt;/p&gt;

&lt;p&gt;The routing happens at two levels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bifrost can select which provider should handle a model request.&lt;/li&gt;
&lt;li&gt;After selecting the provider, it can choose the best API key configured for that provider.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This makes the feature more useful for companies that maintain multiple provider accounts or keys. Instead of manually changing weights whenever a provider starts acting up, Bifrost can react to the recent performance data on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to set Guardrails?
&lt;/h2&gt;

&lt;p&gt;Provider safety controls do not cover every organization's policies. Putting custom guardrails at the gateway lets the same input and output checks run across every model behind it.&lt;/p&gt;

&lt;p&gt;This feature is meant to protect both the prompts sent to a model and the responses coming back from it. Bifrost separates the feature into two parts: rules decide when a check should run, while profiles define what kind of check should be performed. For this test, I wanted to avoid adding another external API key, so I chose Bifrost's built-in Custom Regex provider. According to the &lt;a href="https://docs.getbifrost.ai/enterprise/guardrails" rel="noopener noreferrer"&gt;Guardrails documentation&lt;/a&gt;, Custom Regex runs locally and can be used for deterministic pattern checks.&lt;/p&gt;

&lt;p&gt;So, I created two guardrail rules. One for the input, and the other one for the output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5hko4nft3m5do9q2uxxm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5hko4nft3m5do9q2uxxm.png" alt="Bifrost dashboard showing the 'Edit Guardrail Rule' sidebar interface" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, now if I send a request like &lt;code&gt;Reply with exactly: BIFROST_GUARDRAIL_TEST&lt;/code&gt;, I get a &lt;code&gt;regex pattern matched&lt;/code&gt; error. That’s where the guardrail is actually doing its job.&lt;/p&gt;

&lt;p&gt;The regex rule blocked the matching output without calling an external guardrail service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;When I started exploring Bifrost Enterprise, I expected the paid version to feel like the open-source gateway with a few extra switches. That wasn’t true.&lt;/p&gt;

&lt;p&gt;The core experience stayed familiar, and I could continue using OpenCode through one Bifrost endpoint while the gateway handled the provider connection underneath. That part was convenient. I did not need to change my workflow every time I switched between OpenAI and Gemini.&lt;/p&gt;

&lt;p&gt;The enterprise features that made the most sense to me were Audit Logs and MCP Tool Groups. Audit Logs give teams a central record of what happened, while MCP Tool Groups make it easier to control which tools a client can access.&lt;/p&gt;

&lt;p&gt;Would I use Bifrost Enterprise for my personal setup? Probably not if I only had one provider, one API key, and a handful of requests. The extra governance and operational features would be more machinery than I need.&lt;/p&gt;

&lt;p&gt;For a team running several models, provider keys, MCP clients, and internal users, the situation is different. A shared gateway, centralized logs, access controls, guardrails, and tool restrictions can remove a lot of repeated setup from individual applications. And if you think you are the right candidate, you can always &lt;a href="https://www.getmaxim.ai/bifrost/book-a-demo" rel="noopener noreferrer"&gt;book a demo&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8caibv8q641pm9okuo0d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8caibv8q641pm9okuo0d.png" alt="Please like, follow and share!" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
