Skip to content

Async

The same script object works with either client. Pass a sync client and you get a value; pass an async one and you get an awaitable.

from redis.asyncio import Redis

client = Redis()
remaining = await rate_limit(client, key="user:42", limit=10, ttl=60)

There is no separate decorator, no separate script, and no separate compilation — one @script serves both, and the overloads on __call__ mean a type checker knows which of the two you are looking at:

sync_client: redis.Redis
async_client: redis.asyncio.Redis

a = rate_limit(sync_client, key="u:1", limit=10, ttl=60)  # int
b = rate_limit(async_client, key="u:1", limit=10, ttl=60)  # Awaitable[int]

A client is typed as sync when it has __enter__, as every redis-py sync client does, and as async when it has __aenter__. The sync overload comes first, so a wrapper of your own that forwards attributes through __getattr__, which mypy takes to provide both, is typed as the sync client it wraps.

Script caching, EVALSHA, and the NOSCRIPT reload are handled by redis-py's own script machinery, which this defers to rather than reimplementing.

bind works on async clients just as well, and carries the awaitable through.

coredis

coredis, which is async only, works the same way. A script, a bound script and a library function each return an awaitable, and the overloads type it as Awaitable[R]:

import coredis

async with coredis.Redis() as client:
    remaining = await rate_limit(client, key="user:42", limit=10, ttl=60)

Script caching is coredis's own register_script, which reloads a script the server has dropped. In a coredis pipeline a call is queued like any other command, and coredis loads the pipeline's scripts before running it; await each call once the async with block has run the pipeline:

async with client.pipeline() as pipe:
    queued = rate_limit(pipe, key="user:42", limit=10, ttl=60)
remaining = await queued

As with redis-py, a client created with decode_responses=True decodes replies, so a script annotated bytes returns str from it.