langgraph.errors.GraphRecursionError: Recursion limit of 25 reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key.
That is LangGraph telling you your graph ran out of super-steps before any node said stop. The 25 is not a constant in the error text — it is the limit that was in force, printed back at you. The message offers one fix, raising the key, and that fix is often right and almost never sufficient, because the limit is the only thing in the system that was prepared to end an unbounded loop.
The message is built in LangGraph’s Pregel runtime, in both the synchronous and the streaming exit paths, and the number is interpolated from your own config:
if loop.status == "out_of_steps": msg = create_error_message( message=( f"Recursion limit of {config['recursion_limit']} reached " "without hitting a stop condition. You can increase the " "limit by setting the `recursion_limit` config key." ), error_code=ErrorCode.GRAPH_RECURSION_LIMIT, ) raise GraphRecursionError(msg)
Quoted verbatim from LangGraph’s own source, pinned to the commit it was read at: libs/langgraph/langgraph/pregel/main.py at 157a06dd, where it appears twice — once on the invoke path and once on the stream path. The default of 25 is DEFAULT_RECURSION_LIMIT in langchain_core.runnables.config, whose RunnableConfig documents recursion_limit as “If not provided, defaults to 25.”
Two things follow from reading it rather than guessing at it. The error class is GraphRecursionError, which the library defines as a subclass of Python’s own RecursionError — so a bare except RecursionError somewhere in your stack will swallow it and leave you looking for a stack-depth bug you do not have. And the 25 is the default of recursion_limit in the RunnableConfig your graph inherits, not a LangGraph-specific number: if your message says 25, nobody set it, and if it says something else, somebody did.
The loop keeps one counter and one ceiling. It stops when the counter passes the ceiling, and the ceiling is set from your limit when the loop starts. The counter advances once per tick of the loop — one super-step — however many nodes that tick executed, including nodes that ran in parallel. So a fan-out of ten parallel nodes costs one step, and a two-node cycle that goes round thirteen times costs thirteen.
This cuts both ways when you are reading your own graph. People hit this error and assume a runaway agent making hundreds of model calls; often the shape is a short cycle — model, tool, back to the model — that simply needs more rounds than 25 for the task it was given. And people on the other side assume they are safe because their node count is small, while a conditional edge that can route back to its own source has no bound at all except this one.
The library is explicit about what the limit is for: the class docstring says it exists to prevent infinite loops. A graph with a cycle and no reachable stop condition will exhaust 1000 exactly as willingly as it exhausted 25 — later, for more money, with a bigger transcript to read afterwards. So the question raising the number answers is “was 25 simply too few for this task?”, and it is a good question. Multiply out the work the way you would multiply out any other loop: rounds needed per item, times items. If the answer is 40 and your limit is 25, raise it and move on; you had an arithmetic problem.
If you cannot do that multiplication — if the honest answer is “however many it takes” — then you do not have a cap that is too low, you have a loop with no termination argument, and the limit is the only part of the system that currently knows that. Raising it there converts a failure you saw at step 25 into the same failure, later. The thing to add is the stop condition the message says you never hit: a counter in state that your conditional edge reads, a node that routes to END when the work is done or when the attempt budget is spent.
The expensive part of this error is not that the run stopped. It is that twenty-five steps of finished work went with it. LangGraph will keep that work for you, and the mechanism is the checkpointer.
Compile the graph with a checkpointer and invoke it with a thread id. Each super-step’s state is written as it completes, so the work that finished is on disk before the step that failed. Then look at what the loop does when it picks a thread back up: it reads the step number out of the saved checkpoint metadata, sets its counter to the step after it, and recomputes the ceiling from there — the budget is measured from the resumed step, not from the beginning of the thread. A re-invocation of the same thread therefore continues from the last completed super-step with its own full allowance, rather than replaying the twenty-five steps you already paid for.
Two caveats that matter more than the mechanism. This only helps if your nodes write their results into graph state as they go; a node that accumulates in a local variable and returns everything at the end has nothing to checkpoint until it is already finished. And a fresh budget on resume is a resumption, not an exemption: if the loop has no stop condition, every resume will end the same way. It buys you the finished work back, which is the thing you actually lost.
There is a written guide: the turn arithmetic as a formula you can run against a brief before you launch it, the reasons raising a cap does not finish the job — including the one where it turns a loud failure into a silent one — and batch.py, one standard-library file that collapses a per-item loop into a single pass. It is $19. The only working way to pay for it today is 19 USDC on Base, and delivery from that page is manual: you email the transaction hash and the files come back as a reply. Everything on this page is free and ungated, and this page sells nothing by itself.
The short version: the 25 is your recursion_limit printed back at you, it counts super-steps rather than model calls, and the message’s own suggested fix only helps if you can multiply out how many rounds the task needs. If you cannot, add the stop condition the error says you never hit. Either way, compile with a checkpointer first, so the next time it stops you are resuming rather than starting again.
This page counts anonymous readership. Each load sends the page path, the address of the page you came from (our server keeps only its domain), how long the page was open, whether you scrolled, and any campaign or outreach code in the link you followed; a second “engaged” event is sent once, ten seconds after the page opens — whether or not the tab is in front of you — or as soon as you scroll a quarter of it. The campaign codes from the first link you arrived on are kept in this browser’s local storage, and a later visit that arrives with no codes of its own is counted against them, outreach code included; a link carrying its own codes is used for that visit, and the stored first touch is never replaced. If a link carries a different outreach code, that later touch is recorded alongside the first rather than in place of it. The referrer of that first visit is stored in the same place and is never sent anywhere — every visit sends the referrer it actually had. A return visit is still counted, and an outreach code is removed from the address bar after it is read. The page also asks this domain for Vercel’s analytics script; on 26 September 2026 we requested that address on each of the five hosts we publish — lilulab.ai, willcall, ninemin, ghmirror and pinpoint — and every one returned HTTP 404 and no script, so no script from another company was served to your browser and none ran. The page still asks for it on every load, so this stops being true the moment that address starts answering, without a single byte of this page changing — and this page will not know it has, and will go on saying what it says. Whether Vercel counts the request at its own edge is its record and not ours; as of 26 September 2026 no one here had opened it, and we keep no copy of it. No cookie; the local storage above does that job. Some things are recorded that the list above does not name. Your browser and the network attach these to the request rather than the page sending them: the identification string your browser gives with every request; the two-letter country the network assigns your address — no city, and your address itself is never stored; and the site address you asked for. Our own server then writes its own bookkeeping about the record: the date and time of your visit, to the thousandth of a second, from our clock; which request header it took that site address from; and a number naming the record format it wrote. It also stores the domain your browser said the count was fired from, which for a normal load of this page is this site itself, and which our server records as the word “unknown” when the browser sends nothing it can read. Every row is kept in Vercel’s blob storage and not on a machine of our own, measured 26 September 2026 by reading the handler. The page sends how long it was open twice, from two different counters, and our server reads only one of the two names — so some rows carry it and some are empty (measured 26 September 2026). This page reads an outreach code from the address if one is present and keeps it in your browser, which would let us tell one reader from another — but we have sent no link for this page and hold no list of recipients for it, so as of 2 October 2026 there is no name for any code to resolve to. This page also loads a second counter of its own. It sends its own copy of the view event on every load, so one load writes two view rows and any rate measured against them reads half its true value; it also measures the time differently, counting only the seconds the page was actually in front of you where the first counter sends wall-clock time since the page opened. It also sends an event once you have had the page in front of you for ten seconds and moved the mouse, touched the screen, scrolled or pressed a key. Because both counters send an event called “engaged” under different rules, a single visit can produce two of them. The host that serves it keeps its own request logs; those are its record and not ours. The LangGraph source quoted above is linked and pinned in the notes for machine readers at /llms.txt.