惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
IT之家
IT之家
H
Heimdal Security Blog
Jina AI
Jina AI
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
T
Tailwind CSS Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
有赞技术团队
有赞技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
AWS News Blog
AWS News Blog
C
Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Hacker News
The Hacker News
The Cloudflare Blog
Hugging Face - Blog
Hugging Face - Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Threatpost
S
Securelist
P
Privacy International News Feed
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - 聂微东
博客园 - 叶小钗
J
Java Code Geeks
V
V2EX
博客园 - Franky
Spread Privacy
Spread Privacy
K
Kaspersky official blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Simon Willison's Weblog
Simon Willison's Weblog
Project Zero
Project Zero
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
C
Cybersecurity and Infrastructure Security Agency CISA
C
CERT Recently Published Vulnerability Notes
Latest news
Latest news
NISL@THU
NISL@THU
罗磊的独立博客
W
WeLiveSecurity
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园_首页
V
Visual Studio Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Sliding-Window Spend Guard: the $47K Loop Per-Call Caps Miss
Alexey Spinov · 2026-06-14 · via DEV Community

Sliding-Window Spend Guard for AI Agents: Catch the $47K Loop Per-Call Caps Miss

A sliding-window spend guard sums what your agent has spent over the last N minutes and refuses the next call before it dispatches — which is the thing a per-call cap can't do. A per-call cap asks "is this one call too expensive?" The runaway loops that empty a budget are built from calls that each pass that check. The damage lives in the sum, not in any single call.

In short: a sliding-window spend guard tracks a trailing window of calls and blocks the next one when cumulative spend or a repeated near-identical call breaches a per-window rule. In my run it stopped an Analyzer-Verifier ping-pong at call 12, $45.80 in, after a naive per-call $5 cap let all 12 through. Stdlib, keyless, runs in seconds.

AI disclosure: I wrote window_guard.py with AI assistance and ran it myself before publishing. Every number in the output blocks below is pasted from a real run of that script on a fixture I'll show you. The $47K incident is someone else's, and I link the postmortem next to it. I label which is which.

A $47K agent loop where every single call was fine

In November 2025 a team woke up to a $47,000 bill from a single agent deployment. Four LangChain agents, talking to each other over A2A, and two of them — an Analyzer and a Verifier — got into a ping-pong. Analyzer hands work to Verifier, Verifier kicks it back, repeat. For 264 hours.

The cost didn't spike. It escalated, week over week: $127, then $891, then $6,240, then $18,400. The author of the postmortem, Gabriel Anhaia, describes the root cause in a way I keep coming back to: the dashboard was green for eleven days, and there was no step cap, no per-conversation USD budget, no orchestrator deciding when the work was done (dev.to/gabrielanhaia, Nov 2025). The dashboard showed the number. It just showed it after each call, never before the next one. A follow-up teardown by the Waxell team sharpened the line into the title of their piece: token-budget alerts aren't budget enforcement (dev.to/waxell, 2025).

Here's the part that matters for your code, not your nerves. Not one of those calls was a runaway on its own. Each Analyzer→Verifier round was a cheap, valid, well-formed request. A per-call spend cap — the thing most people reach for first — would have waved through every last one of them, because no individual call was expensive. The loop wasn't a bug in any call. It was the accumulation of perfectly fine calls that no per-call check is built to see.

I've shipped four guards on this blog and all four share that blind spot. SpendGuard caps the cost of one call. TxCanary screens one transaction. The pre-execution gate refuses one bad action. Per-call, per-call, per-call. Useful — and structurally blind to the window. So I built the missing layer and ran it against the exact pattern that cost that team $47K.

What this is, in one paragraph

window_guard.py keeps a trailing window of your agent's recent calls — collections.deque, a timestamp per call — and before it lets the next call dispatch, it checks three things over that window: total spend, how many times a near-identical call has repeated, and how often a named side effect (a refund, a charge) has fired. Trip any one and the call is refused before it runs, not logged after. Stdlib only. No keys, no network, no money moves. The clock is injectable, so a fixture run is byte-for-byte reproducible — same point most of my tools make, because a number you can't reproduce is a number you shouldn't quote.

This is not a server. It's not a gateway you have to adopt. Cloudflare and others now sell per-window spend limits at the proxy layer, and those are good if you're already on their infra. This is forty-odd lines you paste into the agent loop you already have, that does its own arithmetic, in your process, before the request leaves. No vendor.

The guard, end to end

The whole decision lives in one method. Evict what's aged out of the window, then check three rules against what's left plus the call you're about to admit:

def _check(self, now, cost, fp, sideeffect):
    """Raise WindowExceeded if admitting (cost, fp, sideeffect) at `now`
    would breach a rule. Runs BEFORE the call is recorded or executed."""
    self._evict(now)

    # Rule A: cumulative spend over the window (the call itself included).
    projected = sum(e.cost for e in self._events) + cost
    if projected > self.spend_cap:
        raise WindowExceeded("cumulative_spend",
            f"${projected:,.2f} over last {self.window:.0f}s would exceed "
            f"cap ${self.spend_cap:,.2f} ({len(self._events) + 1} calls in window)")

    # Rule B: same semantic call repeated too often inside the window.
    if fp:
        repeats = sum(1 for e in self._events if e.fp == fp) + 1
        if repeats >= self.loop_threshold:
            tool = fp.split("|", 1)[0]
            raise WindowExceeded("loop_repeat",
                f"'{tool}' fired {repeats}x in {self.window:.0f}s "
                f"(threshold {self.loop_threshold}); near-identical args")

Rule A is the $47K rule: sum the window, including the call you're weighing, and if it crosses the cap, stop. Rule B catches the loop a spend cap might miss when the calls are nearly free — an ask_clarification that fires forever and burns five cents a turn.

The loop check has one detail that earns its keep. It doesn't compare calls for exact equality. It fingerprints them, stripping volatile churn fields first:

def _fingerprint(tool, args, ignore=("nonce", "rev", "id", "ts", "request_id")):
    """Two calls that differ only by an incrementing nonce or revision get the
    SAME fingerprint, which is exactly what a strict equality check misses."""
    stable = {k: v for k, v in (args or {}).items() if k not in ignore}
    return tool + "|" + json.dumps(stable, sort_keys=True, ensure_ascii=False)

A stuck agent rarely repeats itself byte-for-byte. It bumps a retry counter, a timestamp, a request id. A naive equality check sees two different calls and lets the loop run. Strip the churn, and the loop shows its real shape.

On a real call site you don't touch any of that. You wrap the function:

guard = WindowGuard(window_seconds=3600, spend_cap_usd=50)

@guard.wrap(cost=lambda r: r["usd"],
            fingerprint=lambda *a, **k: ("search", k["q"]))
def call_tool(...):
    ...

Running it: three scenarios, one verdict

The demo replays three fixtures through a fresh guard. Same policy for all three: a one-hour window, a $50 cap, a loop threshold of 8, and a refund side-effect cap of 5.

python3 window_guard.py --fixture window_fixture.json

The header that prints first:

window_guard.py  -  sliding-window spend & loop guard
policy: window=3600s  spend_cap=$50  loop_threshold=8  sideeffect_cap={'refund': 5}

Scenario 1 — a normal workload passes

First, the case that should sail through, so you trust the brake isn't just trigger-happy. Nine mixed calls — searches, page reads, a couple of drafts — over an hour:

SCENARIO: PASS  (normal mixed workload, never breaches the window)
  call  1 t=    0s  search_web         $0.42  (window spend so far $0.42)
  call  4 t=  600s  summarize          $1.10  (window spend so far $2.11)
  call  7 t= 2100s  draft_section      $2.20  (window spend so far $5.04)
  call  9 t= 3300s  summarize          $1.30  (window spend so far $8.39)
  => PASS. 9 calls admitted, $8.39 total, no window breached.
  (for contrast: a naive per-call $5 cap would block 0 of these 9 calls.)

Nine calls, $8.39, nothing blocked. Note the contrast line, because it runs under every scenario: the per-call $5 cap also blocks 0 here. On a healthy workload the two guards agree. That's the point — the window guard isn't stricter, it's watching a different axis. They only disagree when the axis matters.

Scenario 2 — the $47K loop, in miniature

Now the Analyzer↔Verifier ping-pong. Each call costs about $4 — cheap, valid, the kind a per-call cap nods through. Watch the window spend so far column climb:

SCENARIO: BLOCK-cumulative  (Analyzer<->Verifier ping-pong, each call cheap, sum runs hot)
  call  1 t=    0s  analyze_section    $4.10  (window spend so far $4.10)
  call  5 t=  280s  analyze_section    $4.40  (window spend so far $20.65)
  call  9 t=  600s  analyze_section    $4.20  (window spend so far $37.35)
  call 11 t=  780s  analyze_section    $4.10  (window spend so far $45.80)
  call 12 t=  870s  verify_section     BLOCKED -> [cumulative_spend] $50.05 over last 3600s would exceed cap $50.00 (12 calls in window)
  => BLOCKED at call 12 on rule 'cumulative_spend'. $45.80 spent before the brake hit; call 12 never dispatched.
  (for contrast: a naive per-call $5 cap would block 0 of these 12 calls.)

There it is. The guard does the addition the dashboard does — but it does it before call 12, sees the window would land at $50.05, and refuses. Call 12 never goes out. $45.80 spent, then a hard stop. The per-call $5 cap, on the identical twelve calls? Blocks zero. Every call is under five dollars. That's the $47K loop reproduced in a teacup, and the difference between watching it and stopping it is one sum() evaluated at the right moment.

One honest caveat about that "blocks zero," because it's the obvious objection and I'd rather raise it than have you. The $5 figure is the demo's default, and these calls top out at $4.40 — so of course $5 waves them through. Drop the per-call cap to $4 and it would trip this loop, on call 3, at $8.00. So why isn't a tight per-call cap the answer? Because to catch a $4 loop you'd set the cap near $4, and now every legitimately expensive call — a deep research step, a long synthesis — gets refused too. You'd be tuning a single threshold to sit below your normal call cost, which breaks normal work to catch abnormal accumulation. The window guard doesn't win because it stops sooner (a tight per-call cap stops sooner and cheaper here). It wins because it watches the sum, so it can stay loose on any single call and still see the pile. Different axis, not a stricter one.

I want to be honest about the brake distance: it doesn't stop at zero dollars. It stops at $45.80, because the window has to reach the cap before the next call can breach it. A per-window guard bounds your blast radius to roughly one window of budget. It is not a circuit breaker that fires on call one. If you need a tighter bound, shrink the window or the cap. That's the knob.

Scenario 3 — a loop that's nearly free

The last one is the loop a spend cap is worst at: a clarification call that costs a nickel and never resolves. The args change every time, but only in a churn field, so the fingerprint stays the same:

SCENARIO: BLOCK-loop  (same semantic call repeated; args differ only by a churn field)
  call  1 t=    0s  ask_clarification  $0.05  (window spend so far $0.05)
  call  7 t=   80s  ask_clarification  $0.05  (window spend so far $0.37)
  call  8 t=   94s  ask_clarification  BLOCKED -> [loop_repeat] 'ask_clarification' fired 8x in 3600s (threshold 8); near-identical args
  => BLOCKED at call 8 on rule 'loop_repeat'. $0.37 spent before the brake hit; call 8 never dispatched.
  (for contrast: a naive per-call $5 cap would block 0 of these 8 calls.)

Thirty-seven cents in, the guard sees the same call fired eight times and stops it. A spend cap would never catch this — at a nickel a turn you could run it ten thousand times before any budget alarm twitched. By then it's not a nickel anymore. This is the muggleai class of failure, where the agent guesses and the meter just runs.

The summary the script prints at the end says the whole thing in three rows:

SUMMARY  (window guard  vs  naive per-call $5 cap)
  scenario          window guard              per-call cap
  --------------------------------------------------------
  PASS              PASS                      PASS (0 blocked)
  BLOCK-cumulative  BLOCK cumulative_spend@12 PASS (0 blocked)
  BLOCK-loop        BLOCK loop_repeat@8       PASS (0 blocked)

Three workloads. The per-call cap's column reads PASS, PASS, PASS. On the two that were actually on fire, it never moved. Each call passed. The window didn't.

tracking ≠ control

This is the line I'll defend in the comments, and I'd be glad to be argued out of it: a dashboard that shows your spend is not a control on your spend. It's a rear-view mirror. The $47K team had the number the whole time. It updated faithfully, after every call, all 264 hours. What they didn't have was anything that read that number before the next call and said no.

That's the only move the window guard makes that a dashboard doesn't. Same arithmetic — sum of the window — but evaluated at admission time, in the path of the call, with the authority to raise instead of just render. A per-call cap is one slice of pre-execution control. A per-window guard is the slice that sees accumulation. Most production agents I've seen have neither in the loop and a beautiful dashboard beside it.

What this is NOT

I'd rather you trust the small honest claim than oversell it.

  • It's not a replacement for per-call caps — it's a second layer. A single genuinely runaway call (one $900 request) is a per-call problem, and this guard, if that call alone fits the window budget, would admit it. Keep both. They cover different failures.
  • The brake distance is one window, not zero. It stopped at $45.80, not $0. You bound the damage to roughly a window's worth of budget; you don't prevent the first dollar. Tune window and cap to the blast radius you can live with.
  • The escalation numbers ($127 / $891 / $6,240 / $18,400) are from the public postmortem, not from my run. My fixture demonstrates that a window guard halts this pattern early; it does not re-prove the incident. The only numbers I generated are the demo's: the $45.80 stop, the call-12 block, the call-8 loop trip.
  • The third rule (side-effect cap) ships in the code but isn't exercised by these fixtures. window_guard.py has a Rule C — a per-window cap on a named side effect like a refund or a charge — and the policy header prints sideeffect_cap={'refund': 5}. But all three demo scenarios test only spend and loop; none fire a side effect, so you won't see Rule C trip in the output. It's there, it's tested by the same _check, and you can wire it up with the sideeffect= argument — but I'm not going to claim the demo proves it. It demonstrates two of the three rules. Treat Rule C as code you can adopt, not a result I showed you.
  • The per-call comparison uses a $5 cap on purpose, and the result depends on it. "Blocks 0 of 12" holds because these calls cost ≤$4.40. A per-call cap set below your normal call cost would catch the loop — at the price of refusing legitimate expensive calls. The honest claim isn't "per-call caps fail"; it's "per-call caps and window guards watch different axes, and you want both."
  • A fixture is not your production loop. The window (3600s) and the cap ($50) are demo values. Yours depend on your call rate and your budget. The code is the reusable part; the policy is yours to set.

Pick your window and your cap

Open your agent's loop and find the single call site that fans out — the planner, the verifier, the retry wrapper. Wrap it. Set window_seconds to a few times your normal task length, and spend_cap_usd to the most you'd tolerate losing before a human looks. Then leave it. The day it fires, it'll have saved you from a number with too many zeros.

Here's the open question I haven't settled, and I'd genuinely like your take: what's the right window length when an agent's healthy work and its runaway loop run at the same call rate? Too short and you trip on a legitimate burst; too long and the loop drains a window before the brake bites. I've been setting it to ~3× the longest normal task and eyeballing it, which is not a method I'm proud of. If you've tuned a per-window cap on a real agent and found a rule that holds, drop it in the comments — I read every one. And follow along; the next post takes this guard from one call site to a whole multi-agent graph and tries to find where the window should live.