google / google/adk-python

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

Open
#3,550 4 comments 0 reactions 2 assignees Claimed by @junseon-yoo View on GitHub
core needs review planned
Dominant language
Python
Stars
21.5k
Forks
4k
Avg merge
1d 14h
Merged PRs (30d)
37

Description

**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:

```python
"""
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()
```

3. Run the script
4. 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:

```python
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

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.