fix: cap Redis connection pool max_connections to prevent unbounded growth

Without a cap, redis-py defaults to 2^31 max connections (effectively
unlimited). Under high concurrency, this causes unbounded growth of
lock/MutexValue objects and file descriptors — a secondary memory leak
that contributes to worker RSS growth.

Fix: Default max_connections to 100 (configurable via
DEFAULT_REDIS_MAX_CONNECTIONS env var). Applied in both URL-based and
kwargs-based connection pool creation paths. Users who explicitly set
max_connections in their Redis config still get their custom value.

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-02-27 05:18:24 +00:00
parent d931577678
commit ab4899c318
2 changed files with 28 additions and 4 deletions

View file

@ -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
)

View file

@ -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))