Skip to content

fix: extend socket_timeout for blocking commands to prevent premature timeouts - #4151

Closed
C1-BA-B1-F3 wants to merge 1 commit into
redis:masterfrom
C1-BA-B1-F3:fix-blocking-timeout
Closed

fix: extend socket_timeout for blocking commands to prevent premature timeouts#4151
C1-BA-B1-F3 wants to merge 1 commit into
redis:masterfrom
C1-BA-B1-F3:fix-blocking-timeout

Conversation

@C1-BA-B1-F3

@C1-BA-B1-F3 C1-BA-B1-F3 commented Jun 26, 2026

Copy link
Copy Markdown

Description

Fixes #2807

Blocking commands (BRPOP, BLPOP, BRPOPLPUSH, BLMOVE, BZPOPMIN, BZPOPMAX, BZMPOP, XREAD/XREADGROUP with BLOCK) specify their own timeout at the Redis protocol level. When the command timeout exceeds the client's socket_timeout, the socket layer raises TimeoutError before the blocking command can complete.

Problem

r = redis.Redis(socket_timeout=4)
r.brpop('my_key', timeout=8)  # Raises TimeoutError after 4 seconds

The socket_timeout is a low-level socket setting that should not prevent application-level blocking timeouts from being honored.

Solution

This fix detects blocking commands in _send_command_parse_response and temporarily extends the connection's socket_timeout to cover the command's blocking duration (plus a 1-second buffer). The original timeout is restored after the command completes, even if an exception occurs.

Changes

  • redis/client.py: Added _get_blocking_timeout() method and socket timeout adjustment in _send_command_parse_response()
  • redis/asyncio/client.py: Same fix for the async client
  • tests/test_client.py: Added TestBlockingCommandTimeout test class with 15 tests covering all blocking commands

Commands Fixed

Command Timeout Location
BRPOP, BLPOP, BZPOPMIN, BZPOPMAX Last argument (seconds)
BRPOPLPUSH 3rd argument (seconds)
BLMOVE 5th argument (seconds)
BZMPOP 1st argument (seconds)
XREAD, XREADGROUP After BLOCK keyword (milliseconds, converted to seconds)

Testing

All 25 tests in tests/test_client.py pass, including 15 new tests specifically for the blocking timeout fix.


Note

Medium Risk
Changes the shared command-send path by mutating per-connection socket_timeout, which could affect pooled connections if restore failed; behavior is narrow, guarded, and covered by new tests.

Overview
Fixes premature TimeoutError when a blocking command’s Redis-level wait is longer than the client’s socket_timeout (e.g. brpop(..., timeout=8) with socket_timeout=4).

Sync and async Redis clients now parse the block duration for commands like BRPOP/BLPOP, BRPOPLPUSH, BLMOVE, BZMPOP, and XREAD/XREADGROUP with BLOCK. In _send_command_parse_response, the connection’s socket_timeout is temporarily raised to that duration plus one second (only if needed), then restored in a finally block—including on errors. Non-blocking commands and timeouts of 0 are unchanged.

Adds TestBlockingCommandTimeout in tests/test_client.py for parsing and socket-timeout behavior.

Reviewed by Cursor Bugbot for commit 4bd5e17. Bugbot is set up for automated code reviews on this repo. Configure here.

… timeouts

Blocking commands (BRPOP, BLPOP, BRPOPLPUSH, BLMOVE, BZPOPMIN, BZPOPMAX,
BZMPOP, XREAD/XREADGROUP with BLOCK) specify their own timeout at the Redis
protocol level. When the command timeout exceeds the client's socket_timeout,
the socket layer raises TimeoutError before the blocking command can complete.

This fix detects blocking commands in _send_command_parse_response and
temporarily extends the connection's socket_timeout to cover the command's
blocking duration. The original timeout is restored after the command
completes, even if an exception occurs.

Fixes redis#2807

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 4 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 4bd5e17. Configure here.

Comment thread redis/client.py

try:
conn.send_command(*args, **options)
return self.parse_response(conn, command_name, **options)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sync timeout not applied

High Severity

Blocking-command handling only assigns conn.socket_timeout before parse_response. The sync connection still reads using the OS socket timeout and parser state set at connect time, so BRPOP-style calls can keep raising TimeoutError at the original socket_timeout despite this change.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4bd5e17. Configure here.

Comment thread redis/client.py
arg_str = (
arg.upper() if isinstance(arg, bytes) else str(arg).upper()
)
if arg_str == "BLOCK":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XREAD BLOCK bytes mismatch

High Severity

_get_blocking_timeout treats a BLOCK token as the string "BLOCK", but xread and xreadgroup emit b"BLOCK". That comparison never matches, so stream blocking reads skip socket timeout extension even though the PR lists XREAD/XREADGROUP as fixed.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4bd5e17. Configure here.

Comment thread redis/client.py
return self.parse_response(conn, command_name, **options)
finally:
if orig_socket_timeout is not None:
conn.socket_timeout = orig_socket_timeout

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timeout not restored when None

Medium Severity

When the connection’s original socket_timeout is None and a blocking command raises the timeout, the finally block skips restoration because it only runs when orig_socket_timeout is not None. The pooled connection can keep an inflated timeout after the command finishes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4bd5e17. Configure here.

Comment thread redis/client.py
return self.parse_response(conn, command_name, **options)
blocking_timeout = self._get_blocking_timeout(command_name, args)
orig_socket_timeout = None
if blocking_timeout and blocking_timeout > 0:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zero timeout not extended

Medium Severity

Socket extension runs only when blocking_timeout > 0, but Redis list blocking commands use timeout=0 to block indefinitely. Those calls still fail early against a finite socket_timeout, which the PR aims to prevent for blocking commands generally.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4bd5e17. Configure here.

@petyaslavova

Copy link
Copy Markdown
Collaborator

Please add all related changes to blocking timeout in the other PR --> #4143
I'm closing this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blocking command timeout cannot exceed client's socket_timeout

2 participants