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 straight 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 - 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, turning thousands of identical backend calls into one

  • 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

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

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 lives 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

By default, the first miss of a key stores only a small marker, not the response. 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

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

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

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.

FieldDefaultMeaning
EnabledOffWhether responses of this channel are cached at all
Cache responses for5 minutesHow long each stored response lives, in seconds, minutes or hours
Cache on second requestOnStore a response only once its request repeats
Shared across callersOffAll callers share one cached response per request
Include body in keyOffLets POST requests be cached - on SOAP channels it is always on and cannot be turned off
Vary by headersEmptyHeader names whose values vary the key, for example Accept-Language
Ignored query parametersEmptyParameter names stripped from the key, for example utm_source
Max body size (bytes)1,000,000Requests and responses above it are not cached
ETag supportOffReturn 304 Not Modified when If-None-Match matches
Coalesce timeout (seconds)15How 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

Cached channels add these headers to their responses:

HeaderMeaning
X-CacheHit when the response came from the cache, Miss otherwise
AgeHow many seconds ago the served entry was stored, on hits only
ETagThe entry's ETag, when ETag support is on
$ curl -v http://localhost:17010/api/catalog

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

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:

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

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

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:

# -*- 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

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:

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 page.

Automation with enmasse

Response caching round-trips through enmasse as a response_cache block on channel_rest and channel_soap entries - only the fields that differ from the defaults are exported:

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

  • The cache is backed by the same Redis-based store that services use through self.cache, so entries survive server restarts and expire per their TTL
  • Deleting a channel purges its cached responses

See also

PageWhat it covers
Rate limiting and firewallThe check that runs before the cache is consulted
REST channelsThe channels whose responses you cache
API ManagementQuotas, analytics and the rest of the management layer

Learn more