How caching works
The edge stores a file the first time a viewer asks for it and serves every later request from that copy until it expires, is purged, or is evicted to make room on a full cache. What is stored, and for how long, is decided by your origin's headers and the zone's Cache Expiration Time, in a fixed order.
What is stored#
Only a successful answer to a GET or HEAD: a 200, or a 206 from an origin answering the byte range we asked for. An error, a 404 or a redirect (unless Follow Redirects is on, on a standard origin) is relayed to the viewer as it came and never stored, so a file that was missing for a minute is fetched again on the next request rather than staying missing. The one exception is a private bucket, whose 401 or 403 becomes our 502 page and whose other errors keep their status with a one-line body: see Private bucket origins. Files up to 10 MB are stored whole; larger ones are stored in 5 MB pieces as they are watched, up to 20 GB, and a file beyond that is passed through. Those sizes are binary (10 MB is 10,485,760 bytes); the statistics and the invoice use decimal units. See Video and large files.
How long a copy lives#
The edge decides when it stores a file, and again each time it refreshes an expired copy, from the zone's setting and your origin's headers at that moment. The first rule that applies wins.
- A response with
Cache-Control: no-storeorCache-Control: privateis never stored, whatever the zone is set to. - A response with a
Varyheader naming anything other thanAccept-Encodingis never stored: we cannot keep one copy per value of a header we do not key on.Vary: Cookieis the one exception, allowed when Strip Response Cookies is on, because the cookie never reaches your origin. Only the firstVaryheader on a response is read, so send one rather than several. - A response that sets a cookie is never stored, unless Strip Response Cookies is on and the cookie is removed.
- Cache Expiration Time set to Do Not Cache: nothing on the zone is stored.
- Cache Expiration Time set to a fixed time: the copy lives that long, overriding
max-age,s-maxage,Expiresandno-cachefrom your origin, playlists included. On a full siteno-cachestill wins and the page is not stored. - Cache Expiration Time set to Respect Origin Cache-Control, which is the default:
no-cache: not stored.s-maxageif present, otherwisemax-age: the copy lives that many seconds. Zero means not stored.- Otherwise
Expires: until that time, measured against our clock. A date in the past means not stored. - Otherwise the defaults in the table below, chosen by the file's type. On a full site the defaults apply only when the path names a static file; anything else is fetched every time.
Two things sit on top of that. Under Respect Origin Cache-Control, an HLS or DASH manifest (.m3u8, .mpd) whose lifetime came from Expires or the defaults is capped at 10 seconds, so a playlist that changes is picked up quickly; give it a max-age to set your own figure, and note that a fixed Cache Expiration Time applies to playlists with no cap. And a lifetime is fixed at the moment of storing: changing Cache Expiration Time afterwards does not shorten or lengthen the copies already in the cache, so purge the zone when you need the new value at once.
| When the origin says nothing | The copy lives |
|---|---|
| HLS and DASH manifests | 10 seconds |
| JSON under an API-shaped path (/api/, /v1/, /v2/, /v3/, /graphql) | 5 minutes |
| Video, audio and downloads (archives, installers, PDFs, binaries) | 7 days |
| Images | 24 hours |
| Stylesheets, scripts, fonts and other web assets | 1 hour |
| A file of unknown type of 10 MB or more | 7 days |
| Anything else | 1 hour |
Directives that make no difference: public, must-revalidate, proxy-revalidate, immutable, stale-while-revalidate, stale-if-error and Pragma. An Age header from your origin is not subtracted. Only the first Cache-Control header on a response is read, and a viewer's own Cache-Control or Pragma is ignored, so a browser's hard refresh is served from the cache like any other request.
What a viewer's request can do#
- A request carrying an
Authorizationheader bypasses the cache and goes to your origin, and its answer is not stored. - A request carrying a
Cookiedoes the same on a standard or bucket zone with Strip Response Cookies off. With it on, the cookie is dropped and the request is served from the cache like any other. On a full site the rule is by file type instead: see Full site origins. - A request with
If-None-MatchorIf-Modified-Sincegets a304when the cached copy matches. A file that is not cached yet is answered in full. - A request with a
Rangeheader is served that range from the copy, fetching only the pieces it needs. - A WebSocket handshake is passed to your origin and the connection relayed both ways, and an answer typed
text/event-streamis passed on as it arrives; neither is ever cached. See Server-sent events and WebSockets. - A
POST,PUT,PATCH,DELETEorOPTIONSis passed to your origin with its body, of any size, and never cached; a POST is refused with 403 instead while Block POST Requests is on, and a bucket origin refuses all five with 405 (see Private bucket origins). Any other method is answered 405 on every zone.
What makes two requests the same file#
A copy is filed under the zone, the path and the whole query string. The path is case-sensitive and read after percent-decoding, so /A.mp4 and /a.mp4 are two files and /my%20film.mp4 and /my film.mp4 are one. The query string is taken exactly as written, so /intro.mp4?v=2 is a different file from /intro.mp4 and from /intro.mp4?v=3, and ?a=1&b=2 is different from ?b=2&a=1. That is what makes a version parameter work as a cache buster, and it is also why a parameter that changes per visitor defeats the cache. The hostname joins the key only when Forward Host Header is on; otherwise every hostname on the zone shares one copy. HTTP and HTTPS share it too.
Compression#
The edge stores what your origin sends and never compresses or decompresses anything. A request's Accept-Encoding is sorted into one of four buckets (plain, gzip, brotli, and brotli-only for a client that refuses gzip), each bucket holds its own copy, and each is fetched from your origin with a matching Accept-Encoding. So compress at the origin: a server that sends text as gzip or brotli fills the buckets, and a client that cannot decode gzip is never handed a gzip body.
Serving stale while a refresh runs#
When a copy has expired it is not thrown away. For up to 24 hours past its expiry it is served straight away, marked HIT, while the edge asks your origin in the background whether the file has changed, with If-None-Match or If-Modified-Since where your origin gave it an ETag or a Last-Modified. A 304 renews the copy. A 200 replaces a file stored whole; a file stored in pieces is dropped instead when the refresh shows it changed, or when it has neither an ETag nor a Last-Modified to check with, and the next request fetches it afresh. If your origin cannot be reached, the stale copy keeps serving until the 24 hours are up, so a slow or absent origin is not seen by viewers of a file the edge holds; a piece of a large file the edge never fetched still needs your origin. If your origin answers the refresh with an error, the copy is dropped and the next request fetches afresh. Past 24 hours the copy is discarded and the next request is a miss. Replacing a file at your origin changes nothing at the edge until then: either purge it, or give the new version a new name or query string.
What a hit sends back#
A response served from the cache carries a fixed set of your origin's headers: Content-Type, Content-Encoding, Content-Language, Content-Disposition, Cache-Control, Expires, ETag, Last-Modified, Vary, the Access-Control-* headers, Timing-Allow-Origin, Cross-Origin-Resource-Policy, Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Link and X-Robots-Tag, the first value of each, plus an Age saying how long ago the copy was stored; Link is replaced by the canonical one while Add Canonical Headers is on. A header your origin sends that is not on that list is seen on the request that fetched a file of 10 MB or less and not on the hits that follow, and never on a larger file, whose every response is built from the stored set; Alt-Svc, Age and Server from your origin are never relayed. So a custom header your application relies on belongs on a page that is not cached, not on a cached file. Headers has the full list of what we add and what your origin receives.
Reading what happened#
Every response carries X-CDN-Cache: HIT was served from the copy, fresh or being refreshed; MISS was fetched from your origin on this request, or could not be stored (the edge's own error pages read MISS too); BYPASS was passed straight through because of the request itself, a cookie, an Authorization header, a method other than GET and HEAD, or a WebSocket. A file that reads MISS on every request is being refused by one of the rules above, and your origin's Cache-Control is the first thing to check.