Skip to content

LearnTutorials

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.

By Recaply EditorsAdvanced24 min6 stepsUpdated

A hand presses a round stop button on a small box next to a stack of coins and an hourglass

What you need

  1. Step 1: Set up the project

    4 min

    A small test project lets you try every limit before you add them to a real agent.

    1. Run the commands below. On Windows, activate the environment with .venv\Scripts\activate instead.
    2. Create an API key at platform.claude.com/settings/keys.
    3. Create a .env file with ANTHROPIC_API_KEY= followed by your key, and add .env to .gitignore.
    Run this
    mkdir 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 langchain to check that the version starts with 1.

    Older code that imports AgentExecutor stops working?

    LangChain 1.0 moved older, deprecated features to the langchain-classic package. Build new agents with create_agent, which runs the middleware in this guide.

    Keep the API key out of your code and out of any file you share.

  2. Step 2: Cap model and tool calls

    4 min

    Call limits end a looping agent after a set number of rounds, whatever each round costs.

    1. Create agent.py and paste in the code below. The lookup_order tool stands in for your real tools.
    2. run_limit counts calls in one run. A thread_limit counts across a whole chat, but it needs a checkpointer.
    3. Set each number a little above what a normal run of your agent uses.
    Your agent.py, part 1
    from 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 limits list 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.

  3. Step 3: Add a dollar budget

    5 min

    Call limits don't know prices, so this middleware adds up what each reply costs and stops the run at your budget.

    1. Paste the code below under part 1.
    2. Set PRICE_IN and PRICE_OUT to your model's price per million tokens, from its pricing page.
    3. Replace the print line with your Slack or email alert.
    Your agent.py, part 2
    from 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 None
    

    You should see a BudgetMiddleware class that keeps a running spent_usd total in the agent's state.

    The total stays at $0?

    The model isn't returning token counts. Print usage_metadata from 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.

  4. Step 4: Add a kill switch

    3 min

    A kill switch lets you stop a running agent from outside, between steps, without killing the whole program.

    1. Paste the code below under part 2.
    2. To stop the agent, create an empty file named STOP in the project folder. Delete it to let the agent run again.
    Your agent.py, part 3
    from 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 None
    

    You should see a kill_switch hook that looks for the STOP file 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.

  5. Step 5: Build the agent with a step limit

    3 min

    A recursion limit is the last backstop: it catches any loop the other limits miss.

    1. Paste the code below at the end of agent.py.
    2. Keep kill_switch first in middleware, so it runs before the other checks.
    3. Set recursion_limit to about 10 times your model call limit. Every model call, tool call and hook counts as a step.
    Your agent.py, part 4
    from 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 a try block around the run.

    Normal runs end with the step limit message?

    The limit is too low for your hooks. Raise recursion_limit until only runaway runs reach it. LangGraph's default is 10,007 steps, so always set your own.

  6. Step 6: Test each limit in staging

    5 min

    Each limit should stop the agent cleanly and fire its alert before you trust it with real work.

    1. Set max_usd=0.01, run python agent.py and read the last lines it prints.
    2. Set the budget back to 0.50. Then run the commands below to try the kill switch.
    3. Set run_limit=3 on ModelCallLimitMiddleware and 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.

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

Did this guide work for you?

Sources