Cache

Opt-in Redis cache per resource, with namespace-based invalidation that does not blow away unrelated rows.

Enable on a resource

class SpaceResource(BaseResource):
    model = Space
    cache = True
    cache_ttl = 600   # optional; framework default is 120s

Now GET /spaces and GET /spaces/{id} are cached. POST/PATCH/DELETE invalidate automatically.

Project-level TTL controls

Two keys in the project's MCP bag override the framework default without touching resource code:

MCP = {
    'CACHE_TTL': 300,            # default 120s; framework default for resources
    'CACHE_TTL_ENABLE': True,    # default True; flip to False to kill cache globally
}

Cache key

Built from the request path and a hash of the querystring:

cache:/spaces:<md5(querystring)>
cache:/spaces/42

When session_cache = True, the session id is folded into the key โ€” useful for endpoints whose response depends on the user.

Namespaces and invalidation

Every cached entry is also added to a Redis set keyed by namespace:

Writes invalidate by namespace:

โœจ

Editing row 42 does not drop the cache for row 7. Lists are dropped because their contents may shift, but per-row pages stay warm.

Tenant isolation (automatic)

Multi-tenant deployments share Redis. _build_cache_key folds self.account_id into the key whenever it is set โ€” two tenants hitting the same path get different keys without any configuration. The fold uses account_id is not None, so an explicit account_id = 0 is treated as a real value (folded as a=0), not as absence.

# base prefix : [session_cache] : [a=<account_id>] : [scope=<hash>] : path : md5(qs)
cache:a=42:/clients
cache:sid-abc:a=42:/me
๐Ÿ›ก

No need to declare cache_scope_fields = [('account', 'id')] โ€” the framework already isolates per tenant. cache_scope_fields is for dimensions inside the same tenant (role, space, plan).

Single-tenant deployments that want to keep the legacy key shape (no a= segment) can opt out via MCP = {'AUTO_SCOPE_CACHE_BY_ACCOUNT': False} in settings.py. Anonymous endpoints skip the fold automatically.

๐Ÿช

If your project overrides _build_cache_key, call self._account_cache_segment() and append the result to the key so the override inherits the tenant isolation. Otherwise the override silently re-introduces the cross-tenant cache leak.

Per-scope caching โ€” cache_scope_fields

When the response depends on a dimension carried by the session (user role, space, plan, country), declare it with cache_scope_fields. Users sharing the same scope share the cache; different scopes get isolated keys; the namespace-based invalidation is unchanged.

class TaskResource(BaseResource):
    model = Task
    cache = True
    # Strings are shorthand for ('user', field). Tuples select the
    # source explicitly: 'user' or 'account'.
    cache_scope_fields = ['space_id', ('account', 'plan_id')]

The framework folds the configured fields into a 16-char MD5 hash and prepends it to the request path inside the cache key. Anonymous requests (self.user is None and self.account is None) skip the fold silently.

๐Ÿ›ก

Fail-safe. When auth context exists but a configured scope field is missing from the session payload, the framework logs a WARNING (logger zeromcp.base) and disables cache for that request โ€” the response is neither read from nor written to Redis. Sharing a key across users when the scope can't be resolved would silently leak data. Anonymous requests skip the fold cleanly. None, 0 and '' count as present; only true absence triggers the warning.

๐Ÿช

Coexists with session_cache = True. Both fold into the key without conflict โ€” session_cache gives full per-session isolation (lowest hit rate), cache_scope_fields keeps users on the same scope sharing cache (best hit rate when the dimension count is small).

before_cache hook

Escape hatch for the rare case that needs context outside self.user / self.account โ€” feature flags, request headers, time-of-day. Override before_cache(request):

async def before_cache(self, request):
    flag = await get_feature_flag(self.user)
    self.cache_key += f':flag={flag}'

For routine user/account-derived dimensions, prefer cache_scope_fields โ€” it is declarative and centrally validated.

Custom routes

Custom routes opt in via the cache flag in their route definition:

routes = [
    {'path': r'/me$', 'func': 'me', 'allowed_methods': ['get'], 'cache': True},
]

Hit ratio

Every request increments hit/miss counters in Redis:

Use zeromcp.redis_config.get_cache_stats() to read them, or build an admin endpoint.

0-mcp by Stamatios Stamou Jr โ€” github.com/ssjunior/0-mcp