After this page you know what MDRN caches for your app on its own, how to have MDRN cache your app’s responses at the edge, how your code keeps responses in a cache of its own, how long each lasts, and what is never cached. Static files and requests your app makes need no setup; your app’s own responses are cached only when it asks.
What MDRN caches for you
| What | Cached | You set |
|---|---|---|
| Static files of your app (what your build puts in its static folder) | Yes, at our network’s edge, served from the location nearest each visitor | Nothing |
Files your app’s code fetches from other sites (fetch()) |
Yes, by the usual HTTP rules: a response the other site marks cacheable is kept and reused | Nothing; the other site’s Cache-Control decides |
| Responses your app’s code returns | Only when your app asks, or you turn on auto cache: see below | MDRN-Cache-Control, or Cache-Control with s-maxage |
Cache your app’s responses
MDRN keeps a response your app marks as cacheable at our network’s edge and gives it to the next visitor who asks the same address, without running your code. Mark it with one header:
export default {
async fetch(request) {
return new Response("<h1>Prices</h1>", {
headers: {
"Content-Type": "text/html",
"MDRN-Cache-Control": "max-age=60, stale-while-revalidate=300",
},
});
},
};
| Header on your response | What MDRN does |
|---|---|
MDRN-Cache-Control: max-age=<seconds> |
Keeps it that many seconds. MDRN removes this header before the browser sees it, and it wins over CDN-Cache-Control and Cache-Control. |
CDN-Cache-Control: max-age=<seconds> |
The same, when there is no MDRN-Cache-Control. Passed on to the browser. |
Cache-Control: s-maxage=<seconds> |
Keeps it that many seconds, when neither header above is there. Browsers ignore s-maxage. |
Cache-Control: public, max-age=<seconds> |
Keeps it that many seconds, and the browser caches it too. A max-age without public is for the browser only: MDRN does not keep the response. |
stale-while-revalidate=<seconds> beside any of them |
After its time is up, MDRN still gives the response out for this many seconds while it asks your app for a fresh one in the background, once. Without a number: up to 1 day. |
no-store, no-cache, must-revalidate |
no-store and no-cache keep nothing. must-revalidate keeps it, but never gives it out stale. |
| Limit | Value |
|---|---|
| Longest a response is kept as it is | 1 day (86,400 seconds), whatever you ask |
| Longest it is then given out stale | 1 day (86,400 seconds) |
| Largest response kept | 5 MiB; a larger one is served, not kept |
| Statuses kept | 200, 203, 204, 300, 301, 308, 404, 405, 410, 414, 501 |
| Auto cache, when you turn it on | 10 seconds, then 30 seconds stale, for an HTML 200 of up to 1 MiB, at an address without a query string, that sets no caching header of its own; one copy per first language the browser asks for |
| Responses one release keeps per minute, per server | 256; more are served, not kept (busy) |
Each location of our network keeps its own copy, so the first visitor in each location runs your code. A new release, a rollback or a change to variables starts with nothing cached. You cannot clear the cache by hand yet: deploy, or keep the time short.
Never cached
MDRN never serves a cached response to a request that could get someone’s own answer, and never keeps a response that could be someone’s own. Whatever your headers say, these are never cached:
- a request that is not
GETorHEAD, asks for part of a file (Range), or opens a WebSocket;- a request that carries any cookie or an
Authorizationheader, and every request of a signed-in visitor;- a request that carries any header a browser does not send by itself, such as an API key,
Origin,X-Forwarded-HostorX-HTTP-Method-Override;- MDRN’s own sign-in paths, your sign-in and account pages, and every page that needs a signed-in visitor;
- a deployment only its owner can open (while you build);
- a response that sets a cookie (
Set-Cookie), or saysprivateinCache-Control, even besideMDRN-Cache-Control;- a response whose
Varynames anything butAccept-Encodingand Next.js’s own router headers;- a response with a status not listed above, a body your app compressed itself, an empty
200withoutContent-Length: 0, or a body that does not match itsContent-Length.
A HEAD request is answered from a kept GET, but never fills the cache. A cached response is the same for every visitor: never mark a page that depends on who asks, such as their country, their address or their language, unless the address says it (/de/, ?lang=de). MDRN cannot tell that a page differs by country, and would give one visitor’s page to the next.
Identical requests that arrive together, for an address whose last response was kept, wait for one run of your code and all get its response, when that response may be kept. If it may not (it sets a cookie, say), each runs your code itself.
See what happened
Every response MDRN considered caching says what it did:
x-mdrn-cache |
Meaning |
|---|---|
HIT |
Served from the cache; your code did not run. Age says how old it is, in seconds. |
STALE |
Served from the cache after its time was up, while your app was asked for a fresh one. |
MISS |
Your code ran and the response is now kept. |
BYPASS |
Neither served from nor kept in the cache; x-mdrn-cache-reason says why. |
x-mdrn-cache-reason |
Why |
|---|---|
cookie, authorization, signed-in, sign-in, owner-only, header |
The request could get someone’s own answer. |
method, range, upgrade |
The request is not a plain GET or HEAD. |
set-cookie, private, vary |
The response could be someone’s own. |
no-store, no-cache, no-lifetime |
The response asked not to be kept, or gave no time. |
status, encoded, too-large |
The status, a body your app compressed, or a body over 5 MiB. |
busy |
The server is already keeping as many responses as it may; this one was served, not kept. |
A response your app did not mark, with auto cache off, gets no x-mdrn-cache header and works exactly as before.
Examples
A Hono route:
app.get("/blog/:slug", async (c) => {
const post = await loadPost(c.req.param("slug"));
c.header("MDRN-Cache-Control", "max-age=300, stale-while-revalidate=3600");
return c.html(render(post));
});
With the SDK, cache and noCache write the header for you (SDK):
import { cache } from "@mdrnio/sdk";
return cache(Response.json(prices), { maxAge: 60, staleWhileRevalidate: 300 });
Next.js needs nothing: a page with revalidate, or one built ahead of time, already answers Cache-Control: s-maxage=<seconds>, usually with stale-while-revalidate, which MDRN follows (Frameworks). A route handler sets its own:
export async function GET() {
return Response.json(await prices(), {
headers: { "Cache-Control": "s-maxage=60, stale-while-revalidate=300" },
});
}
To have pages that set no caching header cached for 10 seconds, turn on auto cache for the deployment, in its Configuration tab or from the CLI. Your own headers still win, no-store and private included.
mdrn deployments set <deployment_id> --auto-cache on
What a cached response costs
A response given out from the cache is one request on your Requests, with no CPU time: your code did not run (Billing). A refresh in the background runs your code, and counts as a request with its CPU time like any other.
Cache in your code
Your app has a cache of its own through the standard Cache API. Put a response under a GET request’s URL and match it on a later request:
export default {
async fetch(request, env, ctx) {
const cache = caches.default;
const hit = await cache.match(request);
if (hit) return hit;
const upstream = await fetch("https://<api-you-call>/<path>");
const response = new Response(upstream.body, upstream);
response.headers.set("Cache-Control", "public, max-age=300");
ctx.waitUntil(cache.put(request, response.clone()));
return response;
},
};
caches.default and caches.open("<name>") both work, and code written for either runs unchanged:
caches.defaultis your app’s cache nameddefault, the same onecaches.open("default")gives.caches.open("<name>")gives another cache of your app’s own; two names are two caches.- A response is kept for as long as its
Cache-Controlsays, and may be dropped sooner when the location needs the room. Treat a miss as normal. - Only a
GETrequest can be a key. A response markedno-store, or one that sets a cookie, is not kept. - In a Python app, use
caches.open("<name>"): therecaches.defaultrefuses every call.
How long your app’s cache lasts
| Rule | What it means |
|---|---|
| Private to the deployment and environment | No other app, deployment, or environment (Sandbox, Production) reads or writes it. |
| Kept per location | Each location of our network keeps its own copy: a response put in one location is a miss in another until that location puts it too. |
| Starts empty with every new release | A deploy, a rollback, or a change to variables starts a new release, and its cache starts empty. Nothing from the previous release is served after it. |
What does not work
| Option | On MDRN |
|---|---|
cf.cacheTtl on fetch() |
Ignored: the other site’s Cache-Control decides how long a fetched file is kept. To keep it longer, put it in your own cache (above). |
| Clearing cached responses by hand | Not done by MDRN yet: a new release starts with nothing cached. |
Request and time limits your code runs within are on Limits; every error code is on Errors.