Set cost limits and a kill switch with LangChain
Stop runaway AI agents in LangChain in 24 minutes: cap model and tool calls, set a dollar budget per run and add a kill switch you control.

What you need
Step 1: Set up the project
4 minA small test project lets you try every limit before you add them to a real agent.
- Run the commands below. On Windows, activate the environment with
.venv\Scripts\activateinstead. - Create an API key at platform.claude.com/settings/keys.
- Create a
.envfile withANTHROPIC_API_KEY=followed by your key, and add.envto.gitignore.
Run thismkdir agent-limits && cd agent-limits python3 -m venv .venv && source .venv/bin/activate pip install -U langchain langchain-anthropic python-dotenv
You should see the packages installed with no errors. Run
pip show langchainto check that the version starts with 1.Older code that imports AgentExecutor stops working?
LangChain 1.0 moved older, deprecated features to the
langchain-classicpackage. Build new agents withcreate_agent, which runs the middleware in this guide.Keep the API key out of your code and out of any file you share.
- Run the commands below. On Windows, activate the environment with
Step 2: Cap model and tool calls
4 minCall limits end a looping agent after a set number of rounds, whatever each round costs.
- Create
agent.pyand paste in the code below. Thelookup_ordertool stands in for your real tools. run_limitcounts calls in one run. Athread_limitcounts across a whole chat, but it needs a checkpointer.- Set each number a little above what a normal run of your agent uses.
Your agent.py, part 1from dotenv import load_dotenv from langchain.agents import create_agent from langchain.agents.middleware import ModelCallLimitMiddleware, ToolCallLimitMiddleware load_dotenv() def lookup_order(order_id: str) -> str: """Look up the status of one order by its ID.""" return f"Order {order_id}: shipped" # stands in for a real API call limits = [ ModelCallLimitMiddleware(run_limit=15, exit_behavior="end"), ToolCallLimitMiddleware(run_limit=25), ]You should see a
limitslist with a model call limit and a tool call limit.With
exit_behavior="end", the run ends with a message. Set it to"error"if you'd rather catch an exception.- Create
Step 3: Add a dollar budget
5 minCall limits don't know prices, so this middleware adds up what each reply costs and stops the run at your budget.
- Paste the code below under part 1.
- Set
PRICE_INandPRICE_OUTto your model's price per million tokens, from its pricing page. - Replace the
printline with your Slack or email alert.
Your agent.py, part 2from typing import Any from typing_extensions import NotRequired from langchain.agents.middleware import AgentMiddleware, AgentState, hook_config from langchain.messages import AIMessage PRICE_IN, PRICE_OUT = 4.00, 20.00 # US$ per million tokens, Claude Opus 5.5 class BudgetState(AgentState): spent_usd: NotRequired[float] class BudgetMiddleware(AgentMiddleware[BudgetState]): state_schema = BudgetState def __init__(self, max_usd): super().__init__() self.max_usd = max_usd def after_model(self, state, runtime) -> dict[str, Any] | None: usage = state["messages"][-1].usage_metadata or {} cost = (usage.get("input_tokens", 0) * PRICE_IN + usage.get("output_tokens", 0) * PRICE_OUT) / 1_000_000 return {"spent_usd": state.get("spent_usd", 0.0) + cost} @hook_config(can_jump_to=["end"]) def before_model(self, state, runtime) -> dict[str, Any] | None: spent = state.get("spent_usd", 0.0) if spent >= self.max_usd: print(f"ALERT: budget reached, ${spent:.4f} spent") # swap in your Slack or email alert return {"messages": [AIMessage(f"Stopped: the ${self.max_usd} budget is used up.")], "jump_to": "end"} return NoneYou should see a
BudgetMiddlewareclass that keeps a runningspent_usdtotal in the agent's state.The total stays at $0?
The model isn't returning token counts. Print
usage_metadatafrom the last message to check. Some providers, such as OpenAI when streaming, only send counts if you turn them on.The check runs before each model call, so a run can go one call over. Set the budget a little under your real ceiling.
Step 4: Add a kill switch
3 minA kill switch lets you stop a running agent from outside, between steps, without killing the whole program.
- Paste the code below under part 2.
- To stop the agent, create an empty file named
STOPin the project folder. Delete it to let the agent run again.
Your agent.py, part 3from pathlib import Path from langchain.agents.middleware import before_model @before_model(can_jump_to=["end"]) def kill_switch(state, runtime): if Path("STOP").exists(): print("ALERT: kill switch is on, stopping the agent") return {"messages": [AIMessage("Stopped by the kill switch.")], "jump_to": "end"} return NoneYou should see a
kill_switchhook that looks for theSTOPfile before every model call.Creating STOP doesn't stop the agent?
The file must be in the folder you run the script from. If your agents run on several servers, check a shared flag instead, such as a database row.
Step 5: Build the agent with a step limit
3 minA recursion limit is the last backstop: it catches any loop the other limits miss.
- Paste the code below at the end of
agent.py. - Keep
kill_switchfirst inmiddleware, so it runs before the other checks. - Set
recursion_limitto about 10 times your model call limit. Every model call, tool call and hook counts as a step.
Your agent.py, part 4from langgraph.errors import GraphRecursionError agent = create_agent( model="anthropic:claude-opus-5-5", tools=[lookup_order], system_prompt="You help the support team check orders.", middleware=[kill_switch, BudgetMiddleware(max_usd=0.50), *limits], ) try: result = agent.invoke( {"messages": [{"role": "user", "content": "Check orders 1001 to 1040, one at a time."}]}, config={"recursion_limit": 150}, ) print(result["messages"][-1].text) print(f"Spent: ${result.get('spent_usd', 0):.4f}") except GraphRecursionError: print("ALERT: step limit reached, the agent stopped")You should see a finished
agent.py: the agent, its 4 middleware and atryblock around the run.Normal runs end with the step limit message?
The limit is too low for your hooks. Raise
recursion_limituntil only runaway runs reach it. LangGraph's default is 10,007 steps, so always set your own.- Paste the code below at the end of
Step 6: Test each limit in staging
5 minEach limit should stop the agent cleanly and fire its alert before you trust it with real work.
- Set
max_usd=0.01, runpython agent.pyand read the last lines it prints. - Set the budget back to 0.50. Then run the commands below to try the kill switch.
- Set
run_limit=3onModelCallLimitMiddlewareand run the agent once more.
Run this (Mac or Linux)python agent.py touch STOP python agent.py rm STOP
You should see 3 clean stops: the budget message, "Stopped by the kill switch." and a message that the run limit of 3 was reached.
The budget test doesn't stop after the first call?
The check runs before each model call, so a task that needs only one call finishes first. Ask for a task that needs several tool calls, like the 40 orders.
- Set
You get
A LangChain agent that stops at a call limit, a dollar budget or a step limit, plus a switch to stop it by hand
Try next
Did this guide work for you?
Thanks. It helps us fix the steps.
Sources
0 of 6Next: Set up the project


