> ## Documentation Index
> Fetch the complete documentation index at: https://wundergraphinc-milinda-router-80-entity-caching-base-implem.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Response Cache (Experimental)

> Cache federation entity fetches to avoid fetching from upstream subgraphs.

<Danger>
  **Experimental feature.** Response caching is in alpha and is subject to change, and should not be used in production environments.
</Danger>

The router caches the responses to federation entity fetches. An entity fetch is the request the router sends to a subgraph through the `_entities` root field to resolve fields of an entity that another subgraph owns. Root field fetches are not cached.

Entries are keyed per entity and per selection set. Asking for different fields of the same entity does not share an entry.

Response caching is disabled by default.

## What gets cached

A subgraph response is cached only when its `Cache-Control` header asks to be. The rules apply in this order:

1. `no-store` refuses caching.
2. `no-cache` or `private` refuses caching, in any form. This includes the qualified `no-cache="Set-Cookie"` form.
3. `public` is required. `Cache-Control: max-age=60` on its own is not cached.
4. `s-maxage` sets the lifetime and takes precedence over `max-age`.
5. `max-age` sets the lifetime when `s-maxage` is absent.
6. `fallback_ttl` sets the lifetime when the response says `public` and names no lifetime of its own.

A value of `0` for `s-maxage` or `max-age` refuses caching. It does not fall through to the next rule.

A subgraph that sends no `Cache-Control` header at all is never cached. `fallback_ttl` does not apply to it.

A response carrying GraphQL errors is never cached, whatever its `Cache-Control` header says.

<Info>
  Configuring the response cache does not make anything cacheable on its own. Subgraphs opt in by sending `Cache-Control: public`. If nothing appears to be cached, check the subgraph response headers first.
</Info>

## Enable it

### Redis

Every router replica shares one cache and entries outlive the process. Define a [storage provider](/router/storage-providers), then reference it by `provider_id`.

<CodeGroup>
  ```yaml config.yaml theme={null}
  storage_providers:
    redis:
      - id: 'my_redis'
        cluster_enabled: false
        urls:
          - 'redis://localhost:6379'

  response_cache:
    enabled: true
    fallback_ttl: 30s
    key_prefix: 'cosmo_response_cache:'
    storage:
      provider: 'redis'
      provider_id: 'my_redis'
  ```
</CodeGroup>

An unknown `provider_id` fails startup.

### Router memory

Each replica holds its own cache, nothing survives a restart, and no Redis is needed.

<CodeGroup>
  ```yaml config.yaml theme={null}
  response_cache:
    enabled: true
    fallback_ttl: 30s
    storage:
      provider: 'memory'
      max_entries: 2048
  ```
</CodeGroup>

`provider` defaults to `redis`, so caching in memory has to be asked for by name. This prevents a missing `provider_id` from quietly turning one shared cache into one cache per replica.

## Configuration reference

```yaml config.yaml theme={null}
response_cache:
  # Enable response caching. Default: false.
  enabled: true
  # Lifetime for a response that says public without saying for how long.
  # A response naming its own max-age or s-maxage gets that instead.
  # Minimum 1s. Default: 30s.
  fallback_ttl: 30s
  # Prepended to every cache key so entries cannot collide with anything else
  # sharing the Redis instance. Redis only, ignored by the memory provider.
  # Default: cosmo_response_cache:
  key_prefix: 'cosmo_response_cache:'
  storage:
    # redis or memory. Default: redis.
    provider: 'redis'
    # ID of a provider declared under storage_providers.redis.
    # Required unless provider is memory.
    provider_id: 'my_redis'
    # Memory provider only. Default: 10000.
    max_entries: 10000
```

`storage` is required when `enabled` is `true`.

Environment variables bypass the config schema validation. A `fallback_ttl` of zero or less fails startup either way.

## Limitations

These apply to the current alpha.

**A batch is served from the cache only when every entity in it is present.** One miss sends the whole batch to the subgraph, including the entities that were already cached. Entities are stored individually, so a later batch made up only of cached entities is a full hit.

**Cache hits produce no subgraph telemetry.** A hit skips the subgraph fetch, so no subgraph span or metric is recorded for it. Subgraph request counts fall as the hit rate rises. Use them to confirm the cache is working, not to measure traffic.

**`enable_multi_fetch` disables response caching.** When `engine.enable_multi_fetch` is on, the router merges entity fetches to the same subgraph within one parallel wave into a single request. Merged fetches are not cached, and nothing reports that they were skipped. Do not enable both and expect response caching.

## Observability

The router logs once at startup when the cache is enabled. With the Redis provider:

```
INFO  Response cache enabled  {"fallback_ttl": "30s", "storage_provider": "redis", "key_prefix": "cosmo_response_cache:", "storage_provider_id": "my_redis"}
```

With the memory provider:

```
INFO  Response cache enabled  {"fallback_ttl": "30s", "storage_provider": "memory", "max_entries": 2048}
```

Nothing is logged when the cache is disabled.

Cache failures never fail a request. When a read or a write fails, the router serves from the subgraph and logs a warning:

```
WARN  Response cache degraded, serving from the subgraph instead
```

That warning is sampled to one line per second. An unreachable cache produces one failure per entity fetch of every request in flight, and logging it unsampled would compound the outage.

## Related

* [Storage Providers](/router/storage-providers) defines the Redis instance the `redis` provider references.
* [Cache Warmer](/concepts/cache-warmer) is a different cache. It pre-populates the operation plan cache and does not cache subgraph responses.
