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.
| 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
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 |
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
| Page | What it covers |
|---|---|
| Rate limiting and firewall | The check that runs before the cache is consulted |
| REST channels | The channels whose responses you cache |
| API Management | Quotas, analytics and the rest of the management layer |