From 12e4ffa39cc69e7ee05b9ac896c9576e543b20c4 Mon Sep 17 00:00:00 2001 From: Jaeyeon Kim Date: Thu, 19 Mar 2026 16:15:34 +0900 Subject: [PATCH] fix: close aiohttp sessions on proxy shutdown to prevent fd leaks The proxy was not explicitly closing aiohttp.ClientSession instances during shutdown, causing "Unclosed client session" warnings during GC and potential fd leaks in long-running deployments. Changes: - Add close_litellm_async_clients() call to proxy shutdown path so the global base_llm_aiohttp_handler and cached async clients are cleaned up properly (not just via atexit). - Add async_close() to presidio guardrail so its cached aiohttp sessions are closed during proxy shutdown. - Iterate litellm.callbacks on shutdown and call async_close() on any callback that implements it, providing a general cleanup hook for guardrails and other callbacks that hold resources. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../guardrails/guardrail_hooks/presidio.py | 4 ++++ litellm/proxy/proxy_server.py | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index 0f4ebbd4880..bb860789939 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -244,6 +244,10 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): await session.close() self._loop_sessions.clear() + async def async_close(self) -> None: + """Close resources held by this guardrail (called on proxy shutdown).""" + await self._close_http_session() + def __del__(self): """Cleanup: we try to close, but doing async cleanup in __del__ is risky.""" pass diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 9c29927c5cb..cbe145a4514 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -963,6 +963,27 @@ async def proxy_startup_event(app: FastAPI): # noqa: PLR0915 except Exception as e: verbose_proxy_logger.error(f"Error closing shared aiohttp session: {e}") + # Shutdown event - close cached async HTTP clients (base_llm_aiohttp_handler, etc.) + try: + from litellm.llms.custom_httpx.async_client_cleanup import ( + close_litellm_async_clients, + ) + + await close_litellm_async_clients() + verbose_proxy_logger.info("Closed cached async HTTP clients") + except Exception as e: + verbose_proxy_logger.error(f"Error closing cached async HTTP clients: {e}") + + # Shutdown event - close guardrail/callback resources (e.g. presidio aiohttp sessions) + for callback in litellm.callbacks: + if hasattr(callback, "async_close") and callable(callback.async_close): + try: + await callback.async_close() + except Exception as e: + verbose_proxy_logger.error( + f"Error closing callback {type(callback).__name__}: {e}" + ) + # Shutdown event - stop RDS IAM token refresh background task if ( prisma_client is not None