Pannes en production
limite de récursion de 25 langgraph
Script de reproduction · langgraph-recursion-repro.py · mis à jour le 2026-08-05
Si vous lisez que la limite de récursion de LangGraph est 25, ce chiffre est périmé. Mesuré sur langgraph 1.2.10, la valeur par défaut est 10007, et create_agent dans langchain 1.3.14 fixe 9999. Cette inversion compte plus que l'erreur elle-même : sur une version récente, votre garde-fou est pratiquement supprimé, donc un agent en boucle ne plante plus tôt, il vous facture d'abord.
Le symptôme
L'exception, verbatim depuis une exécution de l'artefact de cette page :
langgraph.errors.GraphRecursionError: Recursion limit of 10007 reached without
hitting a stop condition. You can increase the limit by setting the
`recursion_limit` config key.
For troubleshooting, visit: https://docs.langchain.com/oss/python/langgraph/errors/GRAPH_RECURSION_LIMIT
GraphRecursionError s'importe depuis langgraph.errors. La même chaîne apparaît dans le portage JavaScript et dans les projets qui embarquent LangGraph en aval, ce qui explique pourquoi la recherche renvoie des résultats de dépôts que vous n'avez jamais utilisés.
Le chiffre dans le message est le diagnostic. Lisez-le en premier, car il indique quelle couche a arrêté l'exécution :
| Ce que vous voyez | Ce que ça signifie |
|---|---|
| 10007 | Valeur par défaut de LangGraph en 1.2.x. Rien dans votre stack n'a posé de limite |
| 9999 | create_agent l'a fixée. C'est sa valeur intégrée |
| 25 | Quelque chose a explicitement posé 25, ou vous êtes sur une ancienne version |
| la valeur que vous avez fixée | Bien, votre réglage a été pris en compte |
| une valeur que vous n'avez pas fixée | Quelque chose en aval vous a écrasé |
L'origine de 10007 mérite d'être connue, car c'est aussi le levier le moins coûteux de toute cette page. Dans langgraph/_internal/_config.py :
DEFAULT_RECURSION_LIMIT = int(getenv("LANGGRAPH_DEFAULT_RECURSION_LIMIT", "10007"))
Une variable d'environnement fixe le plancher pour chaque graphe du processus, sans modifier aucun point d'appel. Vérifié : avec LANGGRAPH_DEFAULT_RECURSION_LIMIT=40, un graphe qui ne fixe rien s'arrête à 40.
Le fameux 25 est un docstring jamais mis à jour. Dans langchain_core/runnables/config.py, le champ recursion_limit est toujours documenté ainsi : "Maximum number of times a call can recurse. If not provided, defaults to 25." C'est cette ligne que les gens citent dans les réponses de forum, et elle ne décrit plus aucune valeur par défaut au runtime. Si vous cherchez un 25 aujourd'hui, c'est qu'il a été posé exprès quelque part dans votre stack : trouvez l'origine plutôt que de l'augmenter.
Un dernier point que le message dissimule : le mot récursion est trompeur. Rien ne récurse au sens Python. Le compteur s'incrémente par super-step, un passage à travers les nœuds actifs du graphe, pas par tool call et pas par appel de modèle. Un cycle à deux nœuds brûle le budget deux fois plus vite qu'on ne le lit. Dans un rapport de production, un sous-agent effectuant 10 appels au modèle et 15 tool calls a atteint son plafond exactement à 25 opérations, parce que c'était 25 super-steps.
Ce qui le déclenche
Une condition de sortie qu'aucune réponse d'outil ne peut satisfaire. De loin le cas le plus fréquent. Un arc conditionnel renvoie vers le worker tant qu'un champ n'est pas positionné, et ce champ est censé être positionné par un outil qui retourne une valeur subtilement incorrecte : une liste vide au lieu de null, la chaîne "false" au lieu d'un booléen, un dict dont une clé a été renommée par une mise à jour du fournisseur. Le graphe fait exactement ce qu'on lui a dit ; la sortie est simplement inatteignable. C'est le cas que l'artefact reproduit, délibérément sans LLM, pour que vous puissiez observer le mécanisme sans autre variable.
Un modèle qui rejoue le même appel échoué. L'outil échoue, le modèle l'interprète comme transitoire et retente avec des arguments quasi identiques. Rien dans la boucle par défaut ne détecte que l'état n'a pas avancé. De l'extérieur, ça ressemble à l'agent qui travaille ; dans le relevé de tokens, c'est une fuite.
Un fan-out qui multiplie les étapes. Les branches parallèles et les invocations de sous-graphes puisent dans le même budget. Un graphe qui se termine confortablement sur une entrée peut dépasser le plafond sur une entrée qui fan-out sur six branches, ce qui rend la panne intermittente et dépendante de l'entrée plutôt que structurelle. Le même fan-out qui multiplie les super-steps ici multiplie les requêtes simultanées vers l'API du modèle, ce qui est la façon habituelle dont une rafale de sous-agents se heurte à un mur de 529 overloaded_error.
Une limite que vous avez fixée mais qui n'a jamais été appliquée. Celle qui fait perdre le plus de temps, parce que l'opérateur est certain que le réglage est en place. Trois mécanismes distincts, et l'artefact prouve le premier.
Elle est imbriquée dans la mauvaise clé. recursion_limit est une clé de config autonome ; la documentation est explicite : elle "should not be passed inside the configurable key as all other user-defined configuration." L'imbriquer la fait ignorer sans avertissement. Dans l'artefact, le cas C passe {"configurable": {"recursion_limit": 500}} et s'arrête à 10007, le même chiffre que si on ne fixait rien. Ce chiffre identique est votre indicateur.
Elle est écrasée en aval. Un appelant peut envoyer une config qui prend le dessus sur la vôtre. Un cas rapporté avait fixé la limite sur l'objet agent et voyait quand même 25, parce que le SDK appelant envoyait sa propre config au niveau assistant ; le correctif était du côté de l'appelant, pas de l'agent. Si vous invoquez via un runtime hébergé ou un SDK front-end, considérez que l'appelant l'emporte jusqu'à preuve du contraire.
Elle n'est pas propagée à travers une limite d'imbrication. Un parent configuré à 300 avait ses sous-agents qui retombaient à la valeur par défaut parce que l'invocation du sous-agent ne passait aucune config : await subagent.ainvoke(subagent_state). Pire, la mort du sous-agent remontait comme une tâche annulée plutôt que comme une erreur de récursion, ce qui vous envoie chercher au mauvais endroit. Ce bug spécifique est corrigé, mais le schéma est général : chaque limite d'imbrication est un endroit où la config peut être perdue, et le symptôme mute en remontant.
Distinguer les cas
Avant de changer quoi que ce soit, répondez à une question : l'état a-t-il avancé ?
Un graphe en boucle revisite le même nœud avec un état matériellement identique. Une tâche légitimement longue le revisite avec un état qui grandit ou change. C'est facile à observer, car le compteur d'étapes est disponible dans n'importe quel nœud :
def my_node(state, config):
step = config["metadata"]["langgraph_step"]
...
Loggez le numéro d'étape à côté d'une empreinte des champs que lit votre condition de sortie. Une empreinte identique sur trois visites consécutives signifie une boucle, et aucun plafond ne la corrigera. Une empreinte qui change à chaque passage signifie une tâche longue, et le plafond est réellement trop bas.
LangGraph expose aussi une valeur gérée RemainingSteps, pour qu'un nœud puisse voir combien de budget il lui reste et dégrader volontairement (retourner la meilleure réponse partielle, router vers un nœud de résumé) au lieu de mourir à la limite. Pour tout ce qui est visible utilisateur, c'est préférable à une exception.
Un piège lors du diagnostic : la limite n'est pas toujours appliquée comme prévu. Un rapport sur langgraph==0.5.3 avec langgraph-prebuilt==0.5.2 montre un agent prebuilt avec une limite de 8 qui s'arrête silencieusement après 3 tool calls sans rien lever, apparemment quand le dernier message en état est un message d'outil. Si votre agent s'arrête tôt et silencieusement, ne supposez pas que le plafond en est la cause : vérifiez d'abord si quelque chose a été levé.
Le correctif
L'ordre ci-dessous est délibéré, et c'est presque l'inverse du conseil habituel.
Fixez d'abord un vrai plafond. Sur une version récente, vous tournez avec un budget de boucle effectivement non borné. 10007 super-steps d'un graphe qui lit 40k tokens par passage, c'est une facture, pas un garde-fou. Choisissez un chiffre qui reflète votre pire tâche légitime et appliquez-le à l'ensemble du processus :
LANGGRAPH_DEFAULT_RECURSION_LIMIT=40 python your_app.py
Cette seule variable vous rend le filet de sécurité que la mise à jour avait supprimé, sans toucher un seul point d'appel. Si vous ne pouvez pas estimer le coût maximum en tokens de votre plafond actuel, c'est que vous n'avez pas de plafond.
Rendez la condition de sortie provablement atteignable. Vérifiez la forme que lit votre routeur au moment où l'outil retourne, pas au moment où le routeur s'exécute. Un outil qui peut retourner trois formes retournera tôt ou tard celle que votre condition ne sait pas gérer.
Brisez explicitement le cycle de retry. Gardez un compteur, ou un hash du dernier tool call, dans l'état, et routez vers un chemin d'échec quand il se répète. La boucle par défaut n'a aucune mémoire du non-progrès ; c'est à vous de lui en donner une. C'est le seul correctif qui borne le coût plutôt que de le différer.
Ensuite, et seulement ensuite, fixez une limite par appel là où vous avez besoin de plus de marge. Comme valeur par défaut sur le runnable, sous forme de dict :
agent = create_agent(model=model, tools=tools).with_config({"recursion_limit": 50})
Ou par invocation, comme clé autonome :
graph.invoke(inputs, config={"recursion_limit": 50})
Deux notes opérationnelles. Fixer la limite depuis le CLI ou un runtime hébergé n'est pas couvert par la documentation au moment de cette écriture ; la demande ouverte est dans les sources. Et chaque limite d'imbrication a besoin de sa propre vérification : parent, sous-graphe, sous-agent. La propagation n'est pas automatique.
Vérifier le correctif
Abaissez la limite à 2 et confirmez que l'exception se lève. Cela prouve que le réglage est câblé. C'est l'étape que presque tout le monde saute, et c'est pourquoi la plupart des rapports "j'avais déjà fixé ça" existent. Si 2 ne lève rien, passer à 500 n'aurait rien changé non plus.
Exécutez l'artefact à côté de votre propre graphe. Il ne nécessite ni modèle, ni clé, ni réseau, donc il sépare un problème de mécanisme d'un problème de fournisseur en quelques secondes. Vérifiez que son cas A et son cas C affichent le même chiffre : si c'est aussi le cas sur votre version, le piège de la clé imbriquée est actif dans votre stack.
Puis loggez le compteur d'étapes et l'empreinte de la condition de sortie en production pendant une semaine. Le chiffre à surveiller n'est pas la limite ; c'est la distance entre votre exécution typique et elle. Si le nombre maximum d'étapes par exécution dérive vers le haut pendant que vos entrées restent identiques, vous avez une fuite lente qui finira par trouver quel que soit le plafond que vous fixez.
Sources
Chaque affirmation factuelle ici provient soit d'une exécution de l'artefact, soit d'un cas rapporté listé dans le champ sources de la page : le problème original de recursion limit et ses duplicates en aval, l'agent prebuilt qui ne lève pas, le portage JavaScript où la config est ignorée, la demande ouverte de documentation pour l'usage CLI, le bug de propagation dans les sous-agents et son impact en production, les deux threads de forum sur la factory d'agents actuelle, et les références à l'erreur LangGraph et à la Graph API pour la règle de clé autonome et le compteur d'étapes.
Les valeurs par défaut et le comportement de la variable d'environnement ont été mesurés sur langgraph 1.2.10 et langchain 1.3.14 le 2026-08-05, en lisant DEFAULT_RECURSION_LIMIT dans langgraph/_internal/_config.py, la config de create_agent dans langchain/agents/factory.py, et en exécutant l'artefact. Ces chiffres dépendent de la version et évolueront. L'artefact affiche vos propres valeurs, ce qui justifie son existence.
Cas rapportés
- github.com/langchain-ai/langgraph/issues/148
- github.com/langchain-ai/langgraph/issues/5548
- github.com/langchain-ai/langgraphjs/issues/1524
- github.com/langchain-ai/docs/issues/1121
- github.com/langchain-ai/deepagents/issues/1698
- forum.langchain.com/t/how-to-set-recursion-limit-for-create-agent-v1/1905
- forum.langchain.com/t/i-had-set-recursion-limit-100-but-got-error-recursion-limit-of-25-reached/2569
- docs.langchain.com/oss/python/langgraph/errors/GRAPH_RECURSION_LIMIT
- docs.langchain.com/oss/python/langgraph/graph-api
Juste votre email, rien d’autre à remplir. Alex vous répond.