# API response caching

Serve repeated API requests from the cache and invoke your services only when needed.

Response caching stores the responses of REST and SOAP channels so that repeated requests are served from the cache, without invoking the underlying service at all. Caching is configured per channel in the Dashboard or in enmasse, with no changes to the service behind it, and the cache is consulted only after [rate limiting](https://zato.io/docs/admin/guide/rate-limit.html) - a request over its limits never reaches the cache.

- Serve a product catalog or a reference-data endpoint from the cache for five minutes at a time, so that thousands of identical requests result in a single backend call

- Absorb a thundering herd - when many clients ask for the same uncached resource at once, only one of them invokes the service and everyone else waits for that one response

- Give each API partner its own cached view of the same endpoint, with per-caller responses kept separate

- Let clients revalidate with ETags and receive bodyless 304 responses when nothing changed

## Cache keys and stored responses {#cache-keys-and-stored-responses}

### The cache key {#the-cache-key}

Every request maps to a cache key built from:

- The channel
- The HTTP method
- The matched path - path parameters vary the key automatically
- The query string, sorted by parameter name, so parameter order never splits the cache
- The caller's security definition, unless the channel shares responses across callers
- The values of any headers that the channel varies by
- A hash of the request body, when the body is part of the key

Per-caller keys are the default - each consumer receives its own cached responses. If an endpoint returns the same data to everyone, tick **Shared across callers** and all callers share one cached response per request.

### Stored responses {#stored-responses}

GET and HEAD responses are cacheable as they are. POST responses are cacheable only when **Include body in key** is on, because the response then depends on what the request body contains. On SOAP channels the body always joins the key - the operation is named in the POST body, so method plus path identifies nothing there.

The cache stores only 200-class responses. It never stores responses that contain `Set-Cookie`, nor requests or responses larger than **Max body size**.

### Cache on second request {#cache-on-second-request}

By default, the first miss of a key stores only a small marker. Zato stores the full response only when the same request repeats, so a one-time request stores a marker.

Turn **Cache on second request** off for low-cardinality channels where every entry is known to repeat, and the channel stores responses on the first miss.

### Request coalescing {#request-coalescing}

When many concurrent requests arrive for the same uncached resource, only one of them invokes the service - the others wait for that one invocation and are served its response. If that one invocation is slow, a request that has waited longer than **Coalesce timeout** seconds stops waiting and invokes the service itself.

### Client refresh {#client-refresh}

A request with `Cache-Control: no-cache` skips the cache lookup, but Zato still stores its fresh response - standard refresh semantics. This is the only client-controlled cache behavior.

## Configuration {#configuration}

In Dashboard, navigate to the REST or SOAP channel list via `Connections -> Channels`. Each channel row has a **Cache** link that opens the configuration form.

| Field | Default | Meaning |
| --- | --- | --- |
| Enabled | Off | Whether responses of this channel are cached at all |
| Cache responses for | 5 minutes | How long each stored response lives, in seconds, minutes or hours |
| Cache on second request | On | Store a response only once its request repeats |
| Shared across callers | Off | All callers share one cached response per request |
| Include body in key | Off | Lets POST requests be cached - on SOAP channels it is always on and cannot be turned off |
| Vary by headers | Empty | Header names whose values vary the key, for example `Accept-Language` |
| Ignored query parameters | Empty | Parameter names stripped from the key, for example `utm_source` |
| Max body size (bytes) | 1,000,000 | Requests and responses above it are not cached |
| ETag support | Off | Return 304 Not Modified when `If-None-Match` matches |
| Coalesce timeout (seconds) | 15 | How long a request may wait for another one computing the same entry |

Changes take effect immediately, without a server restart, and every configuration change purges the channel's existing cache entries, so stale responses can never outlive a config change.

The **Clear cache** link on the same page deletes all the cached responses of the channel on demand, after an inline confirmation.

## Response headers {#response-headers}

Cached channels add these headers to their responses:

| Header | Meaning |
| --- | --- |
| `X-Cache` | `Hit` when the response came from the cache, `Miss` otherwise |
| `Age` | How many seconds ago the served entry was stored, on hits only |
| `ETag` | The entry's ETag, when ETag support is on |

```bash
$ curl -v http://localhost:11223/api/catalog

< HTTP/1.1 200 OK
< X-Cache: Hit
< Age: 42
```

## ETag and 304 Not Modified {#etag-and-304-not-modified}

With **ETag support** on, every stored entry has an ETag derived from its body. A client that presents the matching value in `If-None-Match` receives a bodyless `304 Not Modified` straight from the cache:

```bash
$ curl -v -H 'If-None-Match: 9f86d081884c7d65...' http://localhost:11223/api/catalog

< HTTP/1.1 304 Not Modified
< X-Cache: Hit
< ETag: 9f86d081884c7d65...
```

## Programmatic invalidation {#programmatic-invalidation}

Caching becomes usable for data that changes irregularly when the service handling a write purges the read channel's entries as part of the same operation. Every service can do that through `self.cache.invalidate_response`:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class CustomerUpdate(Service):

    def handle(self):

        # .. update the customer in the backend system here ..

        # Purge everything the read channel has cached ..
        self.cache.invalidate_response('customer.get.channel')

        # .. or only the entries whose path and query match a pattern.
        self.cache.invalidate_response('customer.get.channel', '/api/customers*')
```

The first argument is the channel's name. Without a pattern, the call purges the channel's whole cache. With a pattern, it deletes only the entries whose path and query match.

## Metrics {#metrics}

Every cache operation increments the `zato_rest_channel_cache_operations_total` counter, labeled with the channel name and the outcome - `hit`, `miss`, `stored`, `marker_stored`, `coalesced`, `coalesce_timeout`, `not_cached` or `not_modified`. The hit ratio per channel is then one PromQL expression:

```promql
sum by (channel_name) (rate(zato_rest_channel_cache_operations_total{outcome=~"hit|coalesced|not_modified"}[5m]))
/
sum by (channel_name) (rate(zato_rest_channel_cache_operations_total{outcome=~"hit|coalesced|not_modified|miss"}[5m]))
```

The full metric reference is on the [Prometheus reference](https://zato.io/docs/monitoring/prometheus-reference.html) page.

## Automation with enmasse {#automation-with-enmasse}

Response caching round-trips through [enmasse](https://zato.io/docs/admin/enmasse.html) as a `response_cache` block on `channel_rest` and `channel_soap` entries - only the fields that differ from the defaults are exported:

```yaml
channel_rest:
  - name: my.api.channel
    service: my.service
    url_path: /api/v1/catalog
    response_cache:
      is_enabled: true
      ttl: 10
      ttl_unit: minutes
      is_shared_across_callers: true
      ignored_query_parameters:
        - utm_source
        - utm_medium
```

## Storage details {#storage-details}

- The cache is backed by the same Redis-based store that services use through [self.cache](https://zato.io/docs/dev/examples/cache.html), so entries survive server restarts and expire per their TTL
- Deleting a channel purges its cached responses

## See also {#see-also}

- [Rate limiting and firewall](https://zato.io/docs/admin/guide/rate-limit.html) - The check that runs before the cache is consulted
- [REST channels](https://zato.io/docs/dev/rest/channels.html) - The channels whose responses you cache
- [API Management](https://zato.io/docs/api-management/index.html) - Quotas, analytics and the rest of the management layer

## Learn more {#learn-more}

- [REST integration overview](https://zato.io/integrations/rest/) - Start here to see how teams build REST integrations
- [In-depth API tutorial](https://zato.io/tutorials/main/01.html) - Master architectural patterns for production-ready integrations
- [Modern REST API tutorial](https://zato.io/tutorials/rest-api/python.html) - Design, build and secure REST APIs with Python
