fix: cache SSL contexts to prevent excessive memory allocation (#16955)

Previously, get_ssl_configuration() created a new SSL context on every
call, even when the configuration was identical. This caused continuous
memory allocation from ssl.create_default_context(), especially during:
- Proxy server startup
- Background health checks
- HTTP client creation

Solution:
- Added _ssl_context_cache to cache SSL contexts by configuration
  parameters (cafile, ssl_security_level, ssl_ecdh_curve)
- Refactored SSL context creation into _create_ssl_context() helper
- Modified get_ssl_configuration() to reuse cached contexts when
  configuration matches

This significantly reduces memory allocation while maintaining backward
compatibility. SSL contexts are now reused instead of being recreated
repeatedly, eliminating the memory leak observed in memray profiling.

Fixes memory allocation issue where create_default_context was allocating
6.282MB+ continuously even without any requests.
This commit is contained in:
Alexsander Hamir
2025-11-21 19:11:54 -08:00
committed by GitHub
parent f542011076
commit cdb46f919d
+71 -39
View File
@@ -87,6 +87,60 @@ def _prepare_request_data_and_content(
return request_data, request_content
# Cache for SSL contexts to avoid creating duplicate contexts with the same configuration
# Key: tuple of (cafile, ssl_security_level, ssl_ecdh_curve)
# Value: ssl.SSLContext
_ssl_context_cache: Dict[Tuple[Optional[str], Optional[str], Optional[str]], ssl.SSLContext] = {}
def _create_ssl_context(
cafile: Optional[str],
ssl_security_level: Optional[str],
ssl_ecdh_curve: Optional[str],
) -> ssl.SSLContext:
"""
Create an SSL context with the given configuration.
This is separated from get_ssl_configuration to enable caching.
"""
custom_ssl_context = ssl.create_default_context(cafile=cafile)
# Optimize SSL handshake performance
# Set minimum TLS version to 1.2 for better performance
custom_ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
# Configure cipher suites for optimal performance
if ssl_security_level and isinstance(ssl_security_level, str):
# User provided custom cipher configuration (e.g., via SSL_SECURITY_LEVEL env var)
custom_ssl_context.set_ciphers(ssl_security_level)
else:
# Use optimized cipher list that strongly prefers fast ciphers
# but falls back to widely compatible ones
custom_ssl_context.set_ciphers(DEFAULT_SSL_CIPHERS)
# Configure ECDH curve for key exchange (e.g., to disable PQC and improve performance)
# Set SSL_ECDH_CURVE env var or litellm.ssl_ecdh_curve to 'X25519' to disable PQC
# Common valid curves: X25519, prime256v1, secp384r1, secp521r1
if ssl_ecdh_curve and isinstance(ssl_ecdh_curve, str):
try:
custom_ssl_context.set_ecdh_curve(ssl_ecdh_curve)
verbose_logger.debug(f"SSL ECDH curve set to: {ssl_ecdh_curve}")
except AttributeError:
verbose_logger.warning(
f"SSL ECDH curve configuration not supported. "
f"Python version: {sys.version.split()[0]}, OpenSSL version: {ssl.OPENSSL_VERSION}. "
f"Requested curve: {ssl_ecdh_curve}. Continuing with default curves."
)
except ValueError as e:
# Invalid curve name
verbose_logger.warning(
f"Invalid SSL ECDH curve name: '{ssl_ecdh_curve}'. {e}. "
f"Common valid curves: X25519, prime256v1, secp384r1, secp521r1. "
f"Continuing with default curves (including PQC)."
)
return custom_ssl_context
def get_ssl_configuration(
ssl_verify: Optional[VerifyTypes] = None,
) -> Union[bool, str, ssl.SSLContext]:
@@ -102,6 +156,9 @@ def get_ssl_configuration(
If ssl_security_level is set, it will apply the security level to the SSL context.
SSL contexts are cached to avoid creating duplicate contexts with the same configuration,
which reduces memory allocation and improves performance.
Args:
ssl_verify: SSL verification setting. Can be:
- None: Use default from environment/litellm settings
@@ -128,6 +185,7 @@ def get_ssl_configuration(
ssl_verify = ssl_verify_bool
ssl_security_level = os.getenv("SSL_SECURITY_LEVEL", litellm.ssl_security_level)
ssl_ecdh_curve = os.getenv("SSL_ECDH_CURVE", litellm.ssl_ecdh_curve)
cafile = None
if isinstance(ssl_verify, str) and os.path.exists(ssl_verify):
@@ -140,45 +198,19 @@ def get_ssl_configuration(
cafile = certifi.where()
if ssl_verify is not False:
custom_ssl_context = ssl.create_default_context(cafile=cafile)
# Optimize SSL handshake performance
# Set minimum TLS version to 1.2 for better performance
custom_ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
# Configure cipher suites for optimal performance
if ssl_security_level and isinstance(ssl_security_level, str):
# User provided custom cipher configuration (e.g., via SSL_SECURITY_LEVEL env var)
custom_ssl_context.set_ciphers(ssl_security_level)
else:
# Use optimized cipher list that strongly prefers fast ciphers
# but falls back to widely compatible ones
custom_ssl_context.set_ciphers(DEFAULT_SSL_CIPHERS)
# Configure ECDH curve for key exchange (e.g., to disable PQC and improve performance)
# Set SSL_ECDH_CURVE env var or litellm.ssl_ecdh_curve to 'X25519' to disable PQC
# Common valid curves: X25519, prime256v1, secp384r1, secp521r1
ssl_ecdh_curve = os.getenv("SSL_ECDH_CURVE", litellm.ssl_ecdh_curve)
if ssl_ecdh_curve and isinstance(ssl_ecdh_curve, str):
try:
custom_ssl_context.set_ecdh_curve(ssl_ecdh_curve)
verbose_logger.debug(f"SSL ECDH curve set to: {ssl_ecdh_curve}")
except AttributeError:
verbose_logger.warning(
f"SSL ECDH curve configuration not supported. "
f"Python version: {sys.version.split()[0]}, OpenSSL version: {ssl.OPENSSL_VERSION}. "
f"Requested curve: {ssl_ecdh_curve}. Continuing with default curves."
)
except ValueError as e:
# Invalid curve name
verbose_logger.warning(
f"Invalid SSL ECDH curve name: '{ssl_ecdh_curve}'. {e}. "
f"Common valid curves: X25519, prime256v1, secp384r1, secp521r1. "
f"Continuing with default curves (including PQC)."
)
# Use our custom SSL context instead of the original ssl_verify value
return custom_ssl_context
# Create cache key from configuration parameters
cache_key = (cafile, ssl_security_level, ssl_ecdh_curve)
# Check if we have a cached SSL context for this configuration
if cache_key not in _ssl_context_cache:
_ssl_context_cache[cache_key] = _create_ssl_context(
cafile=cafile,
ssl_security_level=ssl_security_level,
ssl_ecdh_curve=ssl_ecdh_curve,
)
# Return the cached SSL context
return _ssl_context_cache[cache_key]
return ssl_verify