fix(redis): add opt-in TCP socket keepalive on all client connections

Introduces REDIS_SOCKET_KEEPALIVE and wires socket_keepalive=True
through to every Redis client created by get_redis_connection
(plain, cluster and sentinel paths, sync and async). When enabled,
the kernel sends TCP keepalive probes on idle connections so
half-closed sockets (e.g. after a silent firewall/LB reset or a NIC
flap) are detected before the next command lands on them and the
request never sees a "Connection reset by peer" error.

Defaults to off so existing deployments see no behavioural change.
Operators who want the protection set REDIS_SOCKET_KEEPALIVE=true
in their environment.
This commit is contained in:
Claude 2026-04-10 09:54:27 +00:00
parent 008cd8e6b9
commit cd087b5368
No known key found for this signature in database
2 changed files with 20 additions and 0 deletions

View file

@ -426,6 +426,15 @@ try:
except ValueError:
REDIS_SOCKET_CONNECT_TIMEOUT = None
# Whether to enable TCP SO_KEEPALIVE on Redis client sockets. Opt-in:
# defaults to off so behavior is unchanged for existing deployments. When
# enabled, the kernel sends TCP keepalive probes on idle connections so
# half-closed sockets (e.g. after a silent firewall/LB reset or a NIC
# flap) are detected before the next command lands on them.
REDIS_SOCKET_KEEPALIVE = (
os.environ.get('REDIS_SOCKET_KEEPALIVE', 'False').lower() == 'true'
)
REDIS_RECONNECT_DELAY = os.environ.get('REDIS_RECONNECT_DELAY', '')
if REDIS_RECONNECT_DELAY == '':

View file

@ -10,6 +10,7 @@ import redis
from open_webui.env import (
REDIS_CLUSTER,
REDIS_SOCKET_CONNECT_TIMEOUT,
REDIS_SOCKET_KEEPALIVE,
REDIS_SENTINEL_HOSTS,
REDIS_SENTINEL_MAX_RETRY_COUNT,
REDIS_SENTINEL_PORT,
@ -197,6 +198,10 @@ def get_redis_connection(
else {}
)
keepalive_kwargs = (
{'socket_keepalive': True} if REDIS_SOCKET_KEEPALIVE else {}
)
if async_mode:
import redis.asyncio as redis
@ -211,6 +216,7 @@ def get_redis_connection(
password=redis_config['password'],
decode_responses=decode_responses,
socket_connect_timeout=REDIS_SOCKET_CONNECT_TIMEOUT,
**keepalive_kwargs,
)
connection = SentinelRedisProxy(
sentinel,
@ -224,12 +230,14 @@ def get_redis_connection(
redis_url,
decode_responses=decode_responses,
**connect_timeout_kwargs,
**keepalive_kwargs,
)
elif redis_url:
connection = redis.from_url(
redis_url,
decode_responses=decode_responses,
**connect_timeout_kwargs,
**keepalive_kwargs,
)
else:
import redis
@ -244,6 +252,7 @@ def get_redis_connection(
password=redis_config['password'],
decode_responses=decode_responses,
socket_connect_timeout=REDIS_SOCKET_CONNECT_TIMEOUT,
**keepalive_kwargs,
)
connection = SentinelRedisProxy(
sentinel,
@ -257,12 +266,14 @@ def get_redis_connection(
redis_url,
decode_responses=decode_responses,
**connect_timeout_kwargs,
**keepalive_kwargs,
)
elif redis_url:
connection = redis.Redis.from_url(
redis_url,
decode_responses=decode_responses,
**connect_timeout_kwargs,
**keepalive_kwargs,
)
_CONNECTION_CACHE[cache_key] = connection