TL;DR. The scary AI bill is rarely an attacker. It is your own agent stuck in a loop overnight, or one tenant whose usage quietly spikes a hundredfold. A rate limiter cannot see either, because it counts requests per minute per route, and the damage here happens inside a single request or across a slow-moving window per customer. This post explains the two failure modes a rate limiter misses, and shows a Laravel circuit breaker that watches the actual AI traffic and trips before the budget is gone. It observes by default, so it cannot break a real workload on day one. Free and open source.
The pattern shows up again and again in developer postmortems: an agent hits a transient error, retries, appends the failure to its context, retries again, and the loop never terminates. Each retry is a full provider call, context grows, tokens compound, and by morning the run has made tens of thousands of calls. Teams on the OpenAI forum describe exactly this, agents looping unattended and running up four- and five-figure charges before anyone is awake to notice (How are you handling runaway agent costs?).
Agents do not spend like chatbots. A chatbot sends one message and gets one answer. An agent runs a reasoning loop with tool calls, reads, edits, and re-checks, so a single user action can fan out into hundreds of model calls. That is the unit of damage, and it is invisible to the tool most teams reach for first.
Laravel's rate limiter is excellent at what it does: cap requests per minute per key, usually per route or per IP. That shape does not match either runaway mode.
Rate limiting models politeness at the front door. Runaway cost is a fire in a back room. Different sensor.
laravel-ai-circuit-breaker registers a global HTTP-client middleware, so it governs every outbound call to a known AI provider regardless of the SDK. Prism and the Http facade are covered automatically; a client with its own Guzzle stack attaches CircuitBreaker::stack().
composer require laravelsecurityaudit/laravel-ai-circuit-breakerIt does two things a rate limiter and a cost dashboard do not.
Within one request lifecycle it counts AI calls and detects repetition: too many calls, or the same call repeated too many times. A self-recursing agent trips this first, inside the request that spawned it, before the loop has a chance to run all night.
Per principal, over a rolling window, it tracks estimated input tokens and call volume. When a principal spikes past your threshold it opens a circuit with a cooldown, so the next calls from that principal fail fast instead of feeding the fire.
A "principal" is the authenticated user, tenant-aware when your user model exposes a tenant key, falling back to the client IP. You can bind your own PrincipalResolver to define the fingerprint that matters for your app.
This is the part that makes it safe to install on a live app. By default the breaker only observes: it counts, fires events, and logs, but never blocks. It cannot break a legitimate workload on a false anomaly, because on day one it is not blocking anything. You watch the events, tune the thresholds to your real traffic, and only then turn on enforcement:
CIRCUIT_BREAKER_ENFORCE=trueWhen enforcing, a tripped limit opens the circuit for that principal and throws CircuitOpenException, which fails the AI call closed. fail_open is true, so an internal error in the breaker itself never breaks a real call.
CIRCUIT_BREAKER_ENABLED=true
CIRCUIT_BREAKER_ENFORCE=false
CIRCUIT_BREAKER_MAX_CALLS=25
CIRCUIT_BREAKER_MAX_REPEATS=5
CIRCUIT_BREAKER_WINDOW=60
CIRCUIT_BREAKER_MAX_TOKENS=200000
CIRCUIT_BREAKER_COOLDOWN=300LoopDetected and CircuitOpened fire in both observe and enforce mode, so you can send them to Slack, PagerDuty, or your SIEM with a single listener and get paged the first time an agent misbehaves, long before you have decided to start blocking.
Event::listen(LoopDetected::class, function ($event) {
// notify your on-call channel
});The outbound request does not carry token usage, so spend is estimated from the request body, roughly four characters per token. This is an anomaly brake, not a billing system. It is tuned to catch the shape of a runaway, an order-of-magnitude jump, not to reconcile your invoice to the cent. Exact post-hoc accounting from the response is a planned addition. Treat it as the smoke detector, not the accountant.
It helps to see the shape the loop breaker watches for. A typical agentic run does something like this inside a single request:
Nothing here is a bug in any one call. Each request is well-formed and each response is reasonable. The failure is emergent: the same call repeating with a slowly growing context. A per-route rate limiter sees one inbound request and waves it through. The loop breaker, counting calls and repeats within that request, sees the fifth near-identical call and trips. The difference is where the sensor sits, the front door versus the actual provider traffic.
Multi-tenancy makes the spend brake matter just as much. In a shared app, one tenant's misbehaving integration should not drain the budget for everyone. Because accounting is per principal, an anomaly is contained to the principal that caused it: the cooldown opens the circuit for that customer alone while everyone else keeps working.
Not while observing, which is the default. You tune the thresholds to your real traffic first, and enforcement is a separate flag you set only when the numbers fit your workload.
The authenticated user, tenant-aware when your user model exposes a tenant key, falling back to client IP. You can bind your own resolver.
Yes. The middleware is on the HTTP client, so calls from a queued job or a console command are governed like calls in a web request, and the request-scoped loop counter applies within each job execution.
Yes. It watches HTTP calls to provider hosts, so it applies to any AI traffic, agentic or a plain sequence of calls.
laravel-ai-circuit-breaker is MIT licensed and part of a family of Laravel AI-security packages: ai-lint for exposed keys in source, the egress guard for secrets and PII leaving in prompts, and a processing ledger for GDPR records. The breaker is the one that protects the budget.
This is defensive tooling, not financial or legal advice, and not affiliated with or endorsed by Laravel, Laravel LLC, or any AI provider.
A senior engineer reviews your agentic code, spend controls, and error handling. Fixed price, every finding with a fix.