---
title: "Cache"
section: "Extensions"
framework: WebFluid
package: webfluid
version: "1.0.0b3"
stage: "beta"
released: "August 17, 2026"
canonical: "https://docs.webfluid.dev/latest/ext/cache.md"
html: "https://docs.webfluid.dev/latest/ext/cache"
audience: ai-agent
format: markdown
---

# Cache


`EXT_CACHE` is a small key-value API with two interchangeable backends: `redis` (the default,
production) and `legacy` (an in-process dict with an expiry heap). Every method has an async twin.


## Enabling

```ini
[extensions]
EXT_CACHE = 1

[data]
REDIS_URI = redis://localhost:6379

[cache]
CACHE_TYPE = redis
```

| Key                     | Default                                        | Notes                                                         |
|-------------------------|------------------------------------------------|---------------------------------------------------------------|
| `CACHE_TYPE`            | `"redis"`                                      | `"redis"` or `"legacy"`. An unknown value raises `ValueError` |
| `CACHE_REDIS_URI`       | `f"{REDIS_URI or 'redis://localhost:6379'}/2"` | Database **2** by default; rate limiting uses **1**           |
| `CACHE_DEFAULT_TIMEOUT` | `300`                                          | Seconds, used when `timeout` is omitted                       |

## The API

```python
from webfluid.core.ext import cache

cache.set(key, value, timeout=None)
cache.get(key)
cache.delete(key)
cache.clear()

await cache.aset(key, value, timeout=None)
await cache.aget(key)
await cache.adelete(key)
await cache.aclear()

cache.backend        # the concrete RedisCache / LegacyCache instance
```

`get` returns `None` for a miss **and** for an expired entry. `clear` / `aclear` is `flushdb` on
redis — it wipes the whole database, including the JWT keys if they share it.

## Backend differences that change your code

|                         | `redis`                            | `legacy`                        |
|-------------------------|------------------------------------|---------------------------------|
| Storage                 | Redis, `decode_responses=True`     | In-process `dict` + expiry heap |
| Value types             | **Everything comes back as `str`** | Whatever you put in, unchanged  |
| Shared across processes | Yes                                | **No**                          |
| Survives a restart      | Yes                                | No                              |
| Needs a server          | Yes                                | No                              |
| Lazy connection         | Client created on first use        | —                               |

> **RULE** — Decode on the way out when you might run against redis. cache.aget returns a string there and the original object on legacy, so int(cached), json.loads(cached) or an explicit str() round-trip is the only shape that works on both. Do not store objects that do not round-trip through a string.

> **WARNING** — CACHE_TYPE = legacy is wrong the moment you run more than one worker: two processes are two caches with two answers. It is also wrong for EXT_JWT — the signing keys live in the cache, so an in-process cache means every restart invalidates every issued token. Use redis for anything you deploy.

## Typical use: memoising a query

```python
# fluid/events/models.py
from webfluid.core.ext import db, cache, events
from sqlalchemy import select, func
from fluid.models import MyModel


def register():
    events.create_signal("model:created", internal=False)

    @events.query("model:count")
    async def count_models(_):
        cached = await cache.aget("model:count")
        if cached is not None:
            return int(cached)

        async with db.async_executor(model=MyModel) as e:
            result = await e.exec(select(func.count(MyModel.id)), scalars=False)
            total = result.scalar()

        await cache.aset("model:count", total, timeout=60)
        return total

    @events.event("model:created", internal=False)
    async def bust_count(_):
        await cache.adelete("model:count")
```

The pattern: read-through on the query, invalidate on the event that changes the underlying data.
Note `int(cached)` — redis hands back a string.

## Key naming

There is no automatic namespacing. Everything shares one keyspace, including the framework:

| Prefix                                          | Owner     |
|-------------------------------------------------|-----------|
| `jwt:current`, `jwt:<kid>`, `jwt:revoked:<jti>` | `EXT_JWT` |
| anything else                                   | you       |

> **RULE** — Prefix your keys with a namespace of your own — myapp:… for the main app, <additive_id>:… for an Additive. Never write under jwt:. And never call clear()/aclear() in an app that also uses JWT: flushdb drops the signing keys and invalidates every issued token.

## Rules

> **RULE** — Never cache anything you cannot reconstruct. A cache entry can vanish at any moment — eviction, a restart, a flush. Treat every read as a possible miss and always have the slow path available.

> **RULE** — Always pass an explicit timeout for values that can go stale. CACHE_DEFAULT_TIMEOUT is 300 seconds; relying on it makes the lifetime invisible at the call site.

> **WARNING** — Both backends resolve the timeout as `timeout or self._default_timeout`, so timeout=0 silently becomes CACHE_DEFAULT_TIMEOUT rather than 'no expiry' or 'expire immediately'. There is no way to store an entry without a TTL.

> **RULE** — Use the async methods inside async code. The redis backend's sync client is a blocking socket call on the event loop.

## Who else uses it

- **`EXT_JWT`** requires this switch. It stores its rotating signing secrets under `jwt:<kid>`,
  the current key id under `jwt:current`, and checks `jwt:revoked:<jti>` on decode.
- **Rate limiting** uses redis directly through `RATELIMIT_STORAGE_URI` (database 1), not through
  this extension.

## Next

- [`ext/jwt.md`](/latest/ext/jwt.md) — the battery that depends on this one.
- [`ext/events.md`](/latest/ext/events.md) — where the invalidation event comes from.


---

## Navigation


- [Overview](/latest/.md) — what WebFluid is, how to navigate these docs, release state and known bugs
- [Getting Started](/latest/get-started.md) — install, minimal app, the layout the framework expects

**Configuration**
- [App configs](/latest/config/app-config.md) — `app_configs/<app>.ini`, switches, `*_FILE` secrets, `[dev]`
- [Config classes](/latest/config/config-class.md) — `register_config`, `DefaultConfig`, every framework key

**Extensions**
- [Introduction](/latest/ext/base.md) — `FluidExtension`, entry points, `expand_fluid`
- [Scheduling](/latest/ext/scheduling.md) — APScheduler `AsyncIOScheduler`
- [SQLAlchemy](/latest/ext/sqlalchemy.md) — `Model`, executors, binds, sessions
- [Migrate](/latest/ext/migrate.md) — Alembic wrapper, `prepare_fluid`, single/multi-db
- [Mail](/latest/ext/mailman.md) — SMTP, sync/async, batching, attachments
- [Babel](/latest/ext/babel.md) — gettext, domains, locale selection, formatters
- [Security](/latest/ext/security.md) — users, gates, CSRF, tokens, OAuth, bearer grants
- [Events](/latest/ext/events.md) — signals, events, queries, browser socket
- [Cache](/latest/ext/cache.md) — redis / legacy backends
- [JWTManager](/latest/ext/jwt.md) — encode/decode, rotation, revocation

**Frontend (surface)**
- [Introduction](/latest/surface/tooling.md) — feature switches, `fluid_base.html`, Tailwind, themes
- [Integration](/latest/surface/frontend.md) — `APP_FRONTEND`, htmx, Vite workspaces
- [Template resolution](/latest/surface/jinja.md) — loader order, namespaces, overriding

**Additives**
- [Introduction](/latest/additives/intro.md) — manifest, routers, enabling
- [Base Additives](/latest/additives/base.md) — `type: base`, `import_base`, extension semantics
- [Interaction between](/latest/additives/contract.md) — events/queries as contracts, requirements, packaging

**Framework utility**
- [Lifecycle](/latest/utils/lifecycle.md) — `mix()`, startup/shutdown hooks, graceful stop
- [Runtime](/latest/utils/runtime.md) — `FluidContext`, request hooks, themes, proxy, rate limits
- [Logging](/latest/utils/logging.md) — the log factory, levels, files, additive attribution

**CLI**
- [Introduction](/latest/cli/wf.md) — command tree, extension CLIs
- [Create](/latest/cli/create.md) — `wf create project/app/additive`
- [Run](/latest/cli/run.md) — `wf run`, flags, debug and interactive mode
- [Ocean](/latest/cli/ocean.md) — search, install, publish

**Reference**
- [Overview](/latest/ref.md) — package layout, stability contract
- [Core](/latest/ref/core.md) — `Fluid`, config, context, `core.ext`, constants
- [Extensions](/latest/ref/extensions.md) — the battery API surface
- [Surface](/latest/ref/surface.md) — `Frontend`, Node and Tailwind tooling
- [Additives](/latest/ref/additives.md) — `Additive`, `Router`, `Manifest`, registry helpers
- [Utils](/latest/ref/utils.md) — helpers, logging, exceptions