Skip to content

AsyncClient.aclose() task destroyed during cleanup - Missing LLM client cleanup in Runner.close() #3550

@junseon-yoo

Description

@junseon-yoo

Describe the bug

When running ADK agents in production with google-genai>=1.50.1, I'm seeing recurring asyncio errors about tasks being destroyed while pending during cleanup. The error happens when the genai AsyncClient.aclose() method is called during garbage collection.

This occurs because the Gemini class uses @cached_property for api_client and _live_api_client, but these clients are never explicitly closed in Runner.close(). Starting from genai 1.50.1, custom httpx clients won't be closed automatically, so when the Gemini instance is garbage collected, the aclose() task starts but gets destroyed before completion.

To Reproduce

Steps to reproduce the behavior:

  1. Install google-adk==1.18.0 and google-genai==1.50.1
  2. Run the following minimal code:
"""
Minimal reproduction for ADK Issue #3550

Bug: AsyncClient.__del__ creates orphaned asyncio tasks when Runner.close()
is not called before event loop shutdown.

Setup environment variables:
  export GOOGLE_GENAI_USE_VERTEXAI=true
  export GOOGLE_CLOUD_PROJECT="your-project-id"
  export GOOGLE_CLOUD_LOCATION="your-location"

"""
import asyncio
import gc

from google.adk.agents import LlmAgent
from google.adk.models.google_llm import Gemini
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types


async def test():
    agent = LlmAgent(
        model=Gemini(model="gemini-2.0-flash-exp"),
        name="test"
    )

    runner = Runner(
        app_name="test",
        agent=agent,
        session_service=InMemorySessionService()
    )

    session = await runner.session_service.create_session(
        app_name="test",
        user_id="user1"
    )

    content = types.Content(
        role='user',
        parts=[types.Part(text='hello')]
    )

    async for event in runner.run_async(
        user_id="user1",
        session_id=session.id,
        new_message=content
    ):
        pass

    # Bug: NOT calling await runner.close()
    del runner
    gc.collect()


if __name__ == '__main__':
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    loop.set_debug(True)

    loop.run_until_complete(test())

    pending = asyncio.all_tasks(loop)
    print(f"\nOrphaned tasks: {len(pending)}")

    loop.close()
  1. Run the script
  2. Error stacktrace:
Orphaned tasks: 2
Task was destroyed but it is pending!
source_traceback: Object created at (most recent call last):

Expected behavior

Runner.close() should clean up all resources including LLM client connections, and no asyncio warnings should appear during cleanup.

Screenshots

N/A

Desktop (please complete the following information):

  • OS: Linux (production), macOS (local)
  • Python version: Python 3.11
  • ADK version: 1.18.0

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: gemini-2.5-flash

Additional context

The issue is in runners.py:1298-1300 where Runner.close() only cleans up toolsets:

async def close(self):
    """Closes the runner."""
    await self._cleanup_toolsets(self._collect_toolset(self.agent))
    # Missing: LLM client cleanup

Root cause in google/adk/models/google_llm.py:

  • Line 196-208: api_client cached property is never closed
  • Line 240-246: _live_api_client cached property is never closed

Related to #1112 and #2804 but specifically affects LLM clients rather than MCP toolsets.

Workaround attempts that didn't work:

  • Upgrading genai 1.49.0 → 1.50.1
  • Adding shutdown cleanup in FastAPI (only fixes app shutdown, not per-request cleanup)
  • Log filtering (masks the symptom)

Proposed fix following the existing _cleanup_toolsets pattern:

  1. Add BaseLlm.aclose() method (default no-op for backward compatibility)
  2. Implement Gemini.aclose() to close cached clients
  3. Add Runner._collect_llm_models() to gather LLM instances
  4. Add Runner._cleanup_llm_models() to close them
  5. Update Runner.close() to cleanup both toolsets and LLM clients

Metadata

Metadata

Labels

core[Component] This issue is related to the core interface and implementationplanned[Status] This issue is planned to be work on by ADK eng team

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions