Reference leak of runnable tools #509

Closed
opened 2026-02-20 17:40:29 -05:00 by yindo · 0 comments
Owner

Originally created by @blafab-hg on GitHub (Mar 14, 2025).

Checked other resources

  • This is a bug, not a usage question. For questions, please use GitHub Discussions.
  • I added a clear and detailed title that summarizes the issue.
  • I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
  • I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue.

Example Code

from typing import Annotated, Any, Optional
import os
import dotenv
import objgraph
from langchain_openai import AzureChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from typing_extensions import TypedDict

from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_core.tools.retriever import create_retriever_tool
from langchain_core.retrievers import BaseRetriever
from langchain_core.documents import Document
from langchain_core.callbacks import Callbacks

dotenv.load_dotenv()


class FakeRetriever(BaseRetriever):
    def _get_relevant_documents(
        self,
        query: str,
        *,
        callbacks: Callbacks = None,
        tags: Optional[list[str]] = None,
        metadata: Optional[dict[str, Any]] = None,
        **kwargs: Any,
    ) -> list[Document]:
        return [Document(page_content="foo"), Document(page_content="bar")]

    async def _aget_relevant_documents(
        self,
        query: str,
        *,
        callbacks: Callbacks = None,
        tags: Optional[list[str]] = None,
        metadata: Optional[dict[str, Any]] = None,
        **kwargs: Any,
    ) -> list[Document]:
        return [Document(page_content="foo"), Document(page_content="bar")]


class State(TypedDict):
    messages: Annotated[list, add_messages]


def do_query(query: str):
    graph_builder = StateGraph(State)

    tools = [
        create_retriever_tool(
            retriever=FakeRetriever(),
            name="Testtool",
            description="Always use this tool!",
        ),
    ]
    llm = AzureChatOpenAI(
        azure_deployment="my-deployment",
        openai_api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
    )
    llm_with_tools = llm.bind_tools(tools)

    def chatbot(state: State):
        return {"messages": [llm_with_tools.invoke(state["messages"])]}

    graph_builder.add_node("chatbot", chatbot)

    tool_node = ToolNode(tools=tools)
    graph_builder.add_node("tools", tool_node)

    graph_builder.add_conditional_edges(
        "chatbot",
        tools_condition,
    )
    graph_builder.add_edge("tools", "chatbot")
    graph_builder.set_entry_point("chatbot")
    memory = MemorySaver()
    graph = graph_builder.compile(checkpointer=memory)

    events = graph.stream(
        {"messages": [{"role": "user", "content": query}]},
        {"configurable": {"thread_id": "2"}},
        stream_mode="values",
    )
    for event in events:
        event["messages"][-1].pretty_print()


if __name__ == "__main__":
    import objgraph
    import gc

    while True:
        query = input("Enter your query: ")
        do_query(query)
        gc.collect()
        alive_tool_count = len(objgraph.by_type("langchain_core.tools.simple.Tool"))
        print(f"Alive tools: {alive_tool_count}")

Error Message and Stack Trace (if applicable)

You can see that the number of alive tools continues to go up with every query.

Description

We encountered the issue with stray database connections that were kept open. I debugged the issue to the point where we found that the retriever is somehow kept alive by langgraph and therefore its database connection is never closed. I created the minimal reproducible setup that manages to show the issue.

I found that that if I remove these lines: https://github.com/langchain-ai/langgraph/blob/main/libs/langgraph/langgraph/pregel/utils.py#L49
The issue does not occur anymore. So it seems somewhere you are holding onto the result of this operation indefinetely.

Searching for PRs that dealt with this function I found this one:
https://github.com/langchain-ai/langgraph/pull/3255/files

It sounds like it could be the source of the issue, maybe the cache holds onto the subgraph?

System Info

System Information

OS: Linux
OS Version: #1 SMP PREEMPT Sat, 17 Oct 2020 13:30:37 +0000
Python Version: 3.13.1 (main, Dec 4 2024, 18:05:56) [GCC 14.2.1 20240910]

Package Information

langchain_core: 0.3.31
langchain: 0.3.15
langchain_community: 0.3.15
langsmith: 0.3.9
langchain_aws: 0.2.13
langchain_openai: 0.3.1
langchain_text_splitters: 0.3.5
langgraph_sdk: 0.1.53
langgraph: 0.2.69

Optional packages not installed

langserve

Other Dependencies

aiohttp: 3.11.12
async-timeout: Installed. No version info available.
boto3: 1.36.25
dataclasses-json: 0.6.7
httpx: 0.28.1
httpx-sse: 0.4.0
jsonpatch: 1.33
langsmith-pyo3: Installed. No version info available.
numpy: 2.2.3
openai: 1.63.2
orjson: 3.10.15
packaging: 24.2
pydantic: 2.10.6
pydantic-settings: 2.7.1
pytest: 8.1.1
PyYAML: 6.0.2
requests: 2.32.3
requests-toolbelt: 1.0.0
rich: Installed. No version info available.
SQLAlchemy: 2.0.38
tenacity: 9.0.0
tiktoken: 0.9.0
typing-extensions: 4.12.2
zstandard: 0.23.0

Originally created by @blafab-hg on GitHub (Mar 14, 2025). ### Checked other resources - [x] This is a bug, not a usage question. For questions, please use GitHub Discussions. - [x] I added a clear and detailed title that summarizes the issue. - [x] I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example). - [x] I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue. ### Example Code ```python from typing import Annotated, Any, Optional import os import dotenv import objgraph from langchain_openai import AzureChatOpenAI from langchain_community.tools.tavily_search import TavilySearchResults from langchain_core.messages import BaseMessage from typing_extensions import TypedDict from langgraph.checkpoint.memory import MemorySaver from langgraph.graph import StateGraph from langgraph.graph.message import add_messages from langgraph.prebuilt import ToolNode, tools_condition from langchain_core.tools.retriever import create_retriever_tool from langchain_core.retrievers import BaseRetriever from langchain_core.documents import Document from langchain_core.callbacks import Callbacks dotenv.load_dotenv() class FakeRetriever(BaseRetriever): def _get_relevant_documents( self, query: str, *, callbacks: Callbacks = None, tags: Optional[list[str]] = None, metadata: Optional[dict[str, Any]] = None, **kwargs: Any, ) -> list[Document]: return [Document(page_content="foo"), Document(page_content="bar")] async def _aget_relevant_documents( self, query: str, *, callbacks: Callbacks = None, tags: Optional[list[str]] = None, metadata: Optional[dict[str, Any]] = None, **kwargs: Any, ) -> list[Document]: return [Document(page_content="foo"), Document(page_content="bar")] class State(TypedDict): messages: Annotated[list, add_messages] def do_query(query: str): graph_builder = StateGraph(State) tools = [ create_retriever_tool( retriever=FakeRetriever(), name="Testtool", description="Always use this tool!", ), ] llm = AzureChatOpenAI( azure_deployment="my-deployment", openai_api_version=os.getenv("AZURE_OPENAI_API_VERSION"), ) llm_with_tools = llm.bind_tools(tools) def chatbot(state: State): return {"messages": [llm_with_tools.invoke(state["messages"])]} graph_builder.add_node("chatbot", chatbot) tool_node = ToolNode(tools=tools) graph_builder.add_node("tools", tool_node) graph_builder.add_conditional_edges( "chatbot", tools_condition, ) graph_builder.add_edge("tools", "chatbot") graph_builder.set_entry_point("chatbot") memory = MemorySaver() graph = graph_builder.compile(checkpointer=memory) events = graph.stream( {"messages": [{"role": "user", "content": query}]}, {"configurable": {"thread_id": "2"}}, stream_mode="values", ) for event in events: event["messages"][-1].pretty_print() if __name__ == "__main__": import objgraph import gc while True: query = input("Enter your query: ") do_query(query) gc.collect() alive_tool_count = len(objgraph.by_type("langchain_core.tools.simple.Tool")) print(f"Alive tools: {alive_tool_count}") ``` ### Error Message and Stack Trace (if applicable) ```shell You can see that the number of alive tools continues to go up with every query. ``` ### Description We encountered the issue with stray database connections that were kept open. I debugged the issue to the point where we found that the retriever is somehow kept alive by langgraph and therefore its database connection is never closed. I created the minimal reproducible setup that manages to show the issue. I found that that if I remove these lines: https://github.com/langchain-ai/langgraph/blob/main/libs/langgraph/langgraph/pregel/utils.py#L49 The issue does not occur anymore. So it seems somewhere you are holding onto the result of this operation indefinetely. Searching for PRs that dealt with this function I found this one: https://github.com/langchain-ai/langgraph/pull/3255/files It sounds like it could be the source of the issue, maybe the cache holds onto the subgraph? ### System Info System Information ------------------ > OS: Linux > OS Version: #1 SMP PREEMPT Sat, 17 Oct 2020 13:30:37 +0000 > Python Version: 3.13.1 (main, Dec 4 2024, 18:05:56) [GCC 14.2.1 20240910] Package Information ------------------- > langchain_core: 0.3.31 > langchain: 0.3.15 > langchain_community: 0.3.15 > langsmith: 0.3.9 > langchain_aws: 0.2.13 > langchain_openai: 0.3.1 > langchain_text_splitters: 0.3.5 > langgraph_sdk: 0.1.53 > langgraph: 0.2.69 Optional packages not installed ------------------------------- > langserve Other Dependencies ------------------ > aiohttp: 3.11.12 > async-timeout: Installed. No version info available. > boto3: 1.36.25 > dataclasses-json: 0.6.7 > httpx: 0.28.1 > httpx-sse: 0.4.0 > jsonpatch: 1.33 > langsmith-pyo3: Installed. No version info available. > numpy: 2.2.3 > openai: 1.63.2 > orjson: 3.10.15 > packaging: 24.2 > pydantic: 2.10.6 > pydantic-settings: 2.7.1 > pytest: 8.1.1 > PyYAML: 6.0.2 > requests: 2.32.3 > requests-toolbelt: 1.0.0 > rich: Installed. No version info available. > SQLAlchemy: 2.0.38 > tenacity: 9.0.0 > tiktoken: 0.9.0 > typing-extensions: 4.12.2 > zstandard: 0.23.0
yindo closed this issue 2026-02-20 17:40:29 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#509