Introduction

Redis is the backbone of many modern Python applications — handling caching, session storage, message brokering, rate limiting, and leaderboards. As more Python services adopt async-first architectures with asyncio, FastAPI, and async Django, the choice of Redis client library becomes increasingly important. The right async client can significantly impact throughput, connection management, and developer productivity.

This article compares five Python Redis client libraries with async support: redis-py (the official client), aioredis, aredis, fakeredis, and walrus. We evaluate connection pooling, async API design, Redis module support, and testability.

LibraryGitHub StarsAsync-NativeConnection PoolPub/SubRedis ModulesSerialization
redis-py 5.x13,580YesExcellentYesFullBuilt-in
aioredis2,283YesGoodYesBasicNo
aredis645YesGoodYesLimitedPickle optional
fakeredis447N/A (in-memory)N/ANoPartialNo
walrus1,203No (sync only)BasicYesNoCompressed

redis-py: The Official Standard

redis-py is the official Redis client for Python, maintained by the Redis team. Starting with version 5.0, it added native asyncio support alongside its synchronous API, allowing both paradigms from a single library.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import redis.asyncio as redis

async def main():
    # Connection pool with async support
    pool = redis.ConnectionPool(
        host="localhost",
        port=6379,
        max_connections=20,
        decode_responses=True
    )
    client = redis.Redis(connection_pool=pool)

    # String operations
    await client.set("page:home", "<html>...</html>")
    await client.expire("page:home", 3600)

    # Hash operations
    await client.hset("user:1001", mapping={
        "name": "Alice",
        "email": "alice@example.com",
        "plan": "premium"
    })

    # Sorted sets for leaderboards
    await client.zadd("leaderboard", {"player1": 1500, "player2": 2300})
    top_players = await client.zrevrange("leaderboard", 0, 9, withscores=True)

    # Pub/sub messaging
    pubsub = client.pubsub()
    await pubsub.subscribe("notifications")
    async for message in pubsub.listen():
        if message["type"] == "message":
            print(f"Received: {message['data']}")

    await client.aclose()

redis-py supports all Redis data structures (strings, hashes, lists, sets, sorted sets, streams, geospatial, bitmaps, hyperloglog), Redis Modules (RedisJSON, RedisSearch, RedisGraph, RedisTimeSeries, RedisBloom), and both standalone and clustered deployments. Its connection pooling handles reconnection, timeouts, and health checks automatically.

For deployment, Redis runs easily via Docker Compose:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
version: "3.8"
services:
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru

volumes:
  redis_data:

Best for: New projects, production systems, and any application needing full Redis feature support.

aioredis: The Original Async Pioneer

aioredis was the first serious async Redis client for Python, and it heavily influenced the async API design that redis-py later adopted. In fact, the redis-py 5.0 async interface was built with aioredis compatibility in mind — many aioredis codebases can migrate to redis-py with minimal changes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import aioredis

async def main():
    # Create Redis connection with connection pooling
    redis = await aioredis.from_url(
        "redis://localhost:6379",
        encoding="utf-8",
        decode_responses=True
    )

    # Pipeline for batch operations
    async with redis.pipeline(transaction=True) as pipe:
        pipe.set("key1", "value1")
        pipe.set("key2", "value2")
        pipe.incr("counter")
        results = await pipe.execute()

    # Distributed locking pattern
    lock = await redis.set("lock:task-worker", "instance-1",
                           nx=True, ex=30)  # NX + EXPIRE
    if lock:
        try:
            await process_task()
        finally:
            await redis.delete("lock:task-worker")

    await redis.close()

aioredis is now in maintenance mode — the official recommendation from both the aioredis and redis-py maintainers is to migrate to redis-py for new projects. However, aioredis remains widely deployed and is stable for existing systems.

Best for: Maintaining existing aioredis-based codebases; new projects should prefer redis-py 5.x.

aredis: The Drop-In Async Replacement

aredis aims to be a drop-in async replacement for redis-py’s synchronous API, inheriting connection pool management and command implementations while wrapping them in asyncio coroutines.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from aredis import StrictRedis

async def main():
    client = StrictRedis(
        host="localhost",
        port=6379,
        decode_responses=True,
        max_connections=10
    )

    # Atomic counter with INCR
    views = await client.incr("article:123:views")
    await client.expire("article:123:views", 86400)

    # List operations for task queues
    await client.lpush("task_queue", "email_welcome:user_42")
    await client.lpush("task_queue", "generate_report:project_7")
    next_task = await client.brpop("task_queue", timeout=5)

    # Set operations for tags
    await client.sadd("article:123:tags", "python", "redis", "async")
    tags = await client.smembers("article:123:tags")
    common = await client.sinter("article:123:tags", "article:456:tags")

aredis’ main advantage is API compatibility — if you have synchronous code using redis-py’s older synchronous API and want async without restructuring, aredis provides the closest migration path. However, it lacks support for many Redis modules and has seen slower maintenance cycles than redis-py.

Best for: Quick async conversion of synchronous redis-py codebases.

fakeredis: Testing Without a Redis Server

fakeredis is a pure-Python in-memory Redis implementation designed for testing. It emulates the Redis server API without requiring an actual Redis process, making unit tests fast, isolated, and deterministic.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import fakeredis
import pytest

@pytest.fixture
def redis_client():
    """Provide a fake Redis client for testing."""
    server = fakeredis.FakeServer()
    return fakeredis.FakeRedis(server=server)

def test_caching_layer(redis_client):
    # Set up test data
    redis_client.set("user:1:name", "Bob")
    redis_client.hset("session:abc123", mapping={"user_id": "1", "ip": "10.0.0.1"})

    # Test the caching function
    result = get_user_name(redis_client, 1)
    assert result == "Bob"

    # Test expiration
    redis_client.expire("user:1:name", 1)
    import time
    time.sleep(2)
    assert redis_client.get("user:1:name") is None

def test_rate_limiter(redis_client):
    """Test rate limiting without real Redis."""
    for _ in range(100):
        assert rate_limit_check(redis_client, "api_key_1", max_requests=100)
    assert not rate_limit_check(redis_client, "api_key_1", max_requests=100)

fakeredis supports most common Redis commands — strings, hashes, lists, sets, sorted sets, transactions, Lua scripting, and pub/sub. It also emulates some Redis Modules (RedisJSON, RedisSearch). Commands that are not supported raise clear errors rather than silently succeeding.

Best for: Unit testing, CI/CD pipelines without Redis, and local development.

walrus: High-Level Utility Layer

walrus takes a different approach — instead of exposing Redis commands directly, it provides high-level Pythonic abstractions for common patterns: models, rate limiters, caches, autocomplete, and container classes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from walrus import Database

db = Database(host="localhost", port=6379)

# Model-style data access
class User(db.Model):
    __database__ = db
    name = db.TextField()
    email = db.TextField()
    plan = db.TextField(default="free")
    login_count = db.IntegerField(default=0)

# Create and query
user = User.create(name="Charlie", email="charlie@example.com")
user.login_count += 1
user.save()

# Built-in rate limiter decorator
@db.rate_limit("api_calls", limit=100, per=60)
def call_external_api():
    return fetch_data()

# Auto-complete index
search = db.autocomplete("search_index")
search.store("user:1", "Python async Redis guide")
search.store("user:2", "Python HTML parsing tutorial")
results = search.search("Python async")  # Returns matching entries

walrus is synchronous-only and does not have built-in asyncio support. It wraps redis-py under the hood and adds abstractions that eliminate boilerplate for recurring patterns. For async applications, you would use redis-py directly alongside walrus-inspired patterns implemented manually.

Best for: Synchronous applications wanting higher-level Redis abstractions without writing boilerplate.

When to Choose Each Client

  • redis-py 5.x: The default choice. Production-grade, full-featured, async-native, and officially maintained by Redis. Use this for any new project unless you have a specific reason to use something else.
  • aioredis: Maintain existing codebases. Do not use for new projects — migrate to redis-py.
  • aredis: Quick async migration of synchronous redis-py code. Consider redis-py instead for new async projects.
  • fakeredis: Essential for testing. Use in all projects that interact with Redis.
  • walrus: High-level Redis abstractions for synchronous applications. Use alongside redis-py for async work.

For related reading, see our comparison of Go Redis client libraries for the cross-language perspective, and our Python caching libraries guide for broader caching strategies.

FAQ

Should I migrate from aioredis to redis-py?

Yes, for new development. redis-py 5.x’s async interface was designed for aioredis compatibility, so the migration is straightforward. aioredis is in maintenance mode — it receives security fixes but no new features. The redis-py team recommends migration for projects under active development.

Can I use fakeredis in async test suites?

fakeredis provides async support via fakeredis.aioredis.FakeRedis which exposes an async-compatible interface. For pytest-asyncio tests, use fakeredis.FakeAsyncRedis() to get an async test client that mimics redis-py’s async API.

How do I handle Redis connection failures in async Python?

Use redis-py’s built-in retry_on_timeout and health_check_interval connection pool options. For circuit breaker patterns, wrap Redis calls in retry logic using tenacity or backoff:

1
2
3
4
5
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=10))
async def safe_redis_get(key):
    return await redis_client.get(key)

What is the difference between redis-py’s async pipeline and synchronous pipeline?

The async pipeline is functionally identical to the synchronous version but all methods are coroutines. The key difference for transactions: use async with client.pipeline(transaction=True) as pipe: in async code. The pipeline still queues commands locally and sends them in a single round-trip.

Is walrus suitable for production use with async frameworks like FastAPI?

No. walrus is synchronous-only and does not integrate with asyncio event loops. For FastAPI or other async frameworks, use redis-py directly and implement walrus-like abstractions as thin async wrappers around redis-py calls if you need the higher-level patterns.

Does fakeredis support Redis Streams for testing?

fakeredis has partial support for Redis Streams commands (XADD, XREAD, XGROUP, XACK). For comprehensive Stream testing, consider using a real Redis instance via Docker Compose in CI, with fakeredis covering the simpler data structure tests.


💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com