<?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: Sebastian Buzdugan</title>
    <description>The latest articles on DEV Community by Sebastian Buzdugan (@sebuzdugan).</description>
    <link>https://dev.to/sebuzdugan</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%2F3992581%2Fdeb35396-1d30-427e-92df-86eeca6871a8.jpg</url>
      <title>DEV Community: Sebastian Buzdugan</title>
      <link>https://dev.to/sebuzdugan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sebuzdugan"/>
    <language>en</language>
    <item>
      <title>Governing Every LLM and MCP Call Across the Enterprise: Virtual Keys, Budgets, and Guardrails with the Bifrost AI Gateway</title>
      <dc:creator>Sebastian Buzdugan</dc:creator>
      <pubDate>Thu, 09 Jul 2026 14:25:38 +0000</pubDate>
      <link>https://dev.to/sebuzdugan/governing-every-llm-and-mcp-call-across-the-enterprise-virtual-keys-budgets-and-guardrails-with-5a9p</link>
      <guid>https://dev.to/sebuzdugan/governing-every-llm-and-mcp-call-across-the-enterprise-virtual-keys-budgets-and-guardrails-with-5a9p</guid>
      <description>&lt;p&gt;You have API keys in a vault, a budget dashboard, and a policy doc that lists which models are approved. Procurement signed off. On paper, enterprise AI is under control.&lt;/p&gt;

&lt;p&gt;So answer one question: who called which model last night, with whose budget, through which MCP tool?&lt;/p&gt;

&lt;p&gt;Nobody can say.&lt;/p&gt;

&lt;h2&gt;
  
  
  The calls nobody is counting
&lt;/h2&gt;

&lt;p&gt;Enterprise AI usage did not arrive as one system. It arrived as a hundred small ones.&lt;/p&gt;

&lt;p&gt;A data team wires GPT-5.5 into a pipeline with a key from the shared vault. A product squad ships a feature on Claude through its own SDK. An agent framework spins up MCP servers that call tools, read files, and hit internal APIs. Each of these is a real AI call leaving your infrastructure, and almost none of them pass through a single point where you could see them, price them, or stop them.&lt;/p&gt;

&lt;p&gt;The result is not a security hole in the dramatic sense. It is worse: it is an accounting gap. You cannot answer basic questions. Which teams are spending what. Which models are actually in use versus approved. Which MCP tools an agent is allowed to invoke, and which it quietly invokes anyway.&lt;/p&gt;

&lt;p&gt;Most teams try to close this with process. A spreadsheet of keys. A wiki page of rules. A quarterly review. Process does not enforce anything at request time, and request time is the only time that matters.&lt;/p&gt;

&lt;p&gt;The fix is not another policy. It is a control plane that every LLM and MCP call has to pass through.&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%2F6g64wj0orlbsi8nr6ffq.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%2F6g64wj0orlbsi8nr6ffq.png" alt="One base URL in front of every model" width="799" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One base URL in front of every model&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Bifrost is an open-source AI gateway built by Maxim AI, written in Go and licensed Apache 2.0. It sits in front of your model providers and gives you one OpenAI-compatible API for all of them: over 1000 models across every major provider behind a single endpoint.&lt;/p&gt;

&lt;p&gt;The adoption cost is deliberately close to zero. You do not rewrite call sites. You change a base URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before: every app talks straight to the provider
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.openai.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After: the same call, now governed, logged, and routable
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080/openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line is the whole point. The SDK, the model names, the request bodies all stay the same. What changes is that the call now lands on something you control before it reaches OpenAI, Anthropic, Bedrock, Vertex, or any of the others.&lt;/p&gt;

&lt;p&gt;The gateway is not a toll booth that slows everything down. You are buying governance, not latency: in Bifrost's own t3.xlarge benchmark, that control layer cost 11 microseconds of added overhead per request at 5,000 requests per second, and held a 100 percent success rate.&lt;/p&gt;

&lt;p&gt;Standing it up locally is a single 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;That pulls the binary, runs migrations, and serves a web UI plus the API on port 8080. I ran exactly this while writing, and everything below is what the running gateway actually did, not a paraphrase of the docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Governance that follows a hierarchy, not a spreadsheet
&lt;/h2&gt;

&lt;p&gt;Once traffic flows through one place, governance stops being a document and becomes a data model. In Bifrost that model has four levels: Customer, Team, Virtual Key, and Provider Config.&lt;/p&gt;

&lt;p&gt;The Virtual Key is the unit you hand out. It is not a raw provider key. It is a governed credential with a &lt;code&gt;sk-bf-&lt;/code&gt; prefix that carries its own budget, rate limits, and allow-lists. I created one through the API and it came back live:&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;-X&lt;/span&gt; POST localhost:8080/api/governance/virtual-keys &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"marketing-team-vk","is_active":true,
       "rate_limit":{"token_max_limit":1000000,"token_reset_duration":"1h",
                     "request_max_limit":1000,"request_reset_duration":"1h"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response handed back &lt;code&gt;sk-bf-82a7fb23...&lt;/code&gt; with a rate-limit object already ticking: &lt;code&gt;token_current_usage: 0&lt;/code&gt;, an hourly reset window, request counters primed. That key is the thing a team codes against. Everything you attach to it travels with every call it makes.&lt;/p&gt;

&lt;p&gt;Budgets cascade. A Provider Config can cap the OpenAI spend inside a key, the key has its own ceiling, the team above it has another, and every applicable budget has to pass for the request to proceed. Cost deducts from each level at once. Budgets reset on windows from one minute to one year, and &lt;code&gt;calendar_aligned&lt;/code&gt; snaps them to real month and year boundaries instead of rolling clocks. Rate limits come in two flavors, request-based and token-based, and unlike budgets they apply at the Virtual Key level only.&lt;/p&gt;

&lt;p&gt;Access control is enforcement, not advice. Put a model or provider outside a key's allow-list and the call returns a 403 with a typed reason, &lt;code&gt;model_blocked&lt;/code&gt; or &lt;code&gt;provider_blocked&lt;/code&gt;. There is no soft warning to ignore.&lt;/p&gt;

&lt;p&gt;One gotcha worth knowing before you wire this into config: a budget attaches as its own referenced object, not as an inline block on the key. I tried to embed the budget directly in the create call and it came back unattached. Create the budget, reference it by id. The docs are right; the shortcut 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%2F3wjo2duv0lcdgxpyxbcs.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%2F3wjo2duv0lcdgxpyxbcs.png" alt="Governing every MCP call, not just every model call" width="799" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Governing every MCP call, not just every model call&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Chat completions were the easy half. The harder half, and the reason this topic exists, is MCP.&lt;/p&gt;

&lt;p&gt;Model Context Protocol is what turns a chat model into something that acts: it calls tools, runs commands, reads and writes real systems. An ungoverned MCP layer is an agent with hands and no supervisor. Bifrost treats MCP as a first-class governed surface, and it does so with tool filtering that is deny-by-default at three levels.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client config sets the baseline of which tools exist at all.&lt;/li&gt;
&lt;li&gt;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 per call.&lt;/li&gt;
&lt;li&gt;Virtual Key filtering overrides both, so a key with no MCP config gets no MCP tools, full stop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Deny-by-default is the whole posture. A tool a key was never granted is not available to it, even if the underlying MCP server exposes it.&lt;/p&gt;

&lt;p&gt;How the calls actually run is configurable, and the default is the safe one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tool Execution mode is human-in-the-loop. Bifrost does not auto-run tool calls; your app reviews them and explicitly hits &lt;code&gt;/v1/mcp/tool/execute&lt;/code&gt;. That review point is where approval and audit live.&lt;/li&gt;
&lt;li&gt;Agent Mode auto-executes only the tools you mark in &lt;code&gt;tools_to_auto_execute&lt;/code&gt;, bounded by &lt;code&gt;max_agent_depth&lt;/code&gt; (default 10).&lt;/li&gt;
&lt;li&gt;Code Mode lets the model write Starlark to orchestrate many tools in one shot through four meta-tools, which Bifrost measures at up to 92.8 percent fewer input tokens than exposing every tool directly.&lt;/li&gt;
&lt;li&gt;Bifrost as an MCP gateway exposes a &lt;code&gt;/mcp&lt;/code&gt; endpoint that external hosts like Claude Desktop, Cursor, and Cline connect to. On the running instance that endpoint answered as a live streaming connection, held open the way an MCP host expects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For teams that want reusable policy, MCP Tool Groups (an Enterprise feature) bundle curated tools and attach them to keys, teams, or users, enforced at request time.&lt;/p&gt;

&lt;p&gt;The one-sentence version: the gateway governs what an agent is allowed to do, not just which model it is allowed to call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can actually see
&lt;/h2&gt;

&lt;p&gt;Governance you cannot observe is governance you cannot prove. Bifrost logs every request asynchronously, so the visibility does not cost latency.&lt;/p&gt;

&lt;p&gt;Each entry carries the model, token counts, cost, and the identity behind the call. When a request fails over or rotates keys, an &lt;code&gt;attempt_trail&lt;/code&gt; records every attempt, the &lt;code&gt;selected_key_id&lt;/code&gt;, and the retry count, so a weird bill or a slow p99 has a paper trail instead of a shrug.&lt;/p&gt;

&lt;p&gt;The exports are the standard enterprise set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prometheus metrics are built in on &lt;code&gt;/metrics&lt;/code&gt;, live out of the box (on a fresh instance you will see the runtime metrics first; the request metrics fill in as traffic flows).&lt;/li&gt;
&lt;li&gt;OpenTelemetry export speaks the &lt;code&gt;genai_extension&lt;/code&gt; semantic conventions to Grafana, Datadog, New Relic, or Honeycomb.&lt;/li&gt;
&lt;li&gt;A native Datadog connector and log exports to S3, GCS, and BigQuery are available on Enterprise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If content is sensitive, &lt;code&gt;disable_content_logging&lt;/code&gt; keeps the metadata, latency, cost, tokens, and identity, while dropping the request and response bodies. You get the accounting without hoarding the payloads.&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%2Fgjfdpthdamzxcew5k9s2.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%2Fgjfdpthdamzxcew5k9s2.png" alt="The controls that make security sign off" width="799" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The controls that make security sign off&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Everything so far runs in the open-source gateway. The pieces a security team asks about before rollout live in Bifrost Enterprise, which is a strict superset: same config schema, same providers, more control.&lt;/p&gt;

&lt;p&gt;Identity comes through OIDC and SCIM, with SSO against Okta, Microsoft Entra, Keycloak, Zitadel, and Google Workspace, plus group-to-role mapping and auto-deprovisioning. Access is role-based across Admin, Developer, and Viewer, the permission set widening at each tier instead of collapsing into a single all-or-nothing admin. Data Access Control adds row-level scoping, so an operator sees only their own, their team's, or all data.&lt;/p&gt;

&lt;p&gt;Audit logs are immutable and HMAC-signed, exportable as JSON, JSON Lines, or Syslog straight into a SIEM. That is what SOC 2 Type 2, GDPR, HIPAA, and ISO 27001 evidence actually looks like.&lt;/p&gt;

&lt;p&gt;Guardrails split into Rules, written in CEL, and Profiles that decide how to act: native Gitleaks-backed secret detection and PII regex, plus external providers including AWS Bedrock, Azure Content Safety, Google Model Armor, CrowdStrike AIDR, GraySwan Cygnal, and Patronus AI. For scale, clustering keeps state in sync across nodes with no single leader.&lt;/p&gt;

&lt;p&gt;This is the layer that turns "we route through a gateway" into "we can show an auditor who did what, signed and exportable straight into the SIEM."&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending the gateway to the laptop
&lt;/h2&gt;

&lt;p&gt;The gateway governs every call that routes through it. The last mile is reach. Think of the app a developer installed this morning that talks straight to a provider from a laptop, never pointed at your gateway. Getting the same governance onto that machine is the other half of the job.&lt;/p&gt;

&lt;p&gt;That is where Bifrost Edge and the gateway work together. Edge is a lightweight endpoint agent for macOS, Windows, and Linux that runs in the menu bar or system tray and intercepts AI traffic at the machine level. There is no base URL to change per app and nothing for the user to configure. Once a machine signs in through your existing identity provider, the same virtual keys, budgets, guardrails, and audit logs you already run on the gateway apply on the laptop too.&lt;/p&gt;

&lt;p&gt;Edge adds two enforcement surfaces the endpoint needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App allow and deny, fleet-wide. An approved app runs fully governed; a denied one is blocked before any data leaves the machine.&lt;/li&gt;
&lt;li&gt;MCP server governance on the device. Edge discovers the MCP servers configured inside Claude Code, Claude Desktop, Gemini CLI, OpenCode, Codex, and Cursor, dedupes them across the fleet, and lets you allow or deny each one centrally. Deny a server and it is stopped on the machine, even inside an app that had it configured before your policy existed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rollout is hands-off, through the MDM you already run: Jamf, Intune, and Kandji, with Omnissa Workspace ONE and JumpCloud in the mix as well. Admins work from an Approvals dashboard (Pending, Approved, Denied) and a Devices fleet view. The gateway centralizes the policy; Edge carries it to every machine.&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%2F1ojaqo9q8hfwu0ysun71.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%2F1ojaqo9q8hfwu0ysun71.png" alt="Bifrost Edge brings the same policy to every machine" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What isn't ready yet
&lt;/h2&gt;

&lt;p&gt;Edge is not something you can turn on this afternoon.&lt;/p&gt;

&lt;p&gt;It is in limited alpha, or early access, and you register to be onboarded rather than downloading a shipping release. It is an Enterprise feature, not part of the open-source repo, so you will not find it in the Bifrost you clone from GitHub. That status is current as of July 2026; check the docs before you quote a timeline, because alpha windows move.&lt;/p&gt;

&lt;p&gt;It is also not a replacement for your endpoint or network security. Edge governs AI traffic specifically. It runs alongside tools like CrowdStrike, Zscaler, and Tailscale, not instead of them.&lt;/p&gt;

&lt;p&gt;None of that changes the part you can use today. The open-source gateway is real, it installs in one command, and every governance, MCP, and observability capability above ran on the instance I stood up while writing this. The endpoint layer is where the story is still being finished.&lt;/p&gt;

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

&lt;p&gt;Start with the gateway, because it is the part you can run today. Install it with &lt;code&gt;npx -y @maximhq/bifrost&lt;/code&gt;, point one base URL at it, and route your team's traffic through it. Then spend an afternoon on the governance: issue a virtual key per team, attach budgets and rate limits, turn on audit logging. At that point every LLM and MCP call your infrastructure makes is attributed, bounded, and logged, and none of it required rewriting a call site.&lt;/p&gt;

&lt;p&gt;The endpoint is the next problem, not the first one. When you are ready to bring the laptops under the same policy, Bifrost Edge is the extension that gets you there, on the timeline the alpha allows.&lt;/p&gt;

&lt;p&gt;Every technical detail here is drawn from the Bifrost gateway and Edge documentation and confirmed against a running instance. If something in this piece does not match what you see in the docs, trust the docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources &amp;amp; References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Bifrost gateway documentation&lt;/li&gt;
&lt;li&gt;Bifrost Edge documentation&lt;/li&gt;
&lt;li&gt;Bifrost on GitHub (open source)&lt;/li&gt;
&lt;li&gt;Register for Bifrost Edge alpha&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://medium.com/data-science-collective/governing-every-llm-and-mcp-call-across-the-enterprise-virtual-keys-budgets-and-guardrails-with-b16514a4c224" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>mcp</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>How to Fine-Tune a 7B Model for Three Dollars on One GPU</title>
      <dc:creator>Sebastian Buzdugan</dc:creator>
      <pubDate>Sun, 05 Jul 2026 11:43:18 +0000</pubDate>
      <link>https://dev.to/sebuzdugan/how-to-fine-tune-a-7b-model-for-three-dollars-on-one-gpu-2lf2</link>
      <guid>https://dev.to/sebuzdugan/how-to-fine-tune-a-7b-model-for-three-dollars-on-one-gpu-2lf2</guid>
      <description>&lt;p&gt;You think fine-tuning a 7B model needs a rack of A100s. It needs one 16GB card and about three dollars of rented compute. The gap between those two beliefs is costing teams entire projects they never start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cluster you think you need does not exist
&lt;/h2&gt;

&lt;p&gt;Ask most engineers what it takes to fine-tune an open model and you get some version of "we don't have the hardware." They picture full fine-tuning: every weight in fp16, gradients and optimizer states for every parameter, a 7B model ballooning past 100GB of VRAM before the first batch even lands.&lt;/p&gt;

&lt;p&gt;That math is real, and it is why the instinct is to give up and go back to prompt engineering a frontier API.&lt;/p&gt;

&lt;p&gt;But you are solving a problem that was fixed in 2023. Full fine-tuning is not the only option, and for the overwhelming majority of practical tasks it is the wrong one. QLoRA, introduced by Tim Dettmers and collaborators, dropped the memory floor so far that a 65B model fits on a single 48GB GPU while matching full 16-bit fine-tuning quality.&lt;/p&gt;

&lt;p&gt;Scale that down to the sizes you actually ship. A 7B fine-tune runs comfortably on a 16GB card. A 13B fits on a 24GB desktop 4090. You are not renting a cluster. You are renting one GPU for an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why four bits is enough, and where the memory actually goes
&lt;/h2&gt;

&lt;p&gt;Full fine-tuning spends VRAM on four things: the model weights, the gradients, the optimizer states, and the activations. QLoRA attacks the first three at once.&lt;/p&gt;

&lt;p&gt;The trick is a clean split. The base model is frozen and stored in 4-bit, using a data type called NF4 (4-bit NormalFloat) that is information-theoretically optimal for the normally-distributed weights neural networks actually have. That 4-bit form is storage only. For the forward and backward pass, each block is de-quantized back to bf16 on the fly, so the math stays high-precision while the resting footprint drops roughly 4x.&lt;/p&gt;

&lt;p&gt;You never compute gradients for those frozen weights. Instead you train small low-rank adapter matrices bolted onto each linear layer, typically well under one percent of the model's parameters. Optimizer state, the silent VRAM killer in full fine-tuning, now covers only those tiny adapters.&lt;/p&gt;

&lt;p&gt;Two smaller innovations close the gap. Double quantization quantizes the quantization constants themselves, saving about 0.37 bits per parameter, roughly 3GB on a 65B model. Paged optimizers use NVIDIA unified memory to absorb the gradient-checkpointing spikes that would otherwise OOM you on a long sequence.&lt;/p&gt;

&lt;p&gt;The result is not a compromise you tolerate. On the paper's own MMLU evaluation, 4-bit NF4 with double quantization replicated 16-bit LoRA performance across LLaMA 7B through 65B. Independent 2026 runs put QLoRA within one to two percent of full-precision LoRA on standard benchmarks. You give up almost nothing measurable.&lt;/p&gt;

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

&lt;p&gt;Here is the part that turns this from theory into a Tuesday afternoon. These are the realistic 4-bit VRAM footprints for a QLoRA fine-tune, base model plus adapters and activations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;7B: comfortable on a 16GB card&lt;/li&gt;
&lt;li&gt;13B: fits on a 24GB card (a desktop RTX 4090)&lt;/li&gt;
&lt;li&gt;30B to 32B: needs roughly a 40GB card&lt;/li&gt;
&lt;li&gt;70B: about 46GB, which fits on a single 48GB card&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sequence length and batch size move these numbers, but not as much as you would fear. An 8B model at a 2048-token sequence length peaks around 6.6GB of reserved VRAM on a 4090 with Unsloth. You have headroom on hardware you may already own.&lt;/p&gt;

&lt;p&gt;Hold that against full fine-tuning of the same 7B model. Weights, gradients, and Adam's two optimizer states in fp16 push you well past 100GB before activations, which means multiple 80GB A100s and a multi-GPU setup to coordinate. QLoRA takes that same job from a cluster you have to requisition down to a single card you can rent by the second. The 4x drop from 4-bit storage is only part of it; freezing the base and training sub-one-percent adapters is what deletes the optimizer-state mountain entirely.&lt;/p&gt;

&lt;p&gt;Now put a clock and a price on it. A 7B QLoRA run of two to three epochs takes roughly two to four hours on an A100 and six to eight hours on an RTX 4090. On RunPod, a Community Cloud 4090 rents from about 0.34 USD per hour and an A100 PCIe from about 1.39 USD per hour, billed by the second.&lt;/p&gt;

&lt;p&gt;Do the arithmetic. A full 7B fine-tune on a 4090 lands around two to three dollars. On an A100 you trade money for wall-clock and finish before lunch for roughly the same total. This is the number that should reframe your roadmap: a custom model is a coffee-run expense, not a capital request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recipe that works on the first try
&lt;/h2&gt;

&lt;p&gt;The tooling collapsed into something you can hold in your head. bitsandbytes handles the 4-bit quantization, PEFT handles the adapters, and Unsloth wraps both with kernels that cut memory and roughly double throughput. Start by loading the base model in 4-bit.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FastLanguageModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta-llama/Llama-3.1-8B&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_seq_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;load_in_4bit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you would rather stay in plain Transformers, the equivalent is a BitsAndBytesConfig that encodes the same three ideas: NF4 storage, bf16 compute, double quantization on.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;bnb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BitsAndBytesConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;load_in_4bit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bnb_4bit_quant_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nf4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bnb_4bit_compute_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bnb_4bit_use_double_quant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, attach the adapters. The single most important choice is targeting all the linear layers, not just the attention projections. Skipping the MLP layers is the most common reason a fine-tune underperforms for no obvious reason.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FastLanguageModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_peft_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lora_alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;target_modules&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;q_proj&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;k_proj&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;v_proj&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;o_proj&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;gate_proj&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;up_proj&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;down_proj&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;use_gradient_checkpointing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unsloth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lora_dropout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3407&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two hyperparameters carry the load. Rank r controls adapter capacity; 16 is a strong default, and 32 if the task is genuinely complex. For alpha, the old alpha = 2 * r convention still works, but Unsloth's 2026 ablations found alpha = r is the cleaner default. Gradient checkpointing set to "unsloth" buys you another 30 percent memory reduction, which is often the difference between fitting and not.&lt;/p&gt;

&lt;p&gt;Then the training arguments. Prefer a small batch size with gradient accumulation rather than a large batch, because that is how you avoid out-of-memory errors while keeping an effective batch size in the healthy 4 to 16 range.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SFTConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;per_device_train_batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;gradient_accumulation_steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;learning_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2e-4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;num_train_epochs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;warmup_ratio&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;weight_decay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3407&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That configuration is not a starting guess to tune for a week. It is close to the endpoint for most supervised fine-tuning jobs. Learning rate 2e-4, one to three epochs, effective batch 16, warmup around five percent. Change the data, not the knobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  From a trained adapter to a model you can serve
&lt;/h2&gt;

&lt;p&gt;What you get out of a QLoRA run is not a fresh multi-gigabyte model. It is an adapter: a small set of low-rank matrices, often around 100MB, that sits on top of the frozen base. That size is a feature. You can train a dozen task-specific adapters against one base model and store them all for less than the footprint of a single full checkpoint.&lt;/p&gt;

&lt;p&gt;You have two ways to serve it. Keep the adapter separate and load it over the base at runtime, which lets you hot-swap behaviors, or merge it into the weights for a single self-contained model. For most deployments, merge and export.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_pretrained_merged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama-8b-support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="n"&gt;save_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merged_16bit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_pretrained_gguf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama-8b-support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="n"&gt;quantization_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q4_k_m&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 merged 16-bit version drops straight into vLLM or TGI for a production endpoint. The GGUF export, quantized to something like q4_k_m, runs on Ollama or llama.cpp on a laptop with no GPU at all. The same afternoon that produced your adapter can end with the model answering requests on your own machine.&lt;/p&gt;

&lt;p&gt;One caution on inference: if you serve the base in 4-bit to save memory, evaluate in that exact configuration. Quality measured on a 16-bit merge does not always transfer cleanly to a 4-bit serving path, and the gap is easiest to catch before you ship, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a fine-tune is the wrong answer
&lt;/h2&gt;

&lt;p&gt;The fastest way to waste your three dollars is to fine-tune a problem that RAG should own. In 2026 the question is no longer "RAG or fine-tuning," it is where each piece of intelligence should live: in the weights, in retrieval, or in both.&lt;/p&gt;

&lt;p&gt;Reach for retrieval when the knowledge changes often, is large, needs citations, or differs per user. Facts, documents, prices, anything you would be embarrassed to see go stale inside a model, belong in a vector store you can update without retraining. Fine-tuning bakes knowledge in at a moment in time; that is a liability for anything dynamic.&lt;/p&gt;

&lt;p&gt;Reach for a fine-tune when you need consistent behavior that prompting cannot reliably enforce: a fixed output format, a specific tone or policy, domain reasoning, or structured output that has to be right every time. It also wins on economics when volume is high enough that a small fine-tuned model beats per-call frontier pricing, or when your latency budget cannot afford a retrieval hop.&lt;/p&gt;

&lt;p&gt;The mature pattern is both. Fine-tune for style, format, and decision behavior; retrieve for the facts. Teams that pick a side out of principle usually ship a worse product than teams that route each concern to the tool built for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure modes nobody warns you about
&lt;/h2&gt;

&lt;p&gt;QLoRA is forgiving, but three things quietly ruin runs.&lt;/p&gt;

&lt;p&gt;Overfitting is the big one, and it hides in a metric that looks like success. If your training loss drops below 0.2, the model has likely memorized your data and will generalize worse, not better. When you see that, cut epochs, raise weight decay, or scale alpha down by half. More than three epochs almost always trades generalization for a prettier loss curve.&lt;/p&gt;

&lt;p&gt;The second is trusting the loss curve at all. A falling loss is not a working model. You need a held-out eval that reflects the actual task, checked before and after, or you are flying blind. Domain-specific evaluation matters far more than any general benchmark score your base model advertises.&lt;/p&gt;

&lt;p&gt;The third is a subtle capacity mismatch. If a fine-tune underperforms and your data is clean, the culprit is usually rank set too low or target modules set too narrow, not the method failing. Bump r to 32 and confirm you are targeting every linear layer before you conclude QLoRA cannot learn your task.&lt;/p&gt;

&lt;p&gt;One optional upgrade is worth knowing: DoRA, weight-decomposed low-rank adaptation, often squeezes out a bit more accuracy at low ranks for a small speed cost. Try it once the plain recipe works, not before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The reason to internalize this is not that fine-tuning is trendy. It is that the cost of trying just fell through the floor, and cost of trying is what governs how much you experiment.&lt;/p&gt;

&lt;p&gt;When a custom 7B model costs three dollars and one afternoon, fine-tuning stops being a quarterly initiative that needs sign-off and becomes something you do on a hunch, twice, before standup. That changes which ideas are worth testing. The team that ships ten cheap fine-tunes and keeps the two that work will out-iterate the team still waiting on a GPU budget.&lt;/p&gt;

&lt;p&gt;Pick one narrow, format-heavy task your frontier API keeps getting subtly wrong. Rent a 4090 for an evening. The worst case is you spent the price of a coffee to learn something concrete about your own data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources &amp;amp; References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;QLoRA: Efficient Finetuning of Quantized LLMs (Dettmers et al., arXiv)&lt;/li&gt;
&lt;li&gt;LoRA Fine-Tuning Hyperparameters Guide (Unsloth Documentation)&lt;/li&gt;
&lt;li&gt;Making LLMs Even More Accessible with 4-bit Quantization and bitsandbytes (Hugging Face)&lt;/li&gt;
&lt;li&gt;GPU VRAM Requirements to Fine-Tune LLMs in 2026: Full, LoRA, and QLoRA Sizing (Spheron)&lt;/li&gt;
&lt;li&gt;RAG vs Fine-Tuning for LLMs in 2026: What Actually Works in Production&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://medium.com/@sebuzdugan/how-to-fine-tune-a-7b-model-for-three-dollars-on-one-gpu-432eb04ba010" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>finetuning</category>
    </item>
    <item>
      <title>Snapshot Once, Rollout a Thousand Times: A Practical RL Setup for Coding Agents</title>
      <dc:creator>Sebastian Buzdugan</dc:creator>
      <pubDate>Wed, 01 Jul 2026 09:09:10 +0000</pubDate>
      <link>https://dev.to/sebuzdugan/snapshot-once-rollout-a-thousand-times-a-practical-rl-setup-for-coding-agents-3fii</link>
      <guid>https://dev.to/sebuzdugan/snapshot-once-rollout-a-thousand-times-a-practical-rl-setup-for-coding-agents-3fii</guid>
      <description>&lt;p&gt;Your RL run has been going for six hours. The GPUs are warm, the reward curve is creeping up, the policy is learning. Good.&lt;/p&gt;

&lt;p&gt;Now look at what those six hours actually bought you. For a large slice of them, your expensive accelerators sat idle, not waiting on the optimizer, not waiting on the model, but waiting on an environment to boot. Clone the repo. Install the dependencies. Restore the dataset. Then, finally, let the policy take its first action.&lt;/p&gt;

&lt;p&gt;This is the part nobody puts in the RL paper. The reward function gets a section. The KL penalty gets a section. The thing that actually ate your wall-clock, replicating the environment for every single rollout, gets nothing, because it feels like plumbing.&lt;/p&gt;

&lt;p&gt;It isn't plumbing. For agent RL, it's the bottleneck. And it's a bottleneck with a clean fix that almost nobody is using yet.&lt;/p&gt;

&lt;p&gt;I spent the last few days testing that fix on real infrastructure. This piece is what I found: the primitive, a working rollout harness you can run yourself, the real timings, and the places it doesn't help so you know where the edges are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rollout is a state-replication problem wearing a compute costume
&lt;/h2&gt;

&lt;p&gt;Strip an RL step down to its skeleton:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s0  -&amp;gt;  policy samples an action  -&amp;gt;  env transitions  -&amp;gt;  ...  -&amp;gt;  terminal  -&amp;gt;  reward
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a math problem, &lt;code&gt;s0&lt;/code&gt; is a string. For a coding agent, &lt;code&gt;s0&lt;/code&gt; is a world: this repo at this commit, this Python version, these installed packages, this dataset on disk, this half-built state from a setup script. Reconstructing that world is the price of admission for every rollout you run.&lt;/p&gt;

&lt;p&gt;And in GRPO, GSPO, or any group-based method, you don't run one rollout per step. You run G of them, eight, sixteen, sixty-four completions, each needing its own clean copy of &lt;code&gt;s0&lt;/code&gt;. Multiply by thousands of steps. The environment gets rebuilt tens of thousands of times across a single run.&lt;/p&gt;

&lt;p&gt;There are three ways teams handle this today, and all three are workarounds for the same underlying problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Containers per rollout.&lt;/strong&gt; Spin up a fresh container, pull the image, install, run, tear down. Clean isolation, but you pay the cold-start-plus-setup tax on every single rollout. Tens of seconds, every time, before the policy moves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warm pools.&lt;/strong&gt; Keep a fleet of pre-built containers ready. Faster, but now you own pool drift, eviction logic, health checks, and the ops surface of a small distributed system. You traded a latency problem for an infrastructure problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One VM, sequential rollouts.&lt;/strong&gt; Cheap and simple, until you realize rollout three poisoned the filesystem that rollout four needed, and you've serialized the one thing you most wanted to parallelize.&lt;/p&gt;

&lt;p&gt;Every one of these is dancing around a single fact: replicating environment state is expensive, so we keep paying for it or building scaffolding to avoid paying for it. What if replicating state was just cheap?&lt;/p&gt;

&lt;h2&gt;
  
  
  The primitive: snapshot once, restore N
&lt;/h2&gt;

&lt;p&gt;Here's the move. Instead of rebuilding &lt;code&gt;s0&lt;/code&gt; for every rollout, you build it once, freeze it, and hand out identical copies.&lt;/p&gt;

&lt;p&gt;The substrate is a microVM sandbox. It boots in well under a second, and you can snapshot its entire state and restore that snapshot into fresh, independent VMs. I tested this on Tensorlake sandboxes, which is what the code below uses, but the pattern is the point. The setup is unremarkable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorlake.sandbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CheckpointType&lt;/span&gt;

&lt;span class="c1"&gt;# Build the world ONCE.
&lt;/span&gt;&lt;span class="n"&gt;sb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&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;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rl-canonical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-lc&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;mkdir -p /home/tl-user/proj&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/tl-user/proj/impl.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buggy_module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/tl-user/proj/test_impl.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hidden_tests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-lc&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;cd /home/tl-user/proj &amp;amp;&amp;amp; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pip install --break-system-packages -q numpy pandas requests pytest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Freeze it.
&lt;/span&gt;&lt;span class="n"&gt;snap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpoint_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CheckpointType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FILESYSTEM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;snap&lt;/code&gt; is &lt;code&gt;s0&lt;/code&gt;. Restoring it gives you a fresh, byte-identical world to roll out in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fork&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&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;snapshot_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# a clean copy of s0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before we go further, the obvious objection, because it's the right one to ask:&lt;/p&gt;

&lt;h2&gt;
  
  
  "Why not just bake a Docker image?"
&lt;/h2&gt;

&lt;p&gt;Because an image and a snapshot are not the same kind of thing, and the difference is exactly what makes this work.&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%2F1bgjs6jvpiit9rzh4i25.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%2F1bgjs6jvpiit9rzh4i25.png" alt="An image distributes an environment. A snapshot captures runtime state." width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An image distributes an environment. A snapshot captures runtime state.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An image is a recipe for building an environment: the repo, the language, the dependencies. Great for distribution. But it's frozen at build time. It knows nothing about what happened after the container started: the files your setup script generated, the dataset you downloaded, the checkpoint your last run wrote, the config a previous step mutated.&lt;/p&gt;

&lt;p&gt;A snapshot is taken at runtime. It captures the world as it actually is, mid-flight, including everything an image structurally cannot.&lt;/p&gt;

&lt;p&gt;I tested this directly. I built a sandbox the way a real session would: installed deps, downloaded a file, generated an &lt;code&gt;embeddings.npy&lt;/code&gt; from running code, wrote a checkpoint directory, and mutated a config. Then I snapshotted it and restored into a fresh sandbox. What survived:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;requests + numpy importable&lt;/span&gt;
&lt;span class="na"&gt;download&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;downloaded_readme.md present&lt;/span&gt;
&lt;span class="na"&gt;generated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;embeddings.npy  (1000, 128)&lt;/span&gt;
&lt;span class="na"&gt;checkpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;step=4200 loss=0.13&lt;/span&gt;
&lt;span class="na"&gt;run_state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;resume_from=4200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Dockerfile gives you the first line. The other four, the runtime state that's often the entire point of &lt;code&gt;s0&lt;/code&gt;, exist only because the snapshot captured them. That's the distinction in one sentence: an image distributes an environment; a snapshot captures runtime state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rollout harness you can actually run
&lt;/h2&gt;

&lt;p&gt;Let's make it concrete. The toy task: an agent has to fix a buggy Python module so a hidden &lt;code&gt;pytest&lt;/code&gt; suite passes. Small enough to fit in an article, structurally identical to anything real you'd train on. The reward is the fraction of tests that pass: clean, dense enough to learn from, impossible to game.&lt;/p&gt;

&lt;p&gt;The starting state &lt;code&gt;s0&lt;/code&gt; is the module-with-bugs plus the installed environment, snapshotted once (the code above). Now the rollout step: for each of G policy samples, restore &lt;code&gt;s0&lt;/code&gt;, apply the candidate, score it.&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;concurrent.futures&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorlake.sandbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rollout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidate_patch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Each rollout gets its own clean copy of s0.
&lt;/span&gt;    &lt;span class="n"&gt;fork&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&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;snapshot_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-lc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cd /home/tl-user/proj &amp;amp;&amp;amp; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;candidate_patch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-lc&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;cd /home/tl-user/proj &amp;amp;&amp;amp; python3 -m pytest -q 2&amp;gt;&amp;amp;1 | tail -1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pass_fraction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# 0.0 .. 1.0
&lt;/span&gt;    &lt;span class="n"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&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;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reward&lt;/span&gt;

&lt;span class="c1"&gt;# Fan out G rollouts from the one snapshot, in parallel.
&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;rollout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a real loop, &lt;code&gt;candidates&lt;/code&gt; comes from your policy. Here I scripted eight candidate patches of varying quality so the whole thing is deterministic and you can reproduce it without a model or an API key. The point isn't the policy, it's what the sandbox does underneath it.&lt;/p&gt;

&lt;p&gt;Here's the actual output from my run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rollout[0] reward=0.00   (3 failed)
rollout[1] reward=0.33   (2 failed, 1 passed)
rollout[2] reward=0.33   (2 failed, 1 passed)
rollout[3] reward=0.67   (1 failed, 2 passed)
rollout[4] reward=0.33   (2 failed, 1 passed)
rollout[5] reward=0.33   (2 failed, 1 passed)
rollout[6] reward=0.33   (2 failed, 1 passed)
rollout[7] reward=0.67   (1 failed, 2 passed)
group mean reward = 0.375     best = 0.67
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That vector of rewards is the whole game. And here's the thing worth internalizing: getting G independent rollouts from one identical starting state is the expensive part, and it's identical across methods. What you do with the rewards afterward is where the techniques split. Rejection-sampling fine-tuning keeps the best completion and trains on it; GRPO and GSPO use the entire group, computing each rollout's advantage as its reward minus the group mean, and nudge the policy toward the above-average ones. Same expensive primitive underneath. The snapshot is what makes that primitive cheap.&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%2Fk83t3t0z8osrq4eco419.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%2Fk83t3t0z8osrq4eco419.png" alt="One snapshot, G rollouts, one reward vector. Rejection sampling keeps the best; GRPO uses the whole group. The fan-out is the same either way." width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One snapshot, G rollouts, one reward vector. Rejection sampling keeps the best; GRPO uses the whole group. The fan-out is the same either way.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The economics, which is the actual argument
&lt;/h2&gt;

&lt;p&gt;Now the part that matters. Let's price the two approaches on the same workload, and let's be fair about it: both of them spin up a fresh sandbox per rollout, so both pay that cost. The only thing that differs is the build.&lt;/p&gt;

&lt;p&gt;Two numbers from my runs. Building the world (installing &lt;code&gt;numpy&lt;/code&gt;, &lt;code&gt;pandas&lt;/code&gt;, &lt;code&gt;requests&lt;/code&gt;, and &lt;code&gt;pytest&lt;/code&gt;, plus generating a dataset) took 7.2 seconds. Restoring a sandbox from a snapshot took about 2 seconds (sub-second on capable infrastructure; free-tier contention pushed mine higher). Now price a full run, G = 8 rollouts per step, 1000 steps, 8,000 rollouts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;per rollout:    build 7.2s   +   restore ~2s
G = 8, 1000 steps  -&amp;gt;  8,000 rollouts
naive loop:     8,000 x (7.2s build + 2s restore)        ~=  20 hours
snapshot loop:  7.2s build once  +  8,000 x 2s restore   ~=  4.4 hours
                -----------------------------------------------------
saved:          ~16 hours, all of it rebuilding identical state
&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%2Fb4nnkwk6afqmf9fw2z5e.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%2Fb4nnkwk6afqmf9fw2z5e.png" alt="Same 1000-step run at G=8. Both loops pay the per-rollout sandbox spin-up (~4 hours). The snapshot loop deletes the ~16 hours of rebuilding identical state." width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Same 1000-step run at G=8. Both loops pay the per-rollout sandbox spin-up (~4 hours). The snapshot loop deletes the ~16 hours of rebuilding identical state.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The ~4 hours of sandbox spin-up is real, and the snapshot loop still pays it. So does the naive loop. That part is a wash. What the snapshot deletes is the 16 hours spent rebuilding the same world 8,000 times. You're not making restores free; you're removing the redundant builds.&lt;/p&gt;

&lt;p&gt;And that's with a trivial seven-second environment. Swap in a real coding-agent setup (a heavy &lt;code&gt;requirements.txt&lt;/code&gt;, a model download, a dataset) and the build cost climbs from seconds to minutes while the restore stays flat. The gap widens until "rebuild every rollout" stops being slow and starts being the difference between a run you can afford and one you can't. Structurally, you've turned an O(G x steps) build cost into O(1).&lt;/p&gt;

&lt;p&gt;One honest note on that restore number. I ran on the free tier (one vCPU, ten concurrent sandboxes), so eight simultaneous restores contended and per-rollout boots stretched to two to seven seconds rather than the sub-second you'd see on real infrastructure. I used ~2s as a fair middle estimate. Tensorlake quotes 10,000+ parallel environments on their full stack, where both the restores and the parallelism get much cheaper. None of it changes the shape: build once, not 8,000 times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this pattern shows up
&lt;/h2&gt;

&lt;p&gt;This isn't a Tensorlake trick. "Snapshot a starting state, fork it into many independent rollouts" is a shape that recurs the moment you need many runs from one point. Three places it's already load-bearing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RL training.&lt;/strong&gt; The case we just walked through. Tensorlake's own GSPO cookbook does exactly this at proper scale, dispatching G completions per step to parallel sandboxes, scoring each against a hidden test suite, using the group to update the policy. It's a working reference implementation if you want more than an architecture diagram.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent evaluation.&lt;/strong&gt; Same primitive, different goal. Instead of training, you're scoring. Snapshot the benchmark's starting state, fork it across every task in the suite, run your agent in each isolated copy, collect pass/fail. Tensorlake plugs into Harbor (a framework for defining and verifying terminal tasks) as the execution runtime, running fleets of sandboxes for &lt;code&gt;terminal-bench&lt;/code&gt;-style evaluation with real filesystem verification instead of trusting an agent's self-report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel search at inference.&lt;/strong&gt; Drop the training entirely. Best-of-N sampling, tree search over tool calls, speculative execution of multiple plans, all of them are "fork the current state, explore N branches, keep the good one." The same snapshot-and-fan-out, just at inference time instead of training time.&lt;/p&gt;

&lt;p&gt;The through-line: state is what makes parallelism expensive, and a cheap snapshot removes the cost. Once that's true, a pile of architectures that were too painful to build become reasonable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it doesn't help (the honest part)
&lt;/h2&gt;

&lt;p&gt;I don't trust a piece that only tells you the good news, so here's the map of the edges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;External state isn't in the snapshot.&lt;/strong&gt; Anything your sandbox reaches over the network (a production database, a third-party API, a shared queue) is outside the VM and outside the freeze. Fork a sandbox mid-API-call and all N forks will independently try to finish that call. If your reward function touches an external service, idempotency and determinism stop being nice-to-haves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick the right checkpoint type.&lt;/strong&gt; Sandboxes give you a filesystem snapshot and a memory snapshot. For rollouts you almost always want the filesystem one: you want a clean disk reset from &lt;code&gt;s0&lt;/code&gt;, not a frozen process tree. The memory snapshot, which additionally captures live RAM and running processes, is for pausing and resuming an actual session, and it costs more. In my tests the memory variant added a few hundred megabytes of captured RAM on top of the disk image. Use it deliberately, not by default. One genuinely counter-intuitive result: the heavier memory snapshot restored faster than the filesystem one in my runs (about 0.9s versus 1.9s) because the page cache comes back warm with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's a per-restore floor.&lt;/strong&gt; Restoring a snapshot isn't free. On capable infrastructure it's sub-second; under a throttled tier it's a few seconds. If your policy step is itself sub-second, that floor is a real fraction of your time. If your rollouts are LLM calls measured in seconds, it vanishes into the noise. Know which regime you're in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot storage is real.&lt;/strong&gt; A snapshot tracks the provisioned disk, not a tiny diff. Keep thousands of them around and you're paying for thousands of disk images. Snapshot deliberately and prune.&lt;/p&gt;

&lt;p&gt;None of these sink the approach. They tell you where it fits: heavy environment, many rollouts, isolation that matters. Which is precisely the shape of agent RL.&lt;/p&gt;

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

&lt;p&gt;If your training loop spends a meaningful chunk of its wall-clock waiting for environments to come up, you are paying a tax you don't have to pay. Rejection-sampling fine-tuning, GRPO and GSPO rollouts, agent eval harnesses, paired DPO sampling, every one of them is expensive for the same reason, and it's not the reason you think. It's not the GPUs. It's that you're rebuilding the same world, over and over, for no reason.&lt;/p&gt;

&lt;p&gt;Snapshot it once. Fork it as many times as you have rollouts. Throw the forks away.&lt;/p&gt;

&lt;p&gt;The cheapest way to feel the difference is to run it. Spin up a sandbox, build something stateful inside it, snapshot it, and restore that snapshot into three fresh sandboxes in parallel. Watch the world appear three times from one freeze. That shift, from rebuilding state to restoring it, is the kind of thing you have to see once before it changes how you architect the whole loop.&lt;/p&gt;

&lt;p&gt;I tested everything in this piece against Tensorlake sandboxes on the free tier (June 2026). The rollout harness and benchmarks are reproducible: scripted candidates, no model or API key required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources &amp;amp; References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dub.sh/tlrl-ref-home" rel="noopener noreferrer"&gt;Tensorlake&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dub.sh/tlrl-ref-docs" rel="noopener noreferrer"&gt;Tensorlake sandbox documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dub.sh/tlrl-ref-playground" rel="noopener noreferrer"&gt;Tensorlake playground (free tier)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tensorlakeai/cookbooks" rel="noopener noreferrer"&gt;Tensorlake cookbooks on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tensorlakeai/cookbooks/tree/main/agent-rl-gspo-sandbox" rel="noopener noreferrer"&gt;Agentic RL with GSPO and Tensorlake sandboxes (the cookbook this piece references)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dub.sh/tlrl-harbor" rel="noopener noreferrer"&gt;Tensorlake is now an official Harbor environment runtime&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2402.03300" rel="noopener noreferrer"&gt;DeepSeekMath: the paper that introduced GRPO (Shao et al.)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2507.18071" rel="noopener noreferrer"&gt;Group Sequence Policy Optimization, GSPO (Qwen Team)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stay in Touch
&lt;/h2&gt;

&lt;p&gt;Short takes and discussions on X -&amp;gt; &lt;a href="https://x.com/sebuzdugan" rel="noopener noreferrer"&gt;https://x.com/sebuzdugan&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Practical AI / ML videos on YouTube -&amp;gt; &lt;a href="https://www.youtube.com/@sebuzdugan/" rel="noopener noreferrer"&gt;https://www.youtube.com/@sebuzdugan/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Partnerships &amp;amp; collabs -&amp;gt; &lt;a href="mailto:sebuzdugan@gmail.com"&gt;sebuzdugan@gmail.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the full piece on Medium
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://medium.com/data-science-collective/snapshot-once-rollout-a-thousand-times-a-practical-rl-setup-for-coding-agents-0f880a450610" rel="noopener noreferrer"&gt;https://medium.com/data-science-collective/snapshot-once-rollout-a-thousand-times-a-practical-rl-setup-for-coding-agents-0f880a450610&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>llm</category>
      <category>programming</category>
    </item>
    <item>
      <title>Handling Multi-Model API Outages Without Melting Production</title>
      <dc:creator>Sebastian Buzdugan</dc:creator>
      <pubDate>Wed, 24 Jun 2026 12:47:36 +0000</pubDate>
      <link>https://dev.to/sebuzdugan/handling-multi-model-api-outages-without-melting-production-255o</link>
      <guid>https://dev.to/sebuzdugan/handling-multi-model-api-outages-without-melting-production-255o</guid>
      <description>&lt;p&gt;Your dashboards go red. Not one model. All of them. Retries spike. Latency climbs. Nothing recovers.&lt;/p&gt;

&lt;h2&gt;
  
  
  When every Claude call starts failing at once
&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2F0%2AyDzeeE2Z9WI3afAv" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2F0%2AyDzeeE2Z9WI3afAv" alt="Multi-model API outage: every model failing at once, seen from the client side" width="1400" height="735"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you ship anything serious on top of LLM APIs, you have lived this moment.&lt;/p&gt;

&lt;p&gt;Requests that worked minutes ago now return errors. You flip the model flag. Same thing. You increase retries. Error rate goes up, not down. Queues back up. Downstream services feel it.&lt;/p&gt;

&lt;p&gt;This is not a prompt issue. This is not a bad deploy. This is what a platform-level incident looks like from the client side.&lt;/p&gt;

&lt;p&gt;The recent Anthropic incident labeled "Elevated error rate across multiple models" is a clean example of the failure mode that matters most in production. Multi-model impact. No numbers. No knobs to turn. Just errors.&lt;/p&gt;

&lt;p&gt;The mistake teams make is assuming model diversity equals resilience. It does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why model fallback quietly fails during platform incidents
&lt;/h2&gt;

&lt;p&gt;Most LLM client stacks are built around a simple idea: if model A fails, try model B.&lt;/p&gt;

&lt;p&gt;That works when failures are model-scoped. It fails when the blast radius is shared infrastructure.&lt;/p&gt;

&lt;p&gt;Under the hood, models often share front-door API gateways, auth and quota systems, routing layers, and shared inference clusters or schedulers.&lt;/p&gt;

&lt;p&gt;When error rate rises across multiple models, it usually means something above the model is degraded. Switching from Opus to Sonnet does not bypass the problem because the problem is not Opus or Sonnet.&lt;/p&gt;

&lt;p&gt;The docs do not emphasize this. SDK examples actively encourage retry and fallback loops that assume independence. In production, this assumption is wrong often enough to matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this failure looks like in real systems
&lt;/h2&gt;

&lt;p&gt;Here is the common sequence I see during incidents like this. Error rate jumps from sub-1 percent to double digits. Clients retry aggressively. Retry traffic amplifies load on the already degraded system. Latency increases, triggering timeouts. Queues back up. Background jobs fall behind. Engineers start manually throttling.&lt;/p&gt;

&lt;p&gt;The most dangerous part is that nothing is technically "down." Health checks pass. Some requests succeed. This keeps retry storms alive.&lt;/p&gt;

&lt;p&gt;If you only built retry logic, you built a traffic amplifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive retry loop that makes things worse
&lt;/h2&gt;

&lt;p&gt;This is the pattern I still see in production code:&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_claude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;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="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-3-sonnet&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;512&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="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="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="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="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;Claude failed after 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;This looks reasonable. It is not.&lt;/p&gt;

&lt;p&gt;During a platform incident, all retries hit the same failing surface, backoff still creates synchronized retry waves, and you contribute to global load at the worst possible moment. This pattern is survivable at low volume. At scale, it cascades.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real fix starts with circuit breakers, not retries
&lt;/h2&gt;

&lt;p&gt;The first control you need is not more retries. It is a circuit breaker that can say "stop calling this API."&lt;/p&gt;

&lt;p&gt;A circuit breaker turns repeated failure into a fast local decision. Here is a minimal breaker using Redis as shared state:&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;redis&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;claude_available&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;r&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;claude_circuit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;trip_claude&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude_circuit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_claude_with_breaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;claude_available&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;Claude circuit open&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;trip_claude&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The breaker is shared across workers. The open state expires automatically. Fail fast beats slow failure every time. This alone will save your system from self-inflicted damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why retries still matter, but only inside boundaries
&lt;/h2&gt;

&lt;p&gt;Retries are not evil. Unbounded retries are.&lt;/p&gt;

&lt;p&gt;The pattern that works is a small retry count, a tight timeout, wrapped inside a breaker:&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;safe_claude_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;attempt&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="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-3-sonnet&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;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&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="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&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;2&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="nf"&gt;call_claude_with_breaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&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;Claude unavailable&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;Notice what is missing: no exponential backoff to 30 seconds, no retry count in the teens, no blind faith that persistence equals success. At incident scale, shorter and fewer retries win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model fallback is not resilience, provider fallback is
&lt;/h2&gt;

&lt;p&gt;If multiple models fail together, the only real isolation is at the provider boundary. That means your fallback graph needs to cross vendors, not model families.&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;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;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="nf"&gt;safe_claude_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;call_openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not about preference. It is about failure domains. If your business cannot tolerate degraded LLM output, you must pay the complexity tax of multi-provider support. There is no shortcut here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bulkheads stop one failing feature from drowning everything
&lt;/h2&gt;

&lt;p&gt;Another failure pattern during incidents is resource starvation. Your summarization jobs eat all workers. User-facing requests time out. Everyone is unhappy.&lt;/p&gt;

&lt;p&gt;Bulkheads prevent this by isolating workloads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;queues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;llm_user_requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
  &lt;span class="na"&gt;llm_background_jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During elevated error rates, user traffic stays responsive and background jobs slow down, not everything. If you only have one queue, you do not have isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency saves you from retry side effects
&lt;/h2&gt;

&lt;p&gt;Retries are dangerous when requests have side effects. If your LLM call triggers writes, notifications, or billing events, retries can duplicate work.&lt;/p&gt;

&lt;p&gt;Use idempotency keys consistently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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-3-sonnet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&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="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When incidents happen, duplicate requests are common. Idempotency turns chaos into correctness.&lt;/p&gt;

&lt;h2&gt;
  
  
  What monitoring actually matters during elevated error rates
&lt;/h2&gt;

&lt;p&gt;Most teams monitor request count and average latency. That is not enough. During incidents, you need error rate by provider, circuit breaker state, retry volume, and queue depth growth.&lt;/p&gt;

&lt;p&gt;A simple metric that pays for itself is retry amplification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt;
  &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_attempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original_requests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;retry_factor&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llm_request_metrics&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'5 minutes'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that number goes above 1.5, you are probably making things worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why status pages do not help you in the moment
&lt;/h2&gt;

&lt;p&gt;Status pages tell you two things: something is wrong, and someone else is fixing it. They do not tell you how long retries will fail, which endpoints are safe, or whether partial recovery is real.&lt;/p&gt;

&lt;p&gt;Your system must assume uncertainty. That is why local control mechanisms matter more than external signals. If your only reaction is waiting for green, you are already behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Graceful degradation beats hard failure
&lt;/h2&gt;

&lt;p&gt;When LLMs are unavailable, many products default to errors. This is often unnecessary. You can return cached responses, use smaller local models for basic tasks, skip non-critical enrichment, or defer generation to async jobs.&lt;/p&gt;

&lt;p&gt;A simple cache fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_hash&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;cached&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;cached&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_hash&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;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;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During incidents, cache hit rates go up. That buys you time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this advice breaks
&lt;/h2&gt;

&lt;p&gt;There are cases where none of this helps: if your workload is real-time and uncachable, if output correctness is legally critical, or if you are contractually bound to one provider.&lt;/p&gt;

&lt;p&gt;In those cases, your only option is admission control. Say no early. Protect the rest of the system. Rejecting requests cleanly is better than timing out everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Admission control and load shedding when breakers are not enough
&lt;/h2&gt;

&lt;p&gt;Circuit breakers stop calls after failure. Admission control stops calls before failure. During platform incidents, the worst thing you can do is accept unlimited work you cannot complete. You need a hard cap.&lt;/p&gt;

&lt;p&gt;A simple token bucket per provider works well:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RateGate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_inflight&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_inflight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_inflight&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inflight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inflight&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_inflight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inflight&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inflight&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="n"&gt;claude_gate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RateGate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_inflight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage:&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;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;claude_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire&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;Claude overloaded locally&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;safe_claude_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;claude_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During partial outages, latency often doubles before error rate spikes. Inflight requests pile up silently. Your worker pool gets saturated even if the provider eventually errors.&lt;/p&gt;

&lt;p&gt;Concrete numbers I have seen: normal p95 latency 1.2s, incident p95 latency 8 to 12s, worker exhaustion within 90 seconds. Load shedding keeps tail latency bounded and preserves capacity for critical paths. It is blunt, but it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hedged requests sound smart and usually backfire with LLM APIs
&lt;/h2&gt;

&lt;p&gt;Some teams try hedged requests: fire the same prompt at two models or providers and take the first response. This can help with single-request latency variance. During incidents, it is gasoline.&lt;/p&gt;

&lt;p&gt;Example of what not to do:&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;hedged_generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;first_completed&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;call_claude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;call_claude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You double traffic exactly when capacity is constrained, trigger rate limits faster, and amplify retry storms upstream. Even cross-provider hedging is risky if you do not gate it:&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;cautious_hedge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;system_healthy&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;call_claude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;first_completed&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;call_claude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;call_openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules that keep this safe: hedge only after a latency threshold, never hedge inside retries, and disable hedging automatically when error rate rises. Hedging is an optimization. Incidents are not the time to optimize.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually test this before production does it for you
&lt;/h2&gt;

&lt;p&gt;Most LLM outage handling code is untested. That is why it fails under stress. You can simulate platform incidents locally with three levers: forced error responses, artificial latency, and partial success rates.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flaky_claude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&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;random&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;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.3&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;500&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;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.6&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="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run load tests and watch circuit breaker open rate, queue depth over time, admission rejections, and the retry amplification metric. You want to see breakers opening quickly, inflight count staying flat, and non-critical queues slowing first. If your test shows recovery only after traffic stops, your controls are too weak.&lt;/p&gt;

&lt;p&gt;This is not chaos for chaos' sake. It is validating that your failure handling actually reduces load instead of redistributing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Elevated error rates across multiple models are not edge cases anymore. They are normal failure modes for platform-scale AI services.&lt;/p&gt;

&lt;p&gt;If your resilience strategy is model switching and hope, you will relive the same outage every time.&lt;/p&gt;

&lt;p&gt;The fix is not clever prompts or better SDK defaults. It is the same discipline that keeps payment systems, search backends, and message brokers alive under stress.&lt;/p&gt;

&lt;p&gt;Treat LLMs like the critical infrastructure they are. Your future incidents will still hurt, but they will not take everything down with them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources &amp;amp; References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://status.anthropic.com/" rel="noopener noreferrer"&gt;Anthropic Incident: Elevated error rate across multiple models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/" rel="noopener noreferrer"&gt;AWS Architecture Blog: Exponential Backoff and Jitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sre.google/sre-book/handling-overload/" rel="noopener noreferrer"&gt;Google SRE Book: Handling Overload&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stripe.com/blog/idempotency" rel="noopener noreferrer"&gt;Stripe Engineering: Designing APIs for failure&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stay in Touch
&lt;/h3&gt;

&lt;p&gt;Short takes and discussions on X → &lt;a href="https://x.com/sebuzdugan" rel="noopener noreferrer"&gt;https://x.com/sebuzdugan&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Practical AI / ML videos on YouTube → &lt;a href="https://www.youtube.com/@sebuzdugan/" rel="noopener noreferrer"&gt;https://www.youtube.com/@sebuzdugan/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Partnerships &amp;amp; collabs → &lt;a href="mailto:sebuzdugan@gmail.com"&gt;sebuzdugan@gmail.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://medium.com/@sebuzdugan/handling-multi-model-api-outages-without-melting-production-965f70d4c99a" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>devops</category>
    </item>
    <item>
      <title>Governing and Monitoring Enterprise AI Usage: Allow and Deny Controls for AI Apps on Managed Devices</title>
      <dc:creator>Sebastian Buzdugan</dc:creator>
      <pubDate>Fri, 19 Jun 2026 12:57:05 +0000</pubDate>
      <link>https://dev.to/sebuzdugan/governing-and-monitoring-enterprise-ai-usage-allow-and-deny-controls-for-ai-apps-on-managed-devices-1c87</link>
      <guid>https://dev.to/sebuzdugan/governing-and-monitoring-enterprise-ai-usage-allow-and-deny-controls-for-ai-apps-on-managed-devices-1c87</guid>
      <description>&lt;p&gt;Your gateway logs look clean. Every API call is accounted for, rate-limited, and piped to your SIEM. You have virtual keys per team, budgets per model, guardrails on PII. The compliance box is checked.&lt;/p&gt;

&lt;p&gt;Then someone asks: which AI apps are actually running on our laptops right now?&lt;/p&gt;

&lt;p&gt;Nobody knows.&lt;/p&gt;

&lt;p&gt;The gateway only sees traffic that goes through it. Claude Desktop, ChatGPT, Cursor, Codex, and a browser full of AI sidebars talk directly to their own endpoints, using API keys that live in the user's home directory. None of that traffic has ever touched your proxy.&lt;/p&gt;

&lt;p&gt;This isn't a configuration problem. It's a structural one. And it has a clean fix that most enterprise teams haven't wired up yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap nobody talks about
&lt;/h2&gt;

&lt;p&gt;A gateway governs the traffic that flows through it. That is exactly what it is for, and Bifrost's gateway does it well: virtual keys, budgets, routing, guardrails, and audit logs, enforced on every request your infrastructure sends.&lt;/p&gt;

&lt;p&gt;But your developers are not only calling AI through your infrastructure. They are running the Claude app on their laptop. They are using Cursor with a personal API key. They installed an MCP server that a GitHub README told them would make their coding agent smarter. That traffic leaves the machine straight for the provider. It never had a reason to pass through your gateway, so it doesn't.&lt;/p&gt;

&lt;p&gt;This is not a gateway shortcoming. It is a question of where the traffic starts. A centralized proxy can only govern what reaches it, and a desktop app talking directly to &lt;code&gt;api.anthropic.com&lt;/code&gt; was never going to reach it on its own. The endpoint is simply a second surface, and until something sits on the machine, that surface is dark.&lt;/p&gt;

&lt;p&gt;Here is what lives there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Desktop chat apps: Claude Desktop, ChatGPT, calling home directly&lt;/li&gt;
&lt;li&gt;Browser AI: ChatGPT and Claude on the web, running over HTTPS&lt;/li&gt;
&lt;li&gt;Coding agents: Claude Code, Cursor, Codex, OpenCode, configured per-user, per-project&lt;/li&gt;
&lt;li&gt;MCP servers: local processes that give those agents filesystem access, shell execution, database connections, and external API calls, spun up from a few lines of JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one deserves a pause. An MCP server is not an app in the traditional sense. It's a process your developer launched that tells their AI agent: here are the tools you can use. File reads, shell commands, web fetches. All of it governed by whatever the developer put in a config file, with no visibility for your security team and no logs anywhere.&lt;/p&gt;

&lt;p&gt;A coding agent with shell access, connected to an ungoverned MCP server, can exfiltrate credentials before your SIEM sees a single packet.&lt;/p&gt;

&lt;p&gt;That is the surface Edge is built to cover, and it covers it by extending the gateway you already run, not by replacing 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%2Fqeux465wod28aepj5f8h.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%2Fqeux465wod28aepj5f8h.png" alt="Diagram of a managed laptop running Claude Desktop, Cursor, Codex, browser AI, and MCP servers: only gateway traffic is visible, while direct calls to anthropic.com and cursor.sh are not" width="720" height="284"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;What your gateway sees vs. what's actually running on managed devices. The gap is everything on the right.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Two layers, one control plane
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://getbifrost.ai" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt;, the open-source AI gateway built by &lt;a href="https://www.getmaxim.ai" rel="noopener noreferrer"&gt;Maxim&lt;/a&gt; and available &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;, splits the governance problem into the two layers it actually has.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;gateway&lt;/strong&gt; is the centralized control plane. It sits between your infrastructure and 1000+ models behind one unified API, and enforces policy on every request: virtual keys, team budgets, rate limits, model routing, PII detection, secrets scanning, content guardrails, and audit logs that feed SOC 2 Type 2, GDPR, and HIPAA stacks. It is built to stay out of the way. In Bifrost's own t3.xlarge benchmark at 5,000 requests per second, the gateway added 11 microseconds of overhead per request at a 100 percent success rate.&lt;/p&gt;

&lt;p&gt;The OSS version is self-hosted and drops in as a base URL change. You point your existing SDK at it and leave the rest of your code alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# before
&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;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-anthropic-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# after: same code, traffic now governed
&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;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080/anthropic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dummy-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# real keys live in Bifrost
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No agent on the machine, no per-app config. This is the layer most teams already have, or can stand up in an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bifrost Edge&lt;/strong&gt; is the endpoint enforcement layer. It is an Enterprise feature, not part of the open-source gateway. Edge runs locally on every managed machine and intercepts AI traffic at the OS level, before it reaches any application's networking stack. That traffic gets routed through your Bifrost gateway for policy evaluation, and the gateway's verdict comes back to Edge, which enforces it on the device.&lt;/p&gt;

&lt;p&gt;The frame that makes this click: &lt;strong&gt;the gateway centralizes governance, Edge enforces it on every machine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every desktop app, every browser extension, every MCP server suddenly inherits the same virtual keys, budgets, guardrails, and audit logging that your API infrastructure already has. Zero per-app configuration. You install Edge once and governance follows the user.&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%2F22lcy6ryjziwn8zatbjj.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%2F22lcy6ryjziwn8zatbjj.png" alt="Diagram of the Bifrost gateway connected to three laptops, each running Bifrost Edge, with policy syncing from the gateway to every device" width="720" height="322"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Bifrost Edge on every managed machine, all routing to the same central gateway. Policy changes propagate to the fleet without touching individual devices.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Edge sits on the machine
&lt;/h2&gt;

&lt;p&gt;Installation is a single MDM push. Edge ships managed config for Jamf, Intune, Kandji, Omnissa Workspace ONE, and JumpCloud, so it rolls out across macOS, Windows, and Linux with no end-user action. The managed config carries only non-sensitive connection settings. The machine arrives pre-pointed at your Bifrost; identity and keys come from the user's sign-in, not from the device profile.&lt;/p&gt;

&lt;p&gt;After install, the user signs in once through a browser SSO flow. That sign-in links the machine to the user and pulls down every policy assigned to them. From then on, Edge runs as a menu-bar agent on macOS or a system-tray process on Windows and Linux.&lt;/p&gt;

&lt;p&gt;Interception happens at the machine level, not per application. Edge does not ask you to change base URLs or touch anything inside the AI apps. It uses an organization certificate, managed centrally from the Bifrost console, to route each app's AI calls through your gateway before they leave the machine. When Claude Desktop calls &lt;code&gt;api.anthropic.com&lt;/code&gt;, Edge sees the request, sends it through Bifrost for policy evaluation, and hands the response back. The app never knows the difference.&lt;/p&gt;

&lt;p&gt;That is what makes this work at scale. You have a hundred AI tools across the fleet; you configure one thing, and all hundred are governed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Allow, deny, and the state in between
&lt;/h2&gt;

&lt;p&gt;Edge discovers AI apps and MCP servers across the fleet and rolls them into a deduplicated catalog: the same tool on 200 machines shows up once. Every entry sits in one of three states.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pending&lt;/strong&gt;: discovered and awaiting review. It keeps working in the meantime, so turning Edge on does not break anyone's workflow on day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approved&lt;/strong&gt;: explicitly allowed. Traffic runs normally and stays under the gateway's full policy stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Denied&lt;/strong&gt;: blocked. Edge stops it on the device, and the user sees a notice that the app is not permitted on company machines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That default-pending posture is the part worth sitting with. You are not forced to choose between locking everything down up front or flying blind. New tools surface as Pending while you decide, so you get visibility immediately and enforcement when you are ready.&lt;/p&gt;

&lt;p&gt;The workflow in practice: a developer installs a new coding agent. It appears as Pending in the console with its name, the devices running it, who last changed its status, and any notes. You approve it, deny it, or select every Pending item and act in bulk. Decisions take effect on each device at its next check-in, which is set by a sync interval you control centrally, down to a few seconds when you need policy to move fast.&lt;/p&gt;

&lt;p&gt;Supported surfaces today: Claude Desktop, ChatGPT, Cursor, and Codex on the desktop; Claude Code, Codex CLI, and OpenCode as coding agents; ChatGPT and Claude on the web. If something you run is not on the list yet, the console has a "request support for an application" path.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP governance is its own layer
&lt;/h2&gt;

&lt;p&gt;MCP servers get their own governance surface in Edge, separate from application-level allow/deny. That separation matters.&lt;/p&gt;

&lt;p&gt;You might want Cursor allowed but a specific MCP server denied. Maybe it has filesystem write access your security team hasn't signed off on. Maybe it was pulled from an untrusted source. Edge lets you approve or deny at the server level, not just the app level.&lt;/p&gt;

&lt;p&gt;The workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Edge discovers MCP servers configured in supported AI apps across the fleet&lt;/li&gt;
&lt;li&gt;The full inventory surfaces in the Bifrost admin console&lt;/li&gt;
&lt;li&gt;Administrators approve or deny each server individually&lt;/li&gt;
&lt;li&gt;Edge enforces those decisions at the device level&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Default posture applies here too. Set Edge to deny all unknown MCP servers by default and new servers require explicit approval before anyone in the fleet can use them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fleet-wide MCP inventory alone is something most security teams have never had.&lt;/strong&gt; Ask a room of security leads which MCP servers run on their developer machines and the honest answer is nobody knows. Edge surfaces each one with its name, whether it connects through a local command or a remote URL, and the exact tools it exposes, then gives you per-server approve and deny. MCP discovery currently covers Claude Code, Claude Desktop, Cursor, Gemini CLI, OpenCode, and Codex.&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%2Fo9ol7edxv7d3v5nzqszy.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%2Fo9ol7edxv7d3v5nzqszy.png" alt="Three-step MCP governance flow: Edge discovers MCP servers, an admin approves or denies each in the Bifrost console, and Edge enforces the decision on every device" width="720" height="246"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;MCP governance in three steps: Edge discovers servers fleet-wide, admin sets per-server policy, Edge enforces on every device.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can see
&lt;/h2&gt;

&lt;p&gt;With Edge deployed, the console becomes a live inventory of AI on the fleet.&lt;/p&gt;

&lt;p&gt;The Devices view lists every machine running the agent: hostname, owner (name and email), platform with OS version and architecture, agent version, how many AI apps and MCP servers it has (versions on hover), when it was first seen, and its last check-in. You can filter by platform, by a specific app or MCP server, or by approval status, so "which machines are still running the app we denied" is a filter, not an investigation. Open a single device and you get its installed apps, its configured MCP servers and the tools they expose, and one-click approve, deny, or remove.&lt;/p&gt;

&lt;p&gt;Underneath the inventory, the guardrails you already configured in the gateway now apply to all of this traffic, in both directions, before a prompt reaches a model and before a response comes back. That includes Gitleaks-backed secrets detection for leaked API keys, tokens, and private keys, PII redaction, and any third-party content-safety integration you run (AWS Bedrock, Azure, Google Model Armor, CrowdStrike, Patronus, and others). Edge does not re-implement any of it. It brings ChatGPT, Claude, and your coding agents under the same protection your API traffic already has.&lt;/p&gt;

&lt;p&gt;Every routed request also inherits the gateway's structured logging: timestamp, user, model, token count, guardrail outcome. Pair that with the gateway's Prometheus, OpenTelemetry, and Datadog exports and you have one view over all AI usage, infrastructure and endpoint alike.&lt;/p&gt;

&lt;h2&gt;
  
  
  The containment argument
&lt;/h2&gt;

&lt;p&gt;Here is the scenario that makes the security case concrete.&lt;/p&gt;

&lt;p&gt;Your team discovers a new AI desktop app is exfiltrating conversation history to an undisclosed third party. You need it off every managed laptop, now.&lt;/p&gt;

&lt;p&gt;Without Edge: you're filing an MDM change request, writing browser extension policies, opening a firewall ticket, coordinating across three teams, and probably missing the macOS desktop app entirely. Timeline: days to weeks.&lt;/p&gt;

&lt;p&gt;With Edge: you open the console, set the app to Denied, and apply it to the fleet. Every machine picks it up at its next check-in, governed by the sync interval you set. The block lands before any data leaves the device. Audited, reversible, fleet-wide in seconds to minutes.&lt;/p&gt;

&lt;p&gt;Same logic for MCP servers. A new server with dangerous tool permissions shows up in a GitHub repo. You deny it fleet-wide before any developer on your team installs it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The governance speed matters as much as the governance capability.&lt;/strong&gt; The threat landscape for AI tooling is moving fast. Waiting weeks to roll out a block is not a viable security posture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;Edge is in limited alpha. You cannot self-serve into production today. You register, you get access when the alpha opens up. That is the reality of where it sits.&lt;/p&gt;

&lt;p&gt;The capability set described here is what the alpha delivers. All of it is live and usable for teams in the program. But if you need this in production next week with an SLA behind it, that conversation starts with the Bifrost team, not a self-serve dashboard.&lt;/p&gt;

&lt;p&gt;Also worth naming: the MCP app support list is current as of this writing. New apps and MCP servers get added, but if you have a specific tool you need covered, check the &lt;a href="https://docs.getbifrost.ai/edge/supported-applications" rel="noopener noreferrer"&gt;supported applications page&lt;/a&gt; before assuming it's there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;The Bifrost gateway (OSS) is available now. It is a drop-in base URL replacement for any OpenAI-compatible SDK call. If you are not running a centralized gateway yet, that is the right starting point. Get visibility on your API traffic first, then add Edge for the endpoint layer.&lt;/p&gt;

&lt;p&gt;For Edge, register for alpha access at &lt;a href="https://docs.getbifrost.ai/edge/overview" rel="noopener noreferrer"&gt;docs.getbifrost.ai/edge/overview&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The path that makes sense:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy the Bifrost gateway, route your team's API traffic through it&lt;/li&gt;
&lt;li&gt;Configure virtual keys, budgets, and audit logging&lt;/li&gt;
&lt;li&gt;Roll out Bifrost Edge via MDM to bring endpoint AI traffic under the same governance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1 and 2 give you coverage on everything your infrastructure controls. Step 3 closes the gap.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Every technical detail here is drawn from the &lt;a href="https://docs.getbifrost.ai/edge/overview" rel="noopener noreferrer"&gt;Bifrost Edge documentation&lt;/a&gt; and the &lt;a href="https://docs.getbifrost.ai/overview" rel="noopener noreferrer"&gt;gateway overview&lt;/a&gt;. If something in this piece doesn't match what you see in the docs, trust the docs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://medium.com/p/45ad2f5c5b00" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;More writing on Medium → &lt;a href="https://medium.com/@sebuzdugan" rel="noopener noreferrer"&gt;https://medium.com/@sebuzdugan&lt;/a&gt;&lt;br&gt;
Short takes on X → &lt;a href="https://x.com/sebuzdugan" rel="noopener noreferrer"&gt;https://x.com/sebuzdugan&lt;/a&gt;&lt;br&gt;
Practical AI / ML videos on YouTube → &lt;a href="https://www.youtube.com/@sebuzdugan/" rel="noopener noreferrer"&gt;https://www.youtube.com/@sebuzdugan/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
