Redis is supposed to be the easy part of your stack — until your FastAPI app starts throwing redis.exceptions.ConnectionError under load, or silently hangs on every third request. This guide walks through the five most common causes and how to fix each one.
Quick Answer: FastAPI + Redis connection errors almost always come from one of these:
- Redis isn’t reachable — wrong host, port, or the container/service isn’t up
- Using the sync
redisclient in async routes — it blocks the event loop instead of failing cleanly - Creating a new client per request — you’re exhausting file descriptors or Redis’s
maxclients - Stale
aioredisimports — the package was merged intoredis-pyand the old one is unmaintained - Pool exhaustion under load — too few connections for your concurrency
Read on to figure out which one is biting you.
Diagnostic Steps
Before chasing a fix, narrow down what’s actually happening:
- Check the exact exception.
ConnectionRefusedError,redis.exceptions.ConnectionError, andredis.exceptions.TimeoutErrorall point to different root causes. - Does it fail immediately or hang? An immediate refusal usually means Redis isn’t listening where you think it is. A hang that eventually times out usually means a blocked event loop or an exhausted pool.
- Does it only happen under concurrent load? If a single request works fine but ten at once fail, you’re looking at pool exhaustion or a
maxclientslimit. - Is this new after an upgrade? If it broke after updating dependencies, check whether
aioredisgot swapped out from under you.
With that in mind, let’s go through each cause.
Cause #1: Redis Isn’t Actually Reachable
What you see:
redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.
This one’s boring but it’s also the most common — especially in Docker. Your app runs in one container, Redis runs in another, and localhost inside the app container doesn’t point to the Redis container at all.
# ❌ Before — works on your machine, breaks in Docker Compose
redis_client = redis.asyncio.from_url("redis://localhost:6379")
# ✅ After — use the service name from docker-compose.yml, driven by env var
import os
import redis.asyncio as redis
REDIS_URL = os.environ.get("REDIS_URL", "redis://redis:6379/0")
redis_client = redis.from_url(REDIS_URL)
In docker-compose.yml, the service name (redis in this example) is the hostname other containers use to reach it — not localhost, and not 127.0.0.1. If you’re on Kubernetes, double-check the service DNS name and namespace instead of hardcoding an IP that’ll change on the next pod restart.
Quick sanity check from inside the app container:
docker exec -it <app-container> python -c "import socket; socket.create_connection(('redis', 6379), timeout=3)"
If that hangs or raises, it’s a networking problem, not an application bug.
Cause #2: Using the Sync Redis Client in Async Routes
What you see:
Requests that hang for seconds at a time, or a server that seems to serialize requests it should be handling concurrently. No exception at all — just bad performance that looks like a connection issue.
This one’s sneaky because redis-py’s sync client works. It just blocks the entire event loop while it waits on a socket, which means every other request in that worker stalls behind it.
# ❌ Before — sync client called directly inside an async def route
import redis
r = redis.Redis(host="redis", port=6379)
@app.get("/cache/{key}")
async def get_cached(key: str):
value = r.get(key) # blocks the event loop
return {"value": value}
# ✅ After — use the async client that ships with redis-py
import redis.asyncio as redis
r = redis.Redis(host="redis", port=6379)
@app.get("/cache/{key}")
async def get_cached(key: str):
value = await r.get(key) # yields control back to the event loop
return {"value": value}
If you’re stuck with a library that only offers a sync Redis client, run it in a thread pool with run_in_threadpool rather than calling it directly from an async def route. This is the same class of bug covered in our guide on mixing sync and async code in FastAPI — Redis is just one of the more common offenders because it’s so easy to reach for the familiar sync API out of habit.
Cause #3: Creating a New Client (or Connection) Per Request
What you see:
redis.exceptions.ConnectionError: Too many connections
or errors that only appear after the app has been running for a while and traffic picks up.
If you instantiate redis.Redis(...) inside your route function, you’re creating a brand-new connection pool on every single request. Under any real traffic, you’ll either hit the OS file descriptor limit or Redis’s own maxclients setting before you hit 500 requests.
# ❌ Before — a new client (and connection) every request
@app.get("/cache/{key}")
async def get_cached(key: str):
r = redis.asyncio.Redis(host="redis", port=6379)
return {"value": await r.get(key)}
# ✅ After — one client, created once, reused via app state
from contextlib import asynccontextmanager
import redis.asyncio as redis
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.redis = redis.from_url(REDIS_URL, max_connections=20)
yield
await app.state.redis.aclose()
app = FastAPI(lifespan=lifespan)
@app.get("/cache/{key}")
async def get_cached(key: str, request: Request):
return {"value": await request.app.state.redis.get(key)}
The redis.asyncio.Redis client already manages an internal connection pool, so you don’t need to build your own pooling logic — you just need to make sure you’re only creating it once, at startup, and reusing it across requests. FastAPI’s lifespan handler is the right place for that; it’s the same pattern you’d use for a database engine, and it’s covered in more depth in our post on SQLAlchemy connection pool exhaustion in FastAPI.
Cause #4: Stale aioredis Imports
What you see:
ModuleNotFoundError: No module named 'aioredis'
or, if it’s installed but outdated:
TypeError: duplicate base class TimeoutError
aioredis used to be the standard way to use Redis asynchronously in Python. As of redis-py 4.2, that project’s functionality was merged directly into the main redis package under redis.asyncio — and aioredis itself is no longer maintained. If you copy-pasted a Redis snippet from an older tutorial, there’s a decent chance it’s importing the deprecated package.
# ❌ Before — deprecated, unmaintained, and breaks on newer Python versions
import aioredis
redis_client = await aioredis.from_url("redis://redis:6379")
# ✅ After — use redis-py's built-in async support instead
import redis.asyncio as redis
redis_client = redis.from_url("redis://redis:6379")
The API is nearly identical, so the fix is usually a find-and-replace. Just make sure your requirements.txt or pyproject.toml pins a recent redis version (redis>=4.2) and drops aioredis entirely — having both installed is how you end up with the confusing duplicate base class error above, since both packages try to define compatible-but-not-identical exception classes.
Cause #5: Connection Pool Exhaustion Under Load
What you see:
redis.exceptions.ConnectionError: Error 24 connecting to redis:6379. Too many open files.
or requests that succeed individually but start timing out as soon as you load test with concurrency.
Even with a properly shared client, the default pool size might not be enough for your traffic. redis.asyncio.Redis defaults to an effectively unbounded pool unless you set max_connections, which sounds helpful until Redis’s own maxclients (default 10000, but often much lower in managed offerings) becomes the real bottleneck instead.
# Step 1: the naive setup — no explicit pool limit
redis_client = redis.from_url(REDIS_URL)
# Step 2: cap it explicitly so failures are predictable
redis_client = redis.from_url(REDIS_URL, max_connections=50)
# Step 3: add retry with backoff for transient blips
from redis.retry import Retry
from redis.backoff import ExponentialBackoff
from redis.exceptions import ConnectionError, TimeoutError
redis_client = redis.from_url(
REDIS_URL,
max_connections=50,
retry=Retry(ExponentialBackoff(), 3),
retry_on_error=[ConnectionError, TimeoutError],
)
Sizing the pool is a balancing act: too small and you get artificial bottlenecks under concurrency, too large and you risk overwhelming Redis itself or a managed instance’s connection cap. As a starting point, size it relative to your Uvicorn worker count and expected concurrent requests per worker, then tune based on what you actually see in production metrics — not guesswork.
Still Not Working?
A few edge cases that don’t fit neatly into the categories above:
- TLS misconfiguration. Managed Redis (AWS ElastiCache, Azure Cache, Upstash) often requires
rediss://instead ofredis://. Using the wrong scheme gives a connection error that looks identical to a plain networking issue. - Firewall or security group rules. The app can resolve the hostname but the port is blocked — this shows up as a timeout rather than an immediate refusal, which is a useful way to tell it apart from Cause #1.
- Redis is out of memory and evicting aggressively, which can make
GETcalls behave oddly even though the connection itself is healthy. CheckINFO memoryon the Redis instance directly. - You’re behind a proxy or service mesh (like Envoy or Istio) that’s terminating idle connections faster than your pool expects. If errors correlate with periods of low traffic followed by a burst, this is worth checking.
Summary Checklist
- [ ] Confirm Redis is reachable from the app’s actual network context (not just your laptop)
- [ ] Use
redis.asyncio, never the sync client, insideasync defroutes - [ ] Create the Redis client once in
lifespan, not per-request - [ ] Replace any lingering
aioredisimports withredis.asyncio - [ ] Set an explicit
max_connectionsand add retry/backoff for transient errors - [ ] Check for TLS scheme mismatches on managed Redis providers
Redis connection issues rarely mean Redis itself is broken — it’s almost always how the client is wired into your FastAPI app’s lifecycle. Once you’ve traced the error back to one of these five causes, the fix is usually a few lines of code, not a redesign.
When you hit one of these in production, the full traceback matters. Use Debugly’s trace formatter to quickly parse and analyze Python tracebacks so you can spot exactly where in your Redis client setup the failure originates.