From cd087b5368c0b12db443e4ef611963e020a4c6bf Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 09:54:27 +0000 Subject: [PATCH] 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. --- backend/open_webui/env.py | 9 +++++++++ backend/open_webui/utils/redis.py | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 53737a1f2f..f612248ffe 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -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 == '': diff --git a/backend/open_webui/utils/redis.py b/backend/open_webui/utils/redis.py index c2e5da1fae..ec1dee5e9b 100644 --- a/backend/open_webui/utils/redis.py +++ b/backend/open_webui/utils/redis.py @@ -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