Problem / Question
We are observing steady memory growth in a minimal FastAPI application when using redis.asyncio.cluster.RedisCluster.
Environment
| Item |
Value |
| Python |
3.11 |
| redis-py |
7.4, also reproduced on 6.4 |
| hiredis |
Not installed |
| OS / base image |
Debian GNU/Linux 13 (trixie) |
| Redis mode |
Redis Cluster |
| Redis SSL |
Enabled |
decode_responses |
Reproduced with both True and False |
| Server |
FastAPI |
| Worker count |
1 |
| Runtime |
Reproduced with both uvicorn and gunicorn |
| Event loop |
Reproduced with both uvloop and standard asyncio.
uvicorn redis_leak_demo:app --host 0.0.0.0 --port 80 --loop asyncio --workers 1 |
The growth is reproduced even when:
- the Redis response is very small:
LRANGE key -1 -1
- the response is not returned to the client
- logging is removed
decode_responses is tested with both True and False
- both
uvloop and standard asyncio are used
- both
uvicorn and gunicorn are used with a single worker
max_connections=1 is configured
Minimal Reproduction Script
# redis_leak_demo.py
import os
import sys
from contextlib import asynccontextmanager
from fastapi import Body, FastAPI
from redis.asyncio.cluster import ClusterNode, RedisCluster
logging.basicConfig(
level=logging.INFO,
stream=sys.stdout,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
REDIS_NODES = [
ClusterNode(host="redis-node-0.example.com", port=6379),
ClusterNode(host="redis-node-1.example.com", port=6379),
ClusterNode(host="redis-node-2.example.com", port=6379),
]
class RedisClient:
def __init__(self):
self.client = None
async def init(self):
self.client = RedisCluster(
startup_nodes=REDIS_NODES,
username=os.getenv("REDIS_USERNAME"),
password=os.getenv("REDIS_PASSWORD"),
decode_responses=True, # also reproduced with False
ssl=True,
ssl_cert_reqs=None,
)
await self.client.initialize()
async def close(self):
if self.client:
await self.client.close()
redis_client = RedisClient()
@asynccontextmanager
async def lifespan(app: FastAPI):
await redis_client.init()
try:
yield
finally:
await redis_client.close()
app = FastAPI(lifespan=lifespan)
@app.post("/predict")
async def predict(payload: dict = Body(...)):
item_id = str(payload.get("id", ""))
# Redis value shape for this key:
# type: Redis List
# length: ~400 elements in the sampled keys
# element size: ~20-25 bytes, typically short ID strings
# raw payload estimate: ~8-10 KB per key
# The RSS growth is also reproduced with a very small response:
# val = await redis_client.client.lrange(f"key:{item_id}", -1, -1)
val = await redis_client.client.lrange(f"key:{item_id}", 0, -1)
return {
"id": item_id,
"value": len(val),
}
Server Startup Commands
gunicorn redis_leak_demo:app -k uvicorn.workers.UvicornWorker -b 0.0.0.0:80 --workers 1
Observations
Additional observations:
- The memory growth becomes more severe as the number of concurrent request threads increases.
- The memory growth becomes more severe when making concurrent requests to multiple Redis clusters.
Problem / Question
We are observing steady memory growth in a minimal FastAPI application when using
redis.asyncio.cluster.RedisCluster.Environment
decode_responsesTrueandFalseuvicornandgunicornuvloopand standardasyncio.uvicorn redis_leak_demo:app --host 0.0.0.0 --port 80 --loop asyncio --workers 1The growth is reproduced even when:
LRANGE key -1 -1decode_responsesis tested with bothTrueandFalseuvloopand standardasyncioare useduvicornandgunicornare used with a single workermax_connections=1is configuredMinimal Reproduction Script
Server Startup Commands
gunicorn redis_leak_demo:app -k uvicorn.workers.UvicornWorker -b 0.0.0.0:80 --workers 1Observations
Additional observations: