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,397 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Cache at the CDN
|
|
3
|
+
description:
|
|
4
|
+
Put a CDN in front of your Zuplo gateway and drive its cache from response
|
|
5
|
+
headers, with separate edge and client TTLs, per-entity purge tags, and stale
|
|
6
|
+
responses through a backend outage.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
A CDN in front of your gateway serves the cheapest request your API will ever
|
|
10
|
+
handle: it never reaches Zuplo, never reaches your backend, and returns from a
|
|
11
|
+
POP near the caller. The hard part is telling the CDN which responses it may
|
|
12
|
+
hold, and telling it something different from what you tell the browser.
|
|
13
|
+
`Cache-Control` serves two audiences, and only one of them can be purged.
|
|
14
|
+
|
|
15
|
+
The gateway is the right place to make that call, because only the gateway sees
|
|
16
|
+
whether _this_ response has an empty result set, a `Set-Cookie`, or an upstream
|
|
17
|
+
`no-store`. The CDN Cache Control policy puts the decision there, so making an
|
|
18
|
+
endpoint cacheable is an edit to `config/policies.json` and a deploy rather than
|
|
19
|
+
a rule per path in Akamai Property Manager or the Cloudflare dashboard.
|
|
20
|
+
|
|
21
|
+
<Diagram type="sequence" height="h-[520px]">
|
|
22
|
+
<DiagramActor id="client" variant="yellow">
|
|
23
|
+
Client
|
|
24
|
+
</DiagramActor>
|
|
25
|
+
<DiagramActor id="cdn" variant="blue">
|
|
26
|
+
CDN
|
|
27
|
+
</DiagramActor>
|
|
28
|
+
<DiagramActor id="gateway" variant="zuplo">
|
|
29
|
+
Zuplo
|
|
30
|
+
</DiagramActor>
|
|
31
|
+
<DiagramActor id="backend" variant="green">
|
|
32
|
+
Backend
|
|
33
|
+
</DiagramActor>
|
|
34
|
+
<DiagramMessage from="client" to="cdn">
|
|
35
|
+
GET /v1/catalog/products
|
|
36
|
+
</DiagramMessage>
|
|
37
|
+
<DiagramMessage from="cdn" to="gateway">
|
|
38
|
+
Forward to origin
|
|
39
|
+
</DiagramMessage>
|
|
40
|
+
<DiagramMessage from="gateway" to="backend">
|
|
41
|
+
Fetch catalog
|
|
42
|
+
</DiagramMessage>
|
|
43
|
+
<DiagramMessage from="backend" to="gateway">
|
|
44
|
+
200 OK
|
|
45
|
+
</DiagramMessage>
|
|
46
|
+
<DiagramMessage from="gateway" to="cdn">
|
|
47
|
+
200 + Edge-Control + tag
|
|
48
|
+
</DiagramMessage>
|
|
49
|
+
<DiagramMessage from="cdn" to="client">
|
|
50
|
+
200 (stored at the edge)
|
|
51
|
+
</DiagramMessage>
|
|
52
|
+
<DiagramMessage from="client" to="cdn">
|
|
53
|
+
Same request again
|
|
54
|
+
</DiagramMessage>
|
|
55
|
+
<DiagramMessage from="cdn" to="client">
|
|
56
|
+
200, origin never runs
|
|
57
|
+
</DiagramMessage>
|
|
58
|
+
</Diagram>
|
|
59
|
+
|
|
60
|
+
## Add the policy
|
|
61
|
+
|
|
62
|
+
Add the policy to the `policies` array in `config/policies.json`. This example
|
|
63
|
+
caches at the edge for 30 minutes and in the client for 60 seconds.
|
|
64
|
+
|
|
65
|
+
```json title="config/policies.json"
|
|
66
|
+
{
|
|
67
|
+
"policies": [
|
|
68
|
+
{
|
|
69
|
+
"name": "cdn-cache-control",
|
|
70
|
+
"policyType": "cdn-cache-control-outbound",
|
|
71
|
+
"handler": {
|
|
72
|
+
"export": "CdnCacheControlOutboundPolicy",
|
|
73
|
+
"module": "$import(@zuplo/runtime)",
|
|
74
|
+
"options": {
|
|
75
|
+
"cdn": "akamai",
|
|
76
|
+
"edge": {
|
|
77
|
+
"maxAge": 1800
|
|
78
|
+
},
|
|
79
|
+
"client": {
|
|
80
|
+
"visibility": "public",
|
|
81
|
+
"maxAge": 60
|
|
82
|
+
},
|
|
83
|
+
"tags": ["catalog"]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This is an outbound policy, so attach it to the `policies.outbound` array of the
|
|
92
|
+
route in `config/routes.oas.json`:
|
|
93
|
+
|
|
94
|
+
```json title="config/routes.oas.json (excerpt)"
|
|
95
|
+
{
|
|
96
|
+
"x-zuplo-route": {
|
|
97
|
+
"policies": {
|
|
98
|
+
"outbound": ["cdn-cache-control"]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The `"cdn-cache-control"` string must match the `name` field in
|
|
105
|
+
`config/policies.json`. Attach the same policy to every route that shares a
|
|
106
|
+
cache profile, and define a second policy with a different `name` for routes
|
|
107
|
+
that need different TTLs or tags.
|
|
108
|
+
|
|
109
|
+
## Choose your CDN
|
|
110
|
+
|
|
111
|
+
`cdn` is required and has no default. Each CDN reads a different header, so a
|
|
112
|
+
default would silently produce a well-formed response with no edge caching at
|
|
113
|
+
all for anyone running a different one — a `curl` that looks correct and a hit
|
|
114
|
+
ratio of zero.
|
|
115
|
+
|
|
116
|
+
| `cdn` | Edge TTL header | Purge tag header | Tag separator |
|
|
117
|
+
| ------------ | ------------------------------ | ---------------- | ------------- |
|
|
118
|
+
| `akamai` | `Edge-Control` | `Edge-Cache-Tag` | comma |
|
|
119
|
+
| `fastly` | `Surrogate-Control` | `Surrogate-Key` | space |
|
|
120
|
+
| `cloudflare` | `Cloudflare-CDN-Cache-Control` | `Cache-Tag` | comma |
|
|
121
|
+
| `cloudfront` | `Cache-Control: s-maxage` | none | — |
|
|
122
|
+
| `generic` | `CDN-Cache-Control` (RFC 9213) | none | — |
|
|
123
|
+
|
|
124
|
+
Use `generic` for a CDN that implements RFC 9213 targeted cache control but is
|
|
125
|
+
not named above.
|
|
126
|
+
|
|
127
|
+
Naming the vendor also lets the policy reject configuration that vendor cannot
|
|
128
|
+
honor instead of dropping it silently: `tags` with `cloudfront` is a build-time
|
|
129
|
+
error pointing you at path invalidation, because CloudFront has no purge by tag.
|
|
130
|
+
|
|
131
|
+
The `strategy` option controls how the edge TTL travels. The default,
|
|
132
|
+
`targeted`, uses the CDN's own header. `s-maxage` folds the edge TTL into
|
|
133
|
+
`Cache-Control` instead, for a property that honors origin `Cache-Control` but
|
|
134
|
+
has not enabled the targeted header. CloudFront defaults to `s-maxage` because
|
|
135
|
+
it reads no targeted header, and `strategy: "targeted"` there is a build-time
|
|
136
|
+
error rather than a silent no-op.
|
|
137
|
+
|
|
138
|
+
Either way, the policy removes any targeted header your backend already set that
|
|
139
|
+
the configured CDN reads and that the policy is not writing itself. A targeted
|
|
140
|
+
header outranks `Cache-Control`, so an upstream `Surrogate-Control` with a large
|
|
141
|
+
`max-age` would otherwise beat the TTL configured here.
|
|
142
|
+
|
|
143
|
+
## Split the edge TTL from the client TTL
|
|
144
|
+
|
|
145
|
+
Take a catalog endpoint on a busy storefront. The product list changes a few
|
|
146
|
+
times a day and every caller sees the same body, so it gets a 30-minute edge TTL
|
|
147
|
+
and a 60-second client TTL — the configuration in
|
|
148
|
+
[Add the policy](#add-the-policy) above. On Akamai that emits:
|
|
149
|
+
|
|
150
|
+
```http
|
|
151
|
+
Cache-Control: public, max-age=60
|
|
152
|
+
Edge-Control: cache-maxage=30m
|
|
153
|
+
Edge-Cache-Tag: catalog
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
At steady state the edge serves nearly all of the traffic: one request per POP
|
|
157
|
+
per 30 minutes reaches Zuplo. Clients revalidate once a minute, which costs a
|
|
158
|
+
conditional request to the edge, not to your backend.
|
|
159
|
+
|
|
160
|
+
Now merchandising updates a product and your catalog service purges the
|
|
161
|
+
`catalog` tag. The two audiences diverge:
|
|
162
|
+
|
|
163
|
+
- **The edge** drops every response tagged `catalog` immediately. The next
|
|
164
|
+
request repopulates it from Zuplo with fresh data.
|
|
165
|
+
- **Clients** keep their copy until their own 60-second `max-age` expires. No
|
|
166
|
+
purge reaches a handset.
|
|
167
|
+
|
|
168
|
+
Sixty seconds of client staleness buys the round trips a client cache saves. Had
|
|
169
|
+
both audiences shared one 30-minute `max-age`, that window would be half an hour
|
|
170
|
+
and entirely outside your control. Keep the client TTL far shorter than the edge
|
|
171
|
+
TTL: the edge is what you can purge, and a client is not.
|
|
172
|
+
|
|
173
|
+
## Cache authenticated responses at the CDN only
|
|
174
|
+
|
|
175
|
+
The most valuable configuration is one a single `Cache-Control` cannot express
|
|
176
|
+
at all: the response is identical for every caller but requires an
|
|
177
|
+
`Authorization` header, so the CDN should absorb the load while no browser
|
|
178
|
+
stores the body.
|
|
179
|
+
|
|
180
|
+
```json title="config/policies.json (options excerpt)"
|
|
181
|
+
{
|
|
182
|
+
"cdn": "cloudflare",
|
|
183
|
+
"edge": { "maxAge": 300 },
|
|
184
|
+
"client": { "visibility": "private", "maxAge": 0 }
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```http
|
|
189
|
+
Cache-Control: private, max-age=0
|
|
190
|
+
Cloudflare-CDN-Cache-Control: max-age=300
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
This works because a CDN that honors a targeted cache header ignores
|
|
194
|
+
`Cache-Control` entirely for its own decision — it never sees the `private`. A
|
|
195
|
+
`private` in `Cache-Control` alone would have suppressed edge caching too, which
|
|
196
|
+
is why the targeted header matters here. CDN-only caching needs
|
|
197
|
+
`strategy: "targeted"`; `s-maxage` cannot express it.
|
|
198
|
+
|
|
199
|
+
Before enabling this on a route, confirm the response really is identical for
|
|
200
|
+
every authenticated caller, and that the edge cache key includes whatever
|
|
201
|
+
distinguishes them if it is not.
|
|
202
|
+
|
|
203
|
+
## Survive a backend outage with staleIfError
|
|
204
|
+
|
|
205
|
+
`edge.staleIfError` is often the highest-value directive on an API. It tells the
|
|
206
|
+
CDN how long it may keep serving the last good response while the gateway
|
|
207
|
+
returns errors, and a read endpoint that keeps working through a backend outage
|
|
208
|
+
beats one that returns 503.
|
|
209
|
+
|
|
210
|
+
```json title="config/policies.json (options excerpt)"
|
|
211
|
+
{
|
|
212
|
+
"cdn": "fastly",
|
|
213
|
+
"edge": {
|
|
214
|
+
"maxAge": 1800,
|
|
215
|
+
"staleWhileRevalidate": 600,
|
|
216
|
+
"staleIfError": 86400
|
|
217
|
+
},
|
|
218
|
+
"client": { "visibility": "public", "maxAge": 60 }
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```http
|
|
223
|
+
Cache-Control: public, max-age=60
|
|
224
|
+
Surrogate-Control: max-age=1800, stale-while-revalidate=600, stale-if-error=86400
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
A backend that fails at 3 a.m. no longer takes reads down with it: the edge
|
|
228
|
+
answers from its last good copy for up to a day while you fix the cause.
|
|
229
|
+
`staleWhileRevalidate` covers the smaller case — once the TTL expires the edge
|
|
230
|
+
serves the stale copy immediately and refreshes in the background, so no caller
|
|
231
|
+
waits for the refill.
|
|
232
|
+
|
|
233
|
+
:::caution{title="Akamai cannot express the stale windows"}
|
|
234
|
+
|
|
235
|
+
Akamai's `Edge-Control` header has no equivalent of `stale-while-revalidate` or
|
|
236
|
+
`stale-if-error`. The policy rejects that combination rather than dropping the
|
|
237
|
+
directives and letting you believe stale serving is configured. On Akamai,
|
|
238
|
+
either configure stale serving in Property Manager or set `strategy: "s-maxage"`
|
|
239
|
+
so the directives travel in `Cache-Control`. See
|
|
240
|
+
[Akamai CDN caching](../dedicated/akamai/caching.mdx) for the property setup.
|
|
241
|
+
|
|
242
|
+
:::
|
|
243
|
+
|
|
244
|
+
## Purge by tag
|
|
245
|
+
|
|
246
|
+
A tag labels a cached response so you can purge a slice of the edge cache later
|
|
247
|
+
— every response tagged `cities` after a city sync — without waiting out the TTL
|
|
248
|
+
or purging everything.
|
|
249
|
+
|
|
250
|
+
**Tag by entity, not just by route.** A `catalog` tag forces you to purge the
|
|
251
|
+
whole catalog when one product changes; adding `product-8891` lets the write
|
|
252
|
+
path purge exactly what it touched. Per-entity tags depend on the request, so
|
|
253
|
+
they come from a [per-response cache rule](./dynamic-cache-rules.mdx) rather
|
|
254
|
+
than the static `tags` array:
|
|
255
|
+
|
|
256
|
+
```ts title="modules/cdn-cache.ts"
|
|
257
|
+
return {
|
|
258
|
+
edge: { maxAge: 1800, staleIfError: 86400 },
|
|
259
|
+
client: { maxAge: 60, visibility: "public" },
|
|
260
|
+
tags: ["catalog", `product-${productId}`],
|
|
261
|
+
};
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Tags are validated against the target CDN's limits before they are emitted:
|
|
265
|
+
|
|
266
|
+
| CDN | Max tag length | Max tags | Max header size |
|
|
267
|
+
| ---------- | -------------- | -------- | --------------- |
|
|
268
|
+
| Akamai | 128 chars | 128 | 8192 bytes |
|
|
269
|
+
| Fastly | 1024 bytes | — | 16384 bytes |
|
|
270
|
+
| Cloudflare | 1024 chars | — | 16384 bytes |
|
|
271
|
+
|
|
272
|
+
Validation matters because the failure mode is silent. Fastly ignores the key it
|
|
273
|
+
is parsing _and every key after it_ once a limit is hit, so one over-long tag
|
|
274
|
+
makes later tags quietly unpurgeable. Akamai additionally rejects spaces,
|
|
275
|
+
commas, colons and brackets. Static tags in `config/policies.json` fail as a
|
|
276
|
+
configuration error you fix before deploying; tags returned from a function are
|
|
277
|
+
dropped with a warning instead, because a 200 should not become a 500 over a
|
|
278
|
+
purge tag.
|
|
279
|
+
|
|
280
|
+
### Calling the purge
|
|
281
|
+
|
|
282
|
+
The policy **emits** tags. It does not purge. Purging is an API call your own
|
|
283
|
+
tooling makes when the underlying data changes. On Fastly that is one request
|
|
284
|
+
per surrogate key:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
curl -X POST \
|
|
288
|
+
-H "Fastly-Key: $FASTLY_API_TOKEN" \
|
|
289
|
+
"https://api.fastly.com/service/$FASTLY_SERVICE_ID/purge/product-8891"
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Akamai's equivalent is a Fast Purge call to `/ccu/v3/invalidate/tag/production`
|
|
293
|
+
with the tags in the body, signed with EdgeGrid credentials; Cloudflare takes a
|
|
294
|
+
`tags` array on its purge endpoint. Wire whichever one you use into the service
|
|
295
|
+
that writes the data, so a product update purges `product-8891` as part of
|
|
296
|
+
saving it.
|
|
297
|
+
|
|
298
|
+
## Safety defaults
|
|
299
|
+
|
|
300
|
+
Three defaults keep a broadly applied cache policy from causing an incident.
|
|
301
|
+
|
|
302
|
+
**`client.mode` defaults to `strictest`.** When the upstream already sent a
|
|
303
|
+
`Cache-Control`, the policy emits the more conservative of the two: the lower
|
|
304
|
+
`max-age` and the union of the restrictive directives. Loosening a cache
|
|
305
|
+
directive is a data-disclosure risk, not only a performance one — if your
|
|
306
|
+
application returns `Cache-Control: private` for a per-user response and the
|
|
307
|
+
policy widened it to `public`, a shared cache could serve one user's data to the
|
|
308
|
+
next. Use `replace` when the gateway should be the authority on client caching,
|
|
309
|
+
or `preserve` to leave the upstream header alone and add only the CDN headers.
|
|
310
|
+
|
|
311
|
+
**`respectUpstream` defaults to `true`.** An upstream `Cache-Control` of
|
|
312
|
+
`no-store`, `no-cache` or `private`, or a `Set-Cookie` on the response,
|
|
313
|
+
suppresses every edge header and purge tag. Your application keeps the final say
|
|
314
|
+
on what is cacheable, and that holds even when the policy is attached broadly.
|
|
315
|
+
Setting it to `false` is the deliberate override; on Akamai that emits
|
|
316
|
+
`Edge-Control: !no-store`.
|
|
317
|
+
|
|
318
|
+
**`vary` is off by default.** Akamai skips caching any response carrying a
|
|
319
|
+
`Vary` header until Cache ID Modification is configured in Property Manager, so
|
|
320
|
+
enabling `vary` can disable edge caching entirely while every response still
|
|
321
|
+
looks correct. The policy logs a warning when `vary` is set with `cdn: akamai`.
|
|
322
|
+
When a response genuinely varies by a request header, configure the cache key at
|
|
323
|
+
the CDN — Cache ID Modification on Akamai, Cache Keys on Cloudflare — rather
|
|
324
|
+
than relying on `Vary`.
|
|
325
|
+
|
|
326
|
+
## Verify it works
|
|
327
|
+
|
|
328
|
+
Send a request to your Zuplo URL and inspect the response headers:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
curl -sI https://my-api.zuplo.app/v1/catalog/products
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
For the Akamai configuration above, expect:
|
|
335
|
+
|
|
336
|
+
```http
|
|
337
|
+
HTTP/2 200
|
|
338
|
+
cache-control: public, max-age=60
|
|
339
|
+
edge-control: cache-maxage=30m
|
|
340
|
+
edge-cache-tag: catalog
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
A missing `edge-control` usually means the response carried a `Set-Cookie` or an
|
|
344
|
+
upstream `no-store`: with `respectUpstream: true`, either one suppresses the
|
|
345
|
+
edge headers by design.
|
|
346
|
+
|
|
347
|
+
That `curl` proves the gateway emits the right headers. Whether the CDN acts on
|
|
348
|
+
them is CDN configuration:
|
|
349
|
+
|
|
350
|
+
- **Akamai** — the property's caching behavior must be set to honor origin
|
|
351
|
+
`Cache-Control`, and Cache Tag Visibility controls whether `Edge-Cache-Tag`
|
|
352
|
+
reaches end users. See [Akamai CDN caching](../dedicated/akamai/caching.mdx).
|
|
353
|
+
- **Cloudflare** — setting `Cloudflare-CDN-Cache-Control` turns Origin Cache
|
|
354
|
+
Control on regardless of the dashboard setting, but a Cache Rule still has to
|
|
355
|
+
make the path eligible for caching.
|
|
356
|
+
|
|
357
|
+
Confirm the offload at the edge rather than at the gateway: check the CDN's hit
|
|
358
|
+
ratio for the path, or send the same request twice and look for the second one
|
|
359
|
+
missing from your Zuplo logs.
|
|
360
|
+
|
|
361
|
+
## Don't stack this with gateway caching
|
|
362
|
+
|
|
363
|
+
:::caution{title="One cache per route"}
|
|
364
|
+
|
|
365
|
+
Do not use this policy and the [Caching](../policies/caching-inbound.mdx) policy
|
|
366
|
+
on the same route. They are alternative places to cache, not layers that stack,
|
|
367
|
+
and combining them gives you neither reliably.
|
|
368
|
+
|
|
369
|
+
The Caching policy serves a hit from an inbound policy, which short-circuits the
|
|
370
|
+
pipeline before outbound policies run, so this policy never executes on a
|
|
371
|
+
gateway cache hit. The CDN then receives the copy that the Caching policy
|
|
372
|
+
stored, and the Caching policy sanitizes every copy it stores: it drops several
|
|
373
|
+
CDN cache headers (exactly which ones depends on the CDN) and overwrites
|
|
374
|
+
`Cache-Control` with its own `s-maxage` (its TTL, 60 seconds by default). The
|
|
375
|
+
edge and client TTLs collapse into one number.
|
|
376
|
+
|
|
377
|
+
Cache misses behave correctly, which is what makes this dangerous. Misses run
|
|
378
|
+
the full outbound stack and produce exactly the headers you configured, so the
|
|
379
|
+
bug stays invisible while you test and appears only once traffic warms the
|
|
380
|
+
cache. Nothing logs it. Pick one: this policy on its own, or
|
|
381
|
+
[cache at the gateway](./gateway-caching.mdx) and let that policy own the
|
|
382
|
+
`Cache-Control` it sends.
|
|
383
|
+
|
|
384
|
+
:::
|
|
385
|
+
|
|
386
|
+
## Next steps
|
|
387
|
+
|
|
388
|
+
- [Per-response cache rules](./dynamic-cache-rules.mdx) — a `cacheConfig`
|
|
389
|
+
function for anything the static options cannot express: per-entity purge
|
|
390
|
+
tags, a TTL that depends on a response header, or skipping an empty result
|
|
391
|
+
set.
|
|
392
|
+
- [Akamai CDN caching](../dedicated/akamai/caching.mdx) — the Property Manager
|
|
393
|
+
side of the setup.
|
|
394
|
+
- [Cache at the gateway](./gateway-caching.mdx) — when no CDN sits in front of
|
|
395
|
+
Zuplo, or the cache needs to sit closer to the backend.
|
|
396
|
+
- [CDN Cache Control policy reference](../policies/cdn-cache-control-outbound.mdx)
|
|
397
|
+
— every configuration option in detail.
|