zuplo 7.0.0 → 7.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/articles/ci-cd-azure/basic-deployment.mdx +2 -2
- package/docs/articles/ci-cd-azure/deploy-and-test.mdx +1 -1
- package/docs/articles/ci-cd-azure/local-testing.mdx +2 -2
- package/docs/articles/ci-cd-azure/multi-stage-deployment.mdx +3 -3
- package/docs/articles/ci-cd-azure/pr-preview-environments.mdx +1 -1
- package/docs/articles/ci-cd-azure/tag-based-releases.mdx +1 -1
- package/docs/articles/ci-cd-bitbucket/basic-deployment.mdx +1 -1
- package/docs/articles/ci-cd-bitbucket/deploy-and-test.mdx +1 -1
- package/docs/articles/ci-cd-bitbucket/local-testing.mdx +1 -1
- package/docs/articles/ci-cd-bitbucket/multi-stage-deployment.mdx +1 -1
- package/docs/articles/ci-cd-bitbucket/pr-preview-environments.mdx +1 -1
- package/docs/articles/ci-cd-bitbucket/tag-based-releases.mdx +1 -1
- package/docs/articles/ci-cd-circleci/basic-deployment.mdx +1 -1
- package/docs/articles/ci-cd-circleci/deploy-and-test.mdx +1 -1
- package/docs/articles/ci-cd-circleci/local-testing.mdx +2 -2
- package/docs/articles/ci-cd-circleci/multi-stage-deployment.mdx +3 -3
- package/docs/articles/ci-cd-circleci/pr-preview-environments.mdx +1 -1
- package/docs/articles/ci-cd-circleci/tag-based-releases.mdx +1 -1
- package/docs/articles/ci-cd-github/basic-deployment.mdx +1 -1
- package/docs/articles/ci-cd-github/cleanup-on-branch-delete.mdx +2 -2
- package/docs/articles/ci-cd-github/deploy-and-test.mdx +1 -1
- package/docs/articles/ci-cd-github/local-testing.mdx +2 -2
- package/docs/articles/ci-cd-github/multi-stage-deployment.mdx +3 -3
- package/docs/articles/ci-cd-github/pr-preview-environments.mdx +2 -2
- package/docs/articles/ci-cd-github/tag-based-releases.mdx +1 -1
- package/docs/articles/ci-cd-gitlab/basic-deployment.mdx +1 -1
- package/docs/articles/ci-cd-gitlab/deploy-and-test.mdx +1 -1
- package/docs/articles/ci-cd-gitlab/local-testing.mdx +1 -1
- package/docs/articles/ci-cd-gitlab/mr-preview-environments.mdx +1 -1
- package/docs/articles/ci-cd-gitlab/multi-stage-deployment.mdx +1 -1
- package/docs/articles/ci-cd-gitlab/tag-based-releases.mdx +1 -1
- package/docs/articles/github-deployment-testing.mdx +1 -1
- package/docs/articles/graphql-caching.mdx +2 -0
- package/docs/articles/local-development.mdx +4 -4
- package/docs/articles/monorepo-deployment.mdx +3 -3
- package/docs/articles/sharing-code-across-projects.mdx +2 -2
- package/docs/articles/step-1-setup-basic-gateway-local.mdx +1 -1
- package/docs/articles/testing.mdx +5 -5
- package/docs/articles/troubleshooting-slow-responses.mdx +5 -0
- package/docs/articles/update-zup-in-github-action.mdx +1 -1
- package/docs/caching/cdn-caching.mdx +397 -0
- package/docs/caching/custom-caching-policy.mdx +426 -0
- package/docs/caching/dynamic-cache-rules.mdx +427 -0
- package/docs/caching/gateway-caching.mdx +308 -0
- package/docs/caching/overview.mdx +150 -0
- package/docs/caching/partial-response-caching.mdx +641 -0
- package/docs/cli/overview.mdx +2 -3
- package/docs/dedicated/akamai/caching.mdx +245 -140
- package/docs/dev-portal/local-development.mdx +1 -1
- package/docs/dev-portal/migration.mdx +1 -1
- package/docs/mcp-gateway/quickstart-local.mdx +1 -1
- package/docs/policies/api-key-inbound/schema.json +4 -2
- package/docs/programmable-api/cache.mdx +7 -0
- package/docs/programmable-api/memory-zone-read-through-cache.mdx +2 -0
- package/docs/programmable-api/streaming-zone-cache.mdx +2 -0
- package/docs/programmable-api/zone-cache.mdx +7 -0
- package/package.json +5 -5
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Cache at the gateway
|
|
3
|
+
description:
|
|
4
|
+
Use the Caching policy to store responses inside the Zuplo gateway so repeat
|
|
5
|
+
requests never reach your backend. Covers cache keys, TTLs, authorization, and
|
|
6
|
+
invalidation.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
The Caching policy (`caching-inbound`) stores a response inside the gateway and
|
|
10
|
+
replays it for the next identical request. On a hit the policy answers from the
|
|
11
|
+
inbound pipeline and the backend never sees the request at all, which is the
|
|
12
|
+
whole point when the backend is the slow, rate-limited, or expensive part of the
|
|
13
|
+
system.
|
|
14
|
+
|
|
15
|
+
## When the gateway cache is the right layer
|
|
16
|
+
|
|
17
|
+
Gateway caching earns its place in three situations.
|
|
18
|
+
|
|
19
|
+
**No CDN sits in front of the gateway.** Traffic goes straight to Zuplo, so the
|
|
20
|
+
gateway is the only place a response can be reused. This is the common case for
|
|
21
|
+
internal APIs, partner APIs, and anything served directly from a Zuplo domain.
|
|
22
|
+
|
|
23
|
+
**The backend is slow, rate-limited, or billed per call.** A catalog search
|
|
24
|
+
endpoint that takes 900 ms of database time, or an upstream provider that allows
|
|
25
|
+
10 requests per second and charges per lookup, is worth protecting even when a
|
|
26
|
+
CDN also caches. A 60-second TTL on an endpoint receiving 200 requests per
|
|
27
|
+
minute turns 12,000 backend calls per hour into 60.
|
|
28
|
+
|
|
29
|
+
**The response must not leave the gateway boundary.** Data that policy or
|
|
30
|
+
compliance keeps off a third-party edge network can still be cached, because the
|
|
31
|
+
entry lives inside your Zuplo environment rather than in CDN storage.
|
|
32
|
+
|
|
33
|
+
:::tip
|
|
34
|
+
|
|
35
|
+
If a CDN already fronts your gateway, cache there first. A CDN hit is served
|
|
36
|
+
from a point of presence near the caller and never touches Zuplo at all, so it
|
|
37
|
+
is both cheaper and faster than a gateway hit. See
|
|
38
|
+
[Cache at the CDN](./cdn-caching.mdx) for the `cdn-cache-control-outbound`
|
|
39
|
+
policy. Use the gateway cache for what the CDN can't cover, not as a duplicate
|
|
40
|
+
layer.
|
|
41
|
+
|
|
42
|
+
:::
|
|
43
|
+
|
|
44
|
+
## How it works
|
|
45
|
+
|
|
46
|
+
The policy builds a cache key from the request, looks it up, and either answers
|
|
47
|
+
immediately or lets the request continue to the handler and stores the result on
|
|
48
|
+
the way out. By default the key covers the request method, the URL path, and the
|
|
49
|
+
query parameters, plus the `Authorization` header, any headers you name, and the
|
|
50
|
+
`cacheId` value. Two requests share an entry only when every one of those parts
|
|
51
|
+
matches.
|
|
52
|
+
|
|
53
|
+
<Diagram type="sequence" height="h-[420px]">
|
|
54
|
+
<DiagramActor id="client" variant="yellow">
|
|
55
|
+
Client
|
|
56
|
+
</DiagramActor>
|
|
57
|
+
<DiagramActor id="policy" variant="zuplo">
|
|
58
|
+
Caching policy
|
|
59
|
+
</DiagramActor>
|
|
60
|
+
<DiagramActor id="backend" variant="green">
|
|
61
|
+
Backend
|
|
62
|
+
</DiagramActor>
|
|
63
|
+
<DiagramMessage from="client" to="policy">
|
|
64
|
+
GET /products?page=1 (miss)
|
|
65
|
+
</DiagramMessage>
|
|
66
|
+
<DiagramMessage from="policy" to="backend">
|
|
67
|
+
Forward request
|
|
68
|
+
</DiagramMessage>
|
|
69
|
+
<DiagramMessage from="backend" to="policy">
|
|
70
|
+
200 OK
|
|
71
|
+
</DiagramMessage>
|
|
72
|
+
<DiagramMessage from="policy" to="client">
|
|
73
|
+
200 OK, response stored
|
|
74
|
+
</DiagramMessage>
|
|
75
|
+
<DiagramMessage from="client" to="policy">
|
|
76
|
+
GET /products?page=1 (hit)
|
|
77
|
+
</DiagramMessage>
|
|
78
|
+
<DiagramMessage from="policy" to="client">
|
|
79
|
+
200 OK from cache
|
|
80
|
+
</DiagramMessage>
|
|
81
|
+
</Diagram>
|
|
82
|
+
|
|
83
|
+
The second request never reaches the backend. Neither does anything else the
|
|
84
|
+
route would normally do after the caching policy: a hit short-circuits the rest
|
|
85
|
+
of the pipeline.
|
|
86
|
+
|
|
87
|
+
## Add the policy to a route
|
|
88
|
+
|
|
89
|
+
Add the policy to `config/policies.json`. This example caches `GET` responses
|
|
90
|
+
for five minutes and keys them by locale.
|
|
91
|
+
|
|
92
|
+
```json title="config/policies.json"
|
|
93
|
+
{
|
|
94
|
+
"policies": [
|
|
95
|
+
{
|
|
96
|
+
"name": "product-cache",
|
|
97
|
+
"policyType": "caching-inbound",
|
|
98
|
+
"handler": {
|
|
99
|
+
"export": "CachingInboundPolicy",
|
|
100
|
+
"module": "$import(@zuplo/runtime)",
|
|
101
|
+
"options": {
|
|
102
|
+
"expirationSecondsTtl": 300,
|
|
103
|
+
"cacheHttpMethods": ["GET"],
|
|
104
|
+
"statusCodes": [200, 404],
|
|
105
|
+
"headers": ["Accept-Language"],
|
|
106
|
+
"cacheId": "$env(CACHE_ID)",
|
|
107
|
+
"dangerouslyIgnoreAuthorizationHeader": false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Every option is optional. The defaults are:
|
|
116
|
+
|
|
117
|
+
| Option | Default | What it controls |
|
|
118
|
+
| -------------------------------------- | ------------------------------------- | ------------------------------------------------ |
|
|
119
|
+
| `expirationSecondsTtl` | `60` | How long an entry stays valid, in seconds |
|
|
120
|
+
| `cacheHttpMethods` | `["GET"]` | Which methods are cached |
|
|
121
|
+
| `statusCodes` | `[200, 206, 301, 302, 303, 404, 410]` | Which response status codes are stored |
|
|
122
|
+
| `headers` | `[]` | Extra request headers folded into the cache key |
|
|
123
|
+
| `cacheId` | none | An arbitrary string in the key, used for busting |
|
|
124
|
+
| `dangerouslyIgnoreAuthorizationHeader` | `false` | Whether to drop `Authorization` from the key |
|
|
125
|
+
|
|
126
|
+
`cacheHttpMethods` accepts `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, and `HEAD`.
|
|
127
|
+
Caching a method that changes state is almost always a mistake; leave the
|
|
128
|
+
default unless you have a read-only `POST` endpoint such as a search query.
|
|
129
|
+
|
|
130
|
+
Then reference the policy by name on the route:
|
|
131
|
+
|
|
132
|
+
```json title="config/routes.oas.json"
|
|
133
|
+
{
|
|
134
|
+
"paths": {
|
|
135
|
+
"/products": {
|
|
136
|
+
"get": {
|
|
137
|
+
"operationId": "list-products",
|
|
138
|
+
"x-zuplo-route": {
|
|
139
|
+
"handler": {
|
|
140
|
+
"export": "urlForwardHandler",
|
|
141
|
+
"module": "$import(@zuplo/runtime)",
|
|
142
|
+
"options": { "baseUrl": "https://api.example.com" }
|
|
143
|
+
},
|
|
144
|
+
"policies": { "inbound": ["product-cache"] }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The string in `policies.inbound` must match the policy's `name` exactly. Put the
|
|
153
|
+
caching policy after authentication policies so that unauthenticated requests
|
|
154
|
+
are rejected before they can read from or write to the cache.
|
|
155
|
+
|
|
156
|
+
## Shape the cache key with headers
|
|
157
|
+
|
|
158
|
+
The `headers` option adds request headers to the key. Use it when the backend
|
|
159
|
+
returns a genuinely different body for different values of that header.
|
|
160
|
+
|
|
161
|
+
**`Accept`** — an endpoint that serves both `application/json` and `text/csv`
|
|
162
|
+
needs `"headers": ["Accept"]`. Without it, the first caller to ask for CSV
|
|
163
|
+
poisons the entry for every JSON caller for the rest of the TTL. Two media types
|
|
164
|
+
double the number of entries, which costs nothing.
|
|
165
|
+
|
|
166
|
+
**`Accept-Language`** — a catalog that localizes product names needs
|
|
167
|
+
`"headers": ["Accept-Language"]`. With five supported locales and 500 distinct
|
|
168
|
+
product URLs the cache holds 2,500 entries instead of 500, and the hit rate
|
|
169
|
+
within each locale is unchanged, because callers sharing a locale share an
|
|
170
|
+
entry.
|
|
171
|
+
|
|
172
|
+
Every header you add multiplies the key space by the number of distinct values
|
|
173
|
+
it carries in real traffic, which is why `User-Agent` is the classic mistake:
|
|
174
|
+
50,000 daily callers present tens of thousands of distinct strings, so nearly
|
|
175
|
+
every entry is written once and read never. The hit rate collapses to near zero
|
|
176
|
+
and the cache becomes pure overhead. To vary by device, normalize the variation
|
|
177
|
+
into a handful of values in an earlier policy and key on that instead.
|
|
178
|
+
|
|
179
|
+
## Cache authenticated responses
|
|
180
|
+
|
|
181
|
+
`dangerouslyIgnoreAuthorizationHeader` defaults to `false`, which means the
|
|
182
|
+
`Authorization` header is part of the cache key. Each bearer token or API key
|
|
183
|
+
gets its own entry, so one caller can never be served another caller's response.
|
|
184
|
+
Keep this default for anything user-specific.
|
|
185
|
+
|
|
186
|
+
Setting it to `true` removes `Authorization` from the key, and every authorized
|
|
187
|
+
caller shares one entry. That is legitimate in exactly one situation: the
|
|
188
|
+
response body is byte-identical for every caller who is allowed through — the
|
|
189
|
+
credential gates access but does not change the data. A shared reference table,
|
|
190
|
+
a public price list behind an API key, and a service status document all
|
|
191
|
+
qualify, and all benefit, because a single entry now absorbs traffic from every
|
|
192
|
+
consumer instead of one entry per token.
|
|
193
|
+
|
|
194
|
+
:::danger
|
|
195
|
+
|
|
196
|
+
If the response varies by caller in any way — a tenant ID in the payload, a
|
|
197
|
+
filtered result set, a personalized field — setting
|
|
198
|
+
`dangerouslyIgnoreAuthorizationHeader` to `true` serves the first caller's data
|
|
199
|
+
to everyone else on that route until the TTL expires. Nothing errors and nothing
|
|
200
|
+
logs; the disclosure is silent. Verify that two different authorized callers
|
|
201
|
+
receive identical bytes before you enable it.
|
|
202
|
+
|
|
203
|
+
:::
|
|
204
|
+
|
|
205
|
+
## Invalidate with cacheId
|
|
206
|
+
|
|
207
|
+
`cacheId` is an arbitrary string folded into the cache key. Change the string
|
|
208
|
+
and every previously stored key becomes unreachable at once. Drive it from an
|
|
209
|
+
environment variable so you can change it without editing code:
|
|
210
|
+
|
|
211
|
+
```json title="config/policies.json"
|
|
212
|
+
{
|
|
213
|
+
"options": {
|
|
214
|
+
"expirationSecondsTtl": 300,
|
|
215
|
+
"cacheId": "$env(CACHE_ID)"
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Set `CACHE_ID` to a timestamp such as `2026-08-03-14-05` in your
|
|
221
|
+
[environment variables](../articles/environment-variables.mdx). To flush, update
|
|
222
|
+
the variable to a new value and redeploy. Every key the policy computes now
|
|
223
|
+
carries the new string, so no request matches an old entry. The orphaned entries
|
|
224
|
+
stay in storage until their TTL expires, but nothing serves them. A deployment
|
|
225
|
+
on its own flushes nothing — only a change to the value does. Give each policy
|
|
226
|
+
that needs independent invalidation its own variable; policies that share a
|
|
227
|
+
variable flush together.
|
|
228
|
+
|
|
229
|
+
:::warning
|
|
230
|
+
|
|
231
|
+
This is the entire invalidation story for the gateway cache. No purge API
|
|
232
|
+
exists, and no way to evict a single key. Plan around it: pick a TTL you are
|
|
233
|
+
willing to serve stale data for, and treat `cacheId` as the emergency lever
|
|
234
|
+
rather than part of a normal write path.
|
|
235
|
+
|
|
236
|
+
:::
|
|
237
|
+
|
|
238
|
+
Clients can also bust their own reads by adding a version or timestamp query
|
|
239
|
+
parameter. Query parameters are part of the key by default, so a new value
|
|
240
|
+
misses and fetches fresh data.
|
|
241
|
+
|
|
242
|
+
## Choose a TTL
|
|
243
|
+
|
|
244
|
+
`expirationSecondsTtl` defaults to 60 seconds. Pick a value from how fast the
|
|
245
|
+
underlying data actually changes, not from how fast you would like responses to
|
|
246
|
+
be — the TTL is the maximum staleness a caller can observe.
|
|
247
|
+
|
|
248
|
+
| Data volatility | TTL | Example endpoints |
|
|
249
|
+
| ------------------- | ------------- | ----------------------------------------------------------------------- |
|
|
250
|
+
| Static | 3600 s and up | `/v1/countries`, `/v1/currencies`, `/v1/plans`, published documentation |
|
|
251
|
+
| Semi-dynamic | 300–3600 s | `/v1/products/{id}`, `/v1/venues`, `/v1/categories`, marketing content |
|
|
252
|
+
| Frequently changing | 60–300 s | `/v1/search?q=`, `/v1/showtimes`, `/v1/inventory`, dashboards |
|
|
253
|
+
|
|
254
|
+
Anything that must reflect a write immediately — carts, seat holds, balances,
|
|
255
|
+
order status — does not belong in a response cache at any TTL. Start at the
|
|
256
|
+
short end of the range and lengthen it once you have measured the hit rate. A
|
|
257
|
+
TTL far below the interval between requests to a given key produces almost no
|
|
258
|
+
hits while still paying the write cost on every response.
|
|
259
|
+
|
|
260
|
+
## Limitations
|
|
261
|
+
|
|
262
|
+
**Cache entries are local to a zone.** Each Zuplo data center keeps its own
|
|
263
|
+
copy, so the first request for a key in a given region is always a miss even
|
|
264
|
+
when the same key is warm elsewhere. Expect a lower hit rate on globally
|
|
265
|
+
distributed traffic than a single-region test suggests.
|
|
266
|
+
|
|
267
|
+
**There is no selective purge.** Entries expire on their TTL or become
|
|
268
|
+
unreachable when `cacheId` changes. No API evicts a single key.
|
|
269
|
+
|
|
270
|
+
**Streaming responses are not cached**, and very large bodies are a poor fit —
|
|
271
|
+
the gateway has to hold the whole response to store it.
|
|
272
|
+
|
|
273
|
+
**A hit skips the rest of the pipeline.** Outbound policies do not run when the
|
|
274
|
+
caching policy answers from storage, so an outbound policy cannot be the sole
|
|
275
|
+
author of a header that must appear on every response.
|
|
276
|
+
|
|
277
|
+
:::caution{title="Do not combine with cdn-cache-control-outbound"}
|
|
278
|
+
|
|
279
|
+
Never put `caching-inbound` and
|
|
280
|
+
[`cdn-cache-control-outbound`](../policies/cdn-cache-control-outbound.mdx) on
|
|
281
|
+
the same route. `caching-inbound` answers a hit from the inbound pipeline, which
|
|
282
|
+
short-circuits the route before outbound policies run, so the CDN policy never
|
|
283
|
+
executes on a hit. The CDN then receives the copy that `caching-inbound` stored,
|
|
284
|
+
and `caching-inbound` sanitizes every copy it stores: it drops several CDN cache
|
|
285
|
+
headers (exactly which ones depends on the CDN) and overwrites `Cache-Control`
|
|
286
|
+
with its own `s-maxage` (its TTL, 60 seconds by default). The edge and client
|
|
287
|
+
TTLs collapse into one number. Misses run the full outbound stack and look
|
|
288
|
+
completely correct, so the defect appears only once the cache warms, and nothing
|
|
289
|
+
logs it. Pick one layer per route.
|
|
290
|
+
|
|
291
|
+
:::
|
|
292
|
+
|
|
293
|
+
## Specialized caches
|
|
294
|
+
|
|
295
|
+
Two policies key on payload shape rather than on bytes, and beat this policy on
|
|
296
|
+
the traffic they are built for:
|
|
297
|
+
|
|
298
|
+
- [GraphQL Cache](../policies/graphql-cache-inbound.mdx) normalizes a GraphQL
|
|
299
|
+
document and its variables before hashing, so two semantically identical
|
|
300
|
+
queries share an entry even when their bodies differ.
|
|
301
|
+
- [Semantic Cache](../policies/semantic-cache-inbound.mdx) matches requests by
|
|
302
|
+
meaning rather than exact text, which is what makes caching viable in front of
|
|
303
|
+
an LLM.
|
|
304
|
+
|
|
305
|
+
To cache a fragment of a response rather than all of it, or to drive the cache
|
|
306
|
+
from your own code, see
|
|
307
|
+
[Build a custom caching policy](./custom-caching-policy.mdx) and
|
|
308
|
+
[ZoneCache](../programmable-api/zone-cache.mdx).
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Caching in Zuplo
|
|
3
|
+
sidebar_label: Overview
|
|
4
|
+
description:
|
|
5
|
+
Compare the four places a response can be cached when Zuplo sits in front of
|
|
6
|
+
your API, what each layer costs on a miss, and how to pick the right one for a
|
|
7
|
+
given endpoint.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
A cached response is one your backend never serves. Every hit removes a database
|
|
11
|
+
query, a compute cycle, and an egress charge from your bill, and it returns to
|
|
12
|
+
the caller in a fraction of the time the real work takes.
|
|
13
|
+
|
|
14
|
+
The question is rarely _whether_ to cache, but _where_. The same response can be
|
|
15
|
+
cached in four different places, each with a different reach, a different
|
|
16
|
+
invalidation story, and a different price when it misses.
|
|
17
|
+
|
|
18
|
+
## The four layers
|
|
19
|
+
|
|
20
|
+
When Zuplo is in the request path, a response can be held at any of these
|
|
21
|
+
points. They run in order from the client inward, and each one only pays for
|
|
22
|
+
itself when the layer in front of it misses.
|
|
23
|
+
|
|
24
|
+
<Diagram height="h-80">
|
|
25
|
+
<DiagramNode id="client">Client</DiagramNode>
|
|
26
|
+
<DiagramNode id="cdn" variant="blue">
|
|
27
|
+
CDN edge cache
|
|
28
|
+
</DiagramNode>
|
|
29
|
+
<DiagramGroup id="zuplo" label="Zuplo gateway">
|
|
30
|
+
<DiagramNode id="gateway" variant="zuplo">
|
|
31
|
+
Response cache
|
|
32
|
+
</DiagramNode>
|
|
33
|
+
<DiagramNode id="programmable" variant="zuplo">
|
|
34
|
+
Programmable cache
|
|
35
|
+
</DiagramNode>
|
|
36
|
+
</DiagramGroup>
|
|
37
|
+
<DiagramNode id="origin" variant="green">
|
|
38
|
+
Backend cache
|
|
39
|
+
</DiagramNode>
|
|
40
|
+
<DiagramEdge from="client" to="cdn" label="Request" />
|
|
41
|
+
<DiagramEdge from="cdn" to="gateway" label="Miss" />
|
|
42
|
+
<DiagramEdge
|
|
43
|
+
from="gateway"
|
|
44
|
+
to="programmable"
|
|
45
|
+
fromSide="bottom"
|
|
46
|
+
toSide="top"
|
|
47
|
+
label="Miss"
|
|
48
|
+
/>
|
|
49
|
+
<DiagramEdge from="programmable" to="origin" label="Miss" />
|
|
50
|
+
</Diagram>
|
|
51
|
+
|
|
52
|
+
**1. The CDN edge cache** is closest to the client and furthest from the data.
|
|
53
|
+
It lives in a CDN you run in front of the gateway: Akamai, Fastly, Cloudflare,
|
|
54
|
+
or CloudFront. Zuplo doesn't operate this cache; the
|
|
55
|
+
[CDN Cache Control policy](../policies/cdn-cache-control-outbound.mdx) writes
|
|
56
|
+
the headers your CDN reads to decide how long to hold a response and which purge
|
|
57
|
+
tags it belongs to. A hit here never reaches your gateway at all, which makes it
|
|
58
|
+
the only layer that removes the request from your infrastructure entirely. A
|
|
59
|
+
miss costs a full trip to the gateway plus everything behind it.
|
|
60
|
+
|
|
61
|
+
**2. The gateway response cache** is the first thing inside Zuplo. The
|
|
62
|
+
[Caching policy](../policies/caching-inbound.mdx) stores whole responses keyed
|
|
63
|
+
by method, URL, query string, and (by default) the `Authorization` header, and
|
|
64
|
+
serves them from an inbound policy. A hit short-circuits the rest of the
|
|
65
|
+
pipeline: the handler, the backend, and every outbound policy. A miss costs the
|
|
66
|
+
full pipeline and a backend round trip.
|
|
67
|
+
|
|
68
|
+
**3. The programmable cache** lives inside your own code, in a handler or a
|
|
69
|
+
custom policy, through the [Cache API](../programmable-api/cache.mdx) or
|
|
70
|
+
[`ZoneCache`](../programmable-api/zone-cache.mdx). It's the only layer that can
|
|
71
|
+
cache something smaller than a response: a shared fragment of a payload, a
|
|
72
|
+
tenant lookup, a JWKS document. A miss costs only the work that one cached value
|
|
73
|
+
stood in for (one query, one fetch), not the whole request.
|
|
74
|
+
|
|
75
|
+
**4. The backend's own cache** is closest to the data and does nothing for the
|
|
76
|
+
network hops in front of it. By the time a request arrives, every layer above
|
|
77
|
+
has already paid its latency. A miss here costs the real work: the query, the
|
|
78
|
+
render, the computation. You build and run this one yourself; you configure the
|
|
79
|
+
three above it in Zuplo.
|
|
80
|
+
|
|
81
|
+
## Which layer to use
|
|
82
|
+
|
|
83
|
+
Find the row that matches the endpoint in front of you. The rows are ordered
|
|
84
|
+
roughly by how much a hit saves.
|
|
85
|
+
|
|
86
|
+
| Situation | Layer | Mechanism | Where to read |
|
|
87
|
+
| ---------------------------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
|
|
88
|
+
| A public catalog endpoint that returns the same body to everyone | CDN edge | `cdn-cache-control-outbound` with an `edge` and a `client` TTL | [Cache at the CDN](./cdn-caching.mdx) |
|
|
89
|
+
| An authenticated endpoint whose body is identical for every caller | CDN edge | `cdn-cache-control-outbound` with `client.visibility: "private"` so only the CDN stores it | [Cache at the CDN](./cdn-caching.mdx) |
|
|
90
|
+
| A mostly shared response with a small caller-specific slice | Programmable | Cache the shared fragment with the Cache API or `ZoneCache`, fetch only the dynamic part | [Cache part of a response](./partial-response-caching.mdx) |
|
|
91
|
+
| A response whose cacheability depends on what it contains (empty results, error bodies, flags) | CDN edge, per response | A `cacheConfig` function that returns TTLs and purge tags for the response in hand | [Per-response cache rules](./dynamic-cache-rules.mdx) |
|
|
92
|
+
| Caching rules no built-in policy expresses (a JWT-claim key, a size limit, an upstream TTL) | Programmable | A custom inbound and outbound policy pair built on the Cache API | [Build a custom caching policy](./custom-caching-policy.mdx) |
|
|
93
|
+
| Identical responses served to many callers, with no CDN in front of the gateway | Gateway | `caching-inbound` with an `expirationSecondsTtl` | [Cache at the gateway](./gateway-caching.mdx) |
|
|
94
|
+
| A GraphQL query repeated across callers, where the cache key is the query and its variables | Gateway | `graphql-cache-inbound` | [GraphQL Cache policy](../policies/graphql-cache-inbound.mdx) |
|
|
95
|
+
| An LLM prompt worded differently each time but asking the same question | Gateway | `semantic-cache-inbound`, which matches on prompt similarity rather than an exact key | [Semantic Cache policy](../policies/semantic-cache-inbound.mdx) |
|
|
96
|
+
|
|
97
|
+
The intuitive answer is wrong in these cases:
|
|
98
|
+
|
|
99
|
+
- **An authenticated response doesn't have to skip the edge.** If the body is
|
|
100
|
+
the same for every caller and the `Authorization` header only decides
|
|
101
|
+
_whether_ the caller may have it, the CDN can hold one copy while browsers
|
|
102
|
+
hold none. A single `Cache-Control` header can't express that; a targeted edge
|
|
103
|
+
header can.
|
|
104
|
+
- **A cache that only holds whole responses can't help a payload split 90%
|
|
105
|
+
shared, 10% personal.** Caching the shared 90% and assembling the rest per
|
|
106
|
+
request turns a full-size backend call into a small one. That's the pattern
|
|
107
|
+
with the largest cost delta on most APIs, and it belongs in code.
|
|
108
|
+
|
|
109
|
+
## Combining layers
|
|
110
|
+
|
|
111
|
+
**Put the CDN edge in front of the programmable cache.** This is the combination
|
|
112
|
+
worth reaching for first. The CDN absorbs the requests that are identical across
|
|
113
|
+
callers, and the programmable cache makes the requests that get through cheap.
|
|
114
|
+
The handler assembles a response from a cached shared fragment plus one small
|
|
115
|
+
backend call instead of one full-size call per request. The two layers touch
|
|
116
|
+
nothing in common: one sets response headers on the way out, the other reads and
|
|
117
|
+
writes keys inside your handler. A purge at the edge doesn't disturb the
|
|
118
|
+
programmable cache, and vice versa.
|
|
119
|
+
|
|
120
|
+
Stacking the gateway response cache with a programmable cache inside your
|
|
121
|
+
handler is also safe. A `caching-inbound` hit skips the handler, so the
|
|
122
|
+
programmable cache simply becomes the miss path.
|
|
123
|
+
|
|
124
|
+
:::caution{title="Never stack caching-inbound and cdn-cache-control-outbound"}
|
|
125
|
+
|
|
126
|
+
Pick one. A gateway cache hit is served from an inbound policy, and an inbound
|
|
127
|
+
policy that returns a response short-circuits the pipeline before any outbound
|
|
128
|
+
policy runs, so `cdn-cache-control-outbound` never executes on a hit. Your CDN
|
|
129
|
+
then receives the copy that `caching-inbound` stored, and `caching-inbound`
|
|
130
|
+
sanitizes every copy it stores: it drops several CDN cache headers (exactly
|
|
131
|
+
which ones depends on the CDN) and overwrites `Cache-Control` with its own
|
|
132
|
+
`s-maxage` (its TTL, 60 seconds by default). The edge and client TTLs collapse
|
|
133
|
+
into one number.
|
|
134
|
+
|
|
135
|
+
The failure is invisible on cache misses. Misses run the full outbound stack and
|
|
136
|
+
produce exactly the headers you configured, so the route looks correct in
|
|
137
|
+
testing and starts sending the wrong cache policy to your CDN only once the
|
|
138
|
+
gateway cache warms up. Nothing logs it.
|
|
139
|
+
|
|
140
|
+
:::
|
|
141
|
+
|
|
142
|
+
## Where to go next
|
|
143
|
+
|
|
144
|
+
The table above links a guide for every layer you configure in Zuplo. For the
|
|
145
|
+
runtime reference behind the programmable cache, see
|
|
146
|
+
[`ZoneCache`](../programmable-api/zone-cache.mdx) for zone-local key-value
|
|
147
|
+
storage and the [Cache API](../programmable-api/cache.mdx) for whole
|
|
148
|
+
`Request`/`Response` pairs. Those two also cover the case the table leaves out:
|
|
149
|
+
expensive reference data such as configuration, tenant metadata, or a JWKS
|
|
150
|
+
document looked up on every request.
|