# Redis

> Fast in-memory data structures for caching, counters, sessions and more

[Redis](https://redis.io) is an in-memory data store, commonly used for caching, rate limiting, session storage, leaderboards and queues.

With Encore.py you declare a Redis database in code, and Encore provisions it: a local Redis emulator when you run your app with `encore run`, and a managed Redis when you deploy. Every command is traced automatically.

```python
-- sessions/store.py --
from encore.storage import redis

sessions = redis.Database("sessions")


async def save_session(session_id: str, user_id: str) -> None:
    await sessions.string.set(f"session:{session_id}", user_id, ex=3600)


async def load_session(session_id: str) -> str | None:
    return await sessions.string.get[str](f"session:{session_id}")
```

> **Note**
>
> **Coming from Encore.go or Encore.ts?** Their caching API ([Go](/docs/go/primitives/caching), [TypeScript](/docs/ts/primitives/caching)) has cache clusters and typed keyspaces. Encore.py has Redis databases instead: you declare a `redis.Database` and call Redis commands on it directly, choosing your own keys and how values are encoded. There are no keyspaces, and no eviction policy option yet.

## Creating a database

Create a database with `redis.Database`, assigned to a module-level variable:

```python
from encore.storage import redis

sessions = redis.Database("sessions")
```

Encore reads the declaration from your source code to provision the database, so:

- Declare it as a module-level variable, not inside a function, class, or `if` block.
- The name must be a string literal. It's a kebab-case name such as `sessions`, unique within the app.

`Database` takes no other options yet: persistence and eviction policies can't be configured.

Import the variable from other modules to use the database there.

## Running commands

Commands are grouped into families, which are properties of the database. Call a command on its family and await it:

```python
await sessions.string.set("user:1", "Ada", ex=60)
name = await sessions.string.get("user:1")  # b"Ada"

await sessions.hash.hset("user:1:profile", ("name", "Ada"), ("lang", "en"))
await sessions.list.rpush("queue", "job-1", "job-2")
await sessions.sorted_set.zadd("leaderboard", (100, "ada"), (85, "grace"))
await sessions.generic.expire("queue", 300)
```

| Family | Commands for |
| --- | --- |
| `generic` | Keys of any type: `exists`, `delete`, `expire`, `ttl`, `rename`, `scan`, … |
| `string` | Strings and counters: `get`, `set`, `mget`, `incr`, `incrby`, `append`, … |
| `bitmap` | Bits of strings: `getbit`, `setbit`, `bitcount`, `bitpos`, `bitop` |
| `hash` | Hashes: `hget`, `hset`, `hgetall`, `hincrby`, `hdel`, … |
| `list` | Lists: `lpush`, `rpush`, `lpop`, `rpop`, `lrange`, `lmove`, … |
| `set` | Sets: `sadd`, `srem`, `smembers`, `sismember`, `sinter`, … |
| `sorted_set` | Sorted sets: `zadd`, `zrange`, `zrangebyscore`, `zincrby`, `zrank`, … |
| `geo` | Geospatial indexes: `geoadd`, `geodist`, `geosearch`, … |
| `stream` | Streams: `xadd`, `xrange`, `xread`, `xreadgroup`, `xack`, … |
| `json` | RedisJSON documents: `set`, `get`, `arrappend`, `numincrby`, … |
| `pubsub` | Redis channels: `publish`, `subscribe`, `psubscribe` |

Methods are named after the Redis commands they run, and their options are keyword arguments, such as `set(key, value, ex=60, nx=True)`. Redis errors raise `encore.EncoreError`.

Keys are plain strings. Encore namespaces them behind the scenes, for example to isolate tests from each other, but you always read and write the unprefixed names: `generic.scan` returns your keys as you wrote them.

### Multiple values

Commands that take several values accept them as extra positional arguments, with a tuple for each grouped value such as a hash field and its value, or a sorted-set score and its member:

```python
await sessions.string.mset(("first", "Ada"), ("last", "Lovelace"))
values = await sessions.string.mget("first", "last")

await sessions.set.sadd("tags", "python", "redis")
```

To pass an existing collection, unpack it with `*`, as in `sessions.set.sadd("tags", *tags)`.

## Values

You can store `bytes`, `str`, `int` and `float` values. Strings are encoded as UTF-8, integers as decimal text, floats using `repr()`, and bytes are stored unchanged. Booleans aren't supported as values.

Reads return `bytes` by default. To decode a stored value as another type, put the type in square brackets after the command:

```python
await sessions.string.set("visits", 42)
visits = await sessions.string.get[int]("visits")  # int | None

await sessions.list.rpush("names", "Ada", "Grace")
names = await sessions.list.lrange[str]("names", 0, -1)  # list[str]

top = await sessions.sorted_set.zrange[str]("leaderboard", 0, 9, withscores=True)
# list[tuple[str, float]]
```

The type applies to stored values: list elements, hash values, set members, and so on. Keys, hash field names, scores and other metadata keep their own types; for example, `hash.hgetall[int]` returns `dict[bytes, int]`.

A missing value is `None`, and an empty collection is empty. A value that can't be decoded as the requested type (invalid UTF-8, or text that isn't a number) raises instead of returning `None`. Commands that change data and also return it, such as `getdel` or `lpop`, run before the result is decoded, so a decoding error doesn't undo them.

### Custom value types

To store your own types, give them an `encode` method that returns `bytes` and a `decode` class method that builds an instance from `bytes`. This is the `redis.RedisValue` protocol; you don't need to inherit from anything:

```python
import json
from dataclasses import dataclass


@dataclass
class User:
    name: str

    def encode(self) -> bytes:
        return json.dumps({"name": self.name}).encode()

    @classmethod
    def decode(cls, data: bytes) -> "User":
        return cls(**json.loads(data))


await sessions.string.set("user:1", User("Ada"))
user = await sessions.string.get[User]("user:1")  # User | None
```

Redis doesn't record which type a value was stored as, so use the same type when you read it back.

## Read and write handles

Encore reads which Redis commands each service uses from your code, to know which services read and which write each database. That's why a command must be called directly on its family, as in `sessions.string.get(...)`: Encore can see both the family and the command at that call. If you store a family in a variable or pass it to a function, Encore can't tell which commands run through it, and reports an error.

To hand access to a database to other code, such as a helper function or a FastAPI dependency, take a handle first:

- `sessions.read()` returns a `redis.ReadOnlyDatabaseRef`, which has only the read commands of each family.
- `sessions.write()` returns a `redis.DatabaseRef`, which has every command.

Handles can be passed around and stored freely; Encore records the access at the `read()` or `write()` call.

```python
from typing import Annotated

from encore.storage import redis
from fastapi import Depends, FastAPI

from sessions.store import sessions

app = FastAPI()


async def cache_reader() -> redis.ReadOnlyDatabaseRef:
    return sessions.read()


async def count_visits(db: redis.DatabaseRef, page: str) -> int:
    return await db.string.incr(f"visits:{page}")


@app.get("/pages/{page}")
async def get_page(
    page: str, reader: Annotated[redis.ReadOnlyDatabaseRef, Depends(cache_reader)]
):
    cached = await reader.string.get[str](f"page:{page}")
    visits = await count_visits(sessions.write(), page)
    return {"cached": cached, "visits": visits}
```

A read handle rejects writes at run time too, not only in your type checker.

> **Note**
>
> `read(primary=True)` asks for reads from the primary server even where replicas exist. Encore currently sends all reads to the primary, so the flag has no effect yet.

## Redis channels

The `pubsub` family is Redis's own publish/subscribe: fire-and-forget messages to whoever is subscribed to a channel at that moment. It's separate from Encore's [Pub/Sub](/docs/py/primitives/pubsub) topics, which store messages durably and retry failed deliveries.

Publish with `publish`, which returns how many subscribers received the message:

```python
await sessions.pubsub.publish("chat:lobby", "hello")
```

`subscribe` opens a dedicated connection and returns a subscription you iterate asynchronously. Use it as an async context manager so the connection is closed when you're done, even if your code raises or is cancelled:

```python
async def listen() -> None:
    async with await sessions.pubsub.subscribe("chat:lobby") as messages:
        async for message in messages:
            print(message.channel, message.payload)  # both bytes
```

`psubscribe` subscribes to channels matching glob patterns, such as `chat:*`. Both take several channels or patterns, and a type for the payloads, as in `subscribe[str]("chat:lobby")`. Each message has a `channel` (always `bytes`) and a decoded `payload`. You can also call `receive()`, which returns the next message or `None` once the subscription is closed, and `close()`.

## JSON

The `json` family works with [RedisJSON](https://redis.io/docs/latest/develop/data-types/json/) documents. Values are Python JSON values, encoded and decoded for you:

```python
await sessions.json.set("profile:1", "$", {"name": "Ada", "active": True})
names = await sessions.json.get("profile:1", "$.name")  # ["Ada"]
await sessions.json.numincrby("profile:1", "$.logins", 1)
```

A JSONPath (starting with `$`) can select several values, so replies to one are lists of matches.

> **Important**
>
> JSON commands need a Redis server with RedisJSON. The local emulator that `encore run` and `encore test` use doesn't support them yet.

## Connecting other clients

`connection_string` returns the database's Redis URL, including credentials, for use with another Redis client:

```python
url = sessions.connection_string
```

It's available on the database and on write handles, not on read handles, and grants full read-write access. Clients using it bypass Encore: they don't apply Encore's key namespacing, so they may not see the same keys as Encore's commands (under `encore test`, for example), and their commands aren't traced.

## Not supported yet

The Python SDK doesn't yet have:

- Persistence or eviction options on `Database`.
- Transactions (`MULTI`/`EXEC`), Lua scripts, or pipelining.
- Read replica routing (see `read(primary=True)` above).
- A way to reference a Redis database by name, as `Bucket.named` and `Topic.named` do. Import the declaring module's variable instead.

## Local development and testing

`encore run` runs a local Redis emulator for your databases, so you don't need to install Redis. Under `encore test`, each test gets its own namespace of keys and channels on a shared local server, so tests don't see each other's data. See [Testing](/docs/py/develop/testing).
