Jump to solution
Verify

The Fix

pip install redis==7.1.0

Based on closed redis/redis-py issue #2065 · PR/commit linked

Production note: This tends to surface only under concurrency. Reproduce with load tests and watch for lock contention/cancellation paths.

Jump to Verify Open PR/Commit
@@ -6,6 +6,7 @@ * Fix scan_iter for RedisCluster * Remove verbose logging when initializing ClusterPubSub, ClusterPipeline or RedisCluster + * Fix broken connection writer lock-up for asyncio (#2065) * 4.1.3 (Feb 8, 2022)
repro.py
import asyncio from fastapi import FastAPI from fastapi.responses import HTMLResponse, StreamingResponse from redis import asyncio as aioredis app = FastAPI() red = aioredis.from_url('redis://redisedge:6379') with open('data/random.jpg', 'rb') as f: placeholder_image = f.read() @app.get('/') def index(): return HTMLResponse('<img src="/video" width="600" height="400" />') @app.get('/push') async def add_image(): return await red.xadd('camera:bug-test', { 'image': placeholder_image }) @app.get('/video') def video_feed(name='camera:bug-test',field='image'): async def stream(): while True: # query image p = red.pipeline(transaction=True).xrevrange(name, count=1) print('before execute') cmsg, = await p.execute() # hangs here forever print('after execute', len(cmsg)) # serve image if not cmsg: break yield format_frame(cmsg[0][1][field.encode('utf-8')]) await asyncio.sleep(0.01) return StreamingResponse(stream(), media_type='multipart/x-mixed-replace; boundary=frame') def format_frame(frame): return ( b'--frame\r\n' b'Pragma-directive: no-cache\r\n' b'Cache-directive: no-cache\r\n' b'Cache-control: no-cache\r\n' b'Pragma: no-cache\r\n' b'Expires: 0\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n\r\n')
verify
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
fix.md
Option A — Upgrade to fixed release\npip install redis==7.1.0\nWhen NOT to use: This fix is not applicable if the connection handling logic is fundamentally altered.\n\n

Why This Fix Works in Production

  • Trigger: RuntimeError: unable to perform operation on <TCPTransport closed=True reading=False>; the handler is closed
  • Mechanism: A race condition occurs when closing a StreamWriter, leading to attempts to reuse a closed writer
  • Why the fix works: Fixes a race condition that causes broken connections in redis.asyncio by ensuring that closed writers are properly discarded. (first fixed release: 7.1.0).
Production impact:
  • If left unfixed, failures can be intermittent under concurrency (hard to reproduce; shows up as sporadic 5xx/timeouts).

Why This Breaks in Prod

  • Shows up under Python 3.7.12 in real deployments (not just unit tests).
  • A race condition occurs when closing a StreamWriter, leading to attempts to reuse a closed writer
  • Surfaces as: RuntimeError: unable to perform operation on <TCPTransport closed=True reading=False>; the handler is closed

Proof / Evidence

  • GitHub issue: #2065
  • Fix PR: https://github.com/redis/redis-py/pull/2077
  • First fixed release: 7.1.0
  • Reproduced locally: No (not executed)
  • Last verified: 2026-02-07
  • Confidence: 0.85
  • Did this fix it?: Yes (upstream fix exists)
  • Own content ratio: 0.41

Discussion

High-signal excerpts from the issue thread (symptoms, repros, edge-cases).

“**Version**: 4.2.0 (only version with asyncio) **Platform**: Mac / Docker / Python 3.7.12 > My code builds off of this repo, but the code in the repo isn't relevant for the minimum code sample. I'm just using it to provide the redis instanc”
Issue thread · issue description · source

Failure Signature (Search String)

  • RuntimeError: unable to perform operation on <TCPTransport closed=True reading=False>; the handler is closed

Error Message

Stack trace
error.txt
Error Message ------------- RuntimeError: unable to perform operation on <TCPTransport closed=True reading=False>; the handler is closed

Minimal Reproduction

repro.py
import asyncio from fastapi import FastAPI from fastapi.responses import HTMLResponse, StreamingResponse from redis import asyncio as aioredis app = FastAPI() red = aioredis.from_url('redis://redisedge:6379') with open('data/random.jpg', 'rb') as f: placeholder_image = f.read() @app.get('/') def index(): return HTMLResponse('<img src="/video" width="600" height="400" />') @app.get('/push') async def add_image(): return await red.xadd('camera:bug-test', { 'image': placeholder_image }) @app.get('/video') def video_feed(name='camera:bug-test',field='image'): async def stream(): while True: # query image p = red.pipeline(transaction=True).xrevrange(name, count=1) print('before execute') cmsg, = await p.execute() # hangs here forever print('after execute', len(cmsg)) # serve image if not cmsg: break yield format_frame(cmsg[0][1][field.encode('utf-8')]) await asyncio.sleep(0.01) return StreamingResponse(stream(), media_type='multipart/x-mixed-replace; boundary=frame') def format_frame(frame): return ( b'--frame\r\n' b'Pragma-directive: no-cache\r\n' b'Cache-directive: no-cache\r\n' b'Cache-control: no-cache\r\n' b'Pragma: no-cache\r\n' b'Expires: 0\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n\r\n')

Environment

  • Python: 3.7.12

What Broke

Redis calls hang indefinitely after refreshing the page multiple times, causing service disruption.

Why It Broke

A race condition occurs when closing a StreamWriter, leading to attempts to reuse a closed writer

Fix Options (Details)

Option A — Upgrade to fixed release Safe default (recommended)

pip install redis==7.1.0

When NOT to use: This fix is not applicable if the connection handling logic is fundamentally altered.

Use when you can deploy the upstream fix. It is usually lower-risk than long-lived workarounds.

Fix reference: https://github.com/redis/redis-py/pull/2077

First fixed release: 7.1.0

Last verified: 2026-02-07. Validate in your environment.

Get updates

We publish verified fixes weekly. No spam.

Subscribe

When NOT to Use This Fix

  • This fix is not applicable if the connection handling logic is fundamentally altered.

Verify Fix

verify
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.

Did This Fix Work in Your Case?

Quick signal helps us prioritize which fixes to verify and improve.

Prevention

  • Add a stress test that runs high-concurrency workloads and fails on thread dumps / blocked locks.
  • Enable watchdog dumps in prod (faulthandler, thread dump endpoint) to capture deadlocks quickly.

Version Compatibility Table

VersionStatus
7.1.0 Fixed

Related Issues

No related fixes found.

Sources

We don’t republish the full GitHub discussion text. Use the links above for context.