diff --git a/litellm/_redis.py b/litellm/_redis.py index a86ebd9ea9e..948c57de9a9 100644 --- a/litellm/_redis.py +++ b/litellm/_redis.py @@ -18,7 +18,11 @@ import redis # type: ignore import redis.asyncio as async_redis # type: ignore from litellm import get_secret, get_secret_str -from litellm.constants import REDIS_CONNECTION_POOL_TIMEOUT, REDIS_SOCKET_TIMEOUT +from litellm.constants import ( + DEFAULT_REDIS_MAX_CONNECTIONS, + REDIS_CONNECTION_POOL_TIMEOUT, + REDIS_SOCKET_TIMEOUT, +) from litellm.litellm_core_utils.sensitive_data_masker import SensitiveDataMasker from ._logging import verbose_logger @@ -461,15 +465,31 @@ def get_redis_connection_pool(**env_overrides): redis_kwargs = _get_redis_client_logic(**env_overrides) verbose_logger.debug("get_redis_connection_pool: redis_kwargs", redis_kwargs) if "url" in redis_kwargs and redis_kwargs["url"] is not None: - return async_redis.BlockingConnectionPool.from_url( - timeout=REDIS_CONNECTION_POOL_TIMEOUT, url=redis_kwargs["url"] - ) + pool_kwargs = {"timeout": REDIS_CONNECTION_POOL_TIMEOUT, "url": redis_kwargs["url"]} + # MEMORY LEAK FIX: Apply max_connections if user provided it + if "max_connections" in redis_kwargs: + try: + pool_kwargs["max_connections"] = int(redis_kwargs["max_connections"]) + except (TypeError, ValueError): + verbose_logger.warning( + "REDIS: invalid max_connections value %r, using default %d", + redis_kwargs["max_connections"], + DEFAULT_REDIS_MAX_CONNECTIONS, + ) + pool_kwargs["max_connections"] = DEFAULT_REDIS_MAX_CONNECTIONS + else: + pool_kwargs["max_connections"] = DEFAULT_REDIS_MAX_CONNECTIONS + return async_redis.BlockingConnectionPool.from_url(**pool_kwargs) connection_class = async_redis.Connection if "ssl" in redis_kwargs: connection_class = async_redis.SSLConnection redis_kwargs.pop("ssl", None) redis_kwargs["connection_class"] = connection_class redis_kwargs.pop("startup_nodes", None) + # MEMORY LEAK FIX: Ensure max_connections is always set to prevent + # unbounded connection pool growth (redis-py defaults to 2^31) + if "max_connections" not in redis_kwargs: + redis_kwargs["max_connections"] = DEFAULT_REDIS_MAX_CONNECTIONS return async_redis.BlockingConnectionPool( timeout=REDIS_CONNECTION_POOL_TIMEOUT, **redis_kwargs ) diff --git a/litellm/constants.py b/litellm/constants.py index 2e8f3f94f5a..ffb808a2754 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -256,6 +256,10 @@ AZURE_DOCUMENT_INTELLIGENCE_DEFAULT_DPI = int( ) REDIS_SOCKET_TIMEOUT = float(os.getenv("REDIS_SOCKET_TIMEOUT", 0.1)) REDIS_CONNECTION_POOL_TIMEOUT = int(os.getenv("REDIS_CONNECTION_POOL_TIMEOUT", 5)) +# MEMORY LEAK FIX: Default max connections for Redis connection pools. +# Without a cap, redis-py defaults to 2^31 connections (effectively unlimited), +# causing unbounded growth of lock/MutexValue objects and file descriptors. +DEFAULT_REDIS_MAX_CONNECTIONS = int(os.getenv("DEFAULT_REDIS_MAX_CONNECTIONS", 100)) # Default Redis major version to assume when version cannot be determined # Using 7 as it's the modern version that supports LPOP with count parameter DEFAULT_REDIS_MAJOR_VERSION = int(os.getenv("DEFAULT_REDIS_MAJOR_VERSION", 7))