HTTP caching semantics
One of the most common uses of the Fastly edge cache is to store HTTP resources, such as webpages, JavaScript, CSS, images, and video. The HTTP Caching standard (RFC 9111) describes how to store a response associated with a request and reuse the stored response for subsequent requests.
Fastly's readthrough cache interface interprets and processes the instructions encoded into HTTP responses. For example, the most common (and best practice) means of controlling cache lifetime is by setting an appropriate Cache-Control header on a backend response.
This page describes the amount of time that HTTP resources are cached, and how you can effectively control the caching behavior.
WARNING: The before-send and after-send callbacks discussed on this page are part of customized readthrough (HTTP) cache behavior. For this version of the Compute JavaScript and Go SDKs, this is an opt-in feature. This feature is not available in this version of the Compute C++ SDK. See this note for details.
Response processing
When a response is received from a backend, the readthrough cache interface parses relevant response headers to determine whether it can be cached, and for how long.
- CDN services
- Compute services
In a CDN service, response processing results can be inspected and overridden during the vcl_fetch subroutine, which is executed once the response has been parsed (unless the request is a revalidation).
In a Compute service, response processing results can be inspected and overridden during the after-send callback, which is executed once the response has been parsed (including when the request is a revalidation).
Parsing cache controls
HTTP responses are parsed for the following cache semantics:
| Property | Parsing logic | Default |
|---|---|---|
| Is response cacheable? | If the fetch is a result of an earlier explicit pass on the request, then no; otherwise if the fetch is a result of a hit-for-pass, then no; otherwise if HTTP status is 200, 203, 300, 301, 302, 404, or 410, then yes;otherwise no | N/A |
| Cache TTL | Response headers in order of preference:Surrogate-Control: max-age={n}, otherwiseCache-Control: s-maxage={n}, otherwiseCache-Control: max-age={n}, otherwiseExpires: {date} | 2 min |
| Stale-while-revalidate TTL | Response headers in order of preference:Surrogate-Control: stale-while-revalidate={n}, otherwiseCache-Control: stale-while-revalidate={n} | 0 |
| Stale-if-error TTL | Response headers in order of preference:Surrogate-Control: stale-if-error={n}, otherwiseCache-Control: stale-if-error={n} | 0 |
For example, an HTTP 200 (OK) response with no cache-freshness indicators in the response headers is cacheable and will have a TTL of 2 minutes. A 500 Internal Server Error response with Cache-Control: max-age=300 is not cacheable, because of its HTTP status code, and therefore the 5 minute TTL (300 seconds) indicated in the Cache-Control header is irrelevant.
- CDN services
- Compute services
In a CDN service, once the response has been parsed, the status code, headers received with the response, and cache controls resulting from parsing the response headers are available as VCL variables during vcl_fetch:
WARNING: stale-if-error is part of customized readthrough (HTTP) cache behavior. For this version of the Compute JavaScript and Go SDKs, this is an opt-in feature. This feature is not available in this version of the Compute C++ SDK. See this note for details.
In a Compute service, once the response has been parsed, the status code, headers received with the response, and cache controls resulting from parsing the response headers are available during the after-send callback.
- Rust
- JavaScript
- Go
- C++
The following methods are available on the CandidateResponse object passed into the after-send callback:
CandidateResponse::get_status(&self) -> StatusCodeCandidateResponse::get_header(&self, name: impl ToHeaderName) -> Option<&HeaderValue>CandidateResponse::is_cacheable(&self) -> boolCandidateResponse::get_ttl(&self) -> DurationCandidateResponse::get_stale_while_revalidate(&self) -> DurationCandidateResponse::get_stale_if_error(&self) -> DurationCandidateResponse::stale_if_error_available(&self) -> bool
The following properties can be read from the Response object passed into the after-send callback:
resp.status: numberresp.headers.get(header: string): string | undefinedresp.ttl: number | undefinedresp.staleWhileRevalidate: number | undefined(orresp.swr)resp.staleIfError: number | undefinedresp.staleIfErrorAvailable: bool
The following methods are available on the CandidateResponse object passed into the after-send callback:
func (cr *CandidateResponse) Status() (int, error)func (cr *CandidateResponse) Header(key string) (string, error)func (cr *CandidateResponse) TTL() (uint32, error)func (cr *CandidateResponse) StaleWhileRevalidate() (uint32, error)func (cr *CandidateResponse) StaleIfError() (uint32, error)func (cr *CandidateResponse) StaleIfErrorAvailable() bool
In this version of the Fastly Compute C++ SDK, the after-send callback is not available.
Age
A backend can set the Age HTTP response header to indicate that an object has already spent some time in a cache upstream before being served to Fastly. If the response includes an Age header with a positive value, that value will be subtracted from the response's max-age, if it has one. If the resulting TTL is negative, it is considered to be zero. If the TTL of a response is derived from an Expires header, any Age header also present on the response will not affect the TTL calculation.
Age does not affect the initial values of stale-while-revalidate or stale-if-error TTLs. If a response includes a Cache-Control: max-age=60, stale-while-revalidate=300 and also Age: 90, then the object's TTL will be set to 0 (because Age is higher than 60) but the separate stale-while-revalidate TTL will still be 300 seconds.
- CDN services
- Compute services
In a CDN service, it's possible to change or remove the Age header on the response during the vcl_fetch subroutine. However, this will not affect the TTL that the object will receive in the cache, as the TTL will have already been calculated by that point.
If you need to modify the TTL, see overriding semantics below.
In a Compute service, it's possible to change or remove the Age header on the response during the after-send callback. However, this will not affect the TTL that the object will receive in the cache, as the TTL will have already been calculated by that point.
If you need to modify the TTL, see overriding semantics below.
Fastly's readthrough cache interface also sets the Age header each time it returns a response. Each response receives a new value for the Age header, equal to the amount of time that the object has spent in the Fastly cache, plus (if set) the value of the Age header on the cached object. This mechanism is used to ensure that objects cached in multiple tiers of the Fastly platform as a result of shielding will not accrue more cache freshness than was originally intended.
- CDN services
- Compute services
In CDN services, the Age header is set in this way just before the response is delivered to the client.
In Compute services, the Age header is set in this way when the response is returned from the readthrough cache.
Surrogate control
The Surrogate-Control: max-age and Cache-Control: s-maxage header directives express a desired TTL for server-based caches (such as Fastly's readthrough cache). Therefore, these will be given preference over Cache-Control: max-age when calculating the initial value of the response object's TTL.
Additionally, Fastly will remove any Surrogate-Control header before a response is sent to an end user. Fastly does not, however, remove the s-maxage directive from any Cache-Control header.
IMPORTANT: If your service uses shielding, then the 'end user' making the request to the Fastly edge may be another Fastly POP. In this situation Fastly does not strip the Surrogate-Control header, so that both POPs will parse and respect the Surrogate-Control instructions.
Overriding semantics
- CDN services
- Compute services
During the vcl_fetch subroutine, you can affect the caching behavior in a number of ways:
Modifying Fastly cache TTL
To change the amount of time the readthrough cache interface will cache an object, override the value ofberesp.ttl,beresp.stale_while_revalidate, andberesp.stale_if_error:set beresp.ttl = 300s;HINT: This will override entirely the TTL that Fastly has determined by parsing the response's freshness semantics. If your service uses shielding, you may want to subtract
Agemanually. See theberesp.ttldocs for more information.Modifying downstream (browser) cache TTL
To change the way that downstream caches (including browsers) treat the resource, override the value of the caching headers attached to the object. Take care if you use shielding since you may also be changing the caching policy of a downstream Fastly cache:if (req.backend.is_origin) {set beresp.http.Cache-Control = "max-age=86400"; # Rules for browsersset beresp.http.Surrogate-Control = "max-age=31536000"; # Rules for downstream Fastly cachesunset beresp.http.Expires;}
The standard VCL boilerplate (which is also included in any Fastly CDN service that does not use custom VCL) applies some logic that affects freshness:
- If the response has a
Cache-Control: privateheader, execute areturn(pass). - If the response has a
Set-Cookieheader, execute areturn(pass). - If the response does not have any of
Cache-Control: max-age,Cache-Control: s-maxageorSurrogate-Control: max-ageheaders, setberesp.ttlto the fallback TTL configured for your Fastly service.
WARNING: If you are using custom VCL, the fallback TTL configured via the web interface or API will not be applied, and the fallback TTL will be as hard-coded into your VCL boilerplate (you're free to remove any of the default interventions, including the fallback TTL logic, if you wish)
WARNING: stale-if-error is part of customized readthrough (HTTP) cache behavior. For this version of the Compute JavaScript and Go SDKs, this is an opt-in feature. This feature is not available in this version of the Compute C++ SDK. See this note for details.
During the after-send callback, you can affect the caching behavior in a number of ways:
- Rust
- JavaScript
- Go
- C++
Modifying Fastly cache TTL
To change the amount of time the readthrough cache interface will cache an object, call the following methods onCandidateResponse:CandidateResponse::set_ttl(&mut self, ttl: Duration)CandidateResponse::set_stale_while_revalidate(&mut self, stale_while_revalidate: Duration)CandidateResponse::set_stale_if_error(&mut self, stale_if_error: Duration)
For example:
req.set_after_send(|resp| {resp.set_ttl(Duration::from_secs(300));Ok(())});Modifying downstream (browser) cache TTL
To change the way that downstream caches (including browsers) treat the resource, override the value of the caching headers attached to the object. Take care if you use shielding since you may also be changing the caching policy of a downstream Fastly cache:req.set_after_send(|resp| {resp.set_header("Cache-Control", "max-age=86400"); // Rules for browsersresp.set_header("Surrogate-Control", "max-age=31536000"); // Rules for downstream Fastly cachesresp.remove_header("Expires");Ok(())});
Modifying Fastly cache TTL
To change the amount of time the readthrough cache interface will cache an object, write to the following properties onResponse:resp.ttl: numberresp.staleWhileRevalidate: number(orresp.swr)resp.staleIfError: number
For example:
12345678const backendResp = await fetch(clientReq, {backend: 'example_backend',cacheOverride: new CacheOverride({afterSend(resp) {resp.ttl = 300;},}),});Modifying downstream (browser) cache TTL
To change the way that downstream caches (including browsers) treat the resource, override the value of the caching headers attached to the object. Take care if you use shielding since you may also be changing the caching policy of a downstream Fastly cache:12345678910const backendResp = await fetch(clientReq, {backend: 'example_backend',cacheOverride: new CacheOverride({afterSend(resp) {resp.headers.set('Cache-Control', 'max-age=86400'); // Rules for browsersresp.headers.set('Surrogate-Control', 'max-age=31536000'); // Rules for downstream Fastly cachesresp.headers.delete('expires');},}),});
Modifying Fastly cache TTL
To change the amount of time the readthrough cache interface will cache an object, call the following methods onCandidateResponse:func (cr *CandidateResponse) SetTTL(ttl uint32)func (cr *CandidateResponse) SetStaleWhileRevalidate(swr uint32)func (cr *CandidateResponse) SetStaleIfError(swr uint32)
For example:
r.CacheOptions.AfterSend = func(cr *fsthttp.CandidateResponse) error {cr.SetTTL(300)return nil}Modifying downstream (browser) cache TTL
To change the way that downstream caches (including browsers) treat the resource, override the value of the caching headers attached to the object. Take care if you use shielding since you may also be changing the caching policy of a downstream Fastly cache:r.CacheOptions.AfterSend = func(cr *fsthttp.CandidateResponse) error {cr.SetHeader("Cache-Control", "max-age=86400") // Rules for browserscr.SetHeader("Surrogate-Control", "max-age=31536000") // Rules for downstream Fastly cachescr.DelHeader("Expires")cr.SetTTL(4000)return nil}
In this version of the Fastly Compute C++ SDK, this behavior cannot be affected, as the after-send callback is not available.
Cache outcome
- CDN services
- Compute services
After parsing the response for freshness information and executing the vcl_fetch subroutine, the readthrough cache decides whether to save the object based on the following criteria, in this order of priority:
| Outcome | Trigger | Result | |
|---|---|---|---|
| 1 | Deliver stale | return(deliver_stale) is executed in vcl_fetch (see more about stale content for details). | An existing, stale object is served from the cache. The downloaded response is discarded, regardless of its cacheability or proposed TTL. No changes are made to the cache. |
| 2 | Deliver uncached | The content is deemed uncacheable or has a total TTL1 of zero. Fastly's cache deems a response uncacheable based on its HTTP status and other factors, following the HTTP Caching RFC. The default behavior of the readthrough cache also excludes responses that include a set-cookie header.This behavior can be overridden using beresp.cacheable. | The new response is served to the end user, and no record is made in the cache. Requests queued up due to request collapsing are dequeued and forwarded individually to the backend. |
| 3 | Cache and pass | return(pass) is executed in vcl_fetch. | The new response is served to the end user, and an empty hit-for-pass object is saved into the cache. This object exists to allow subsequent requests to proceed directly to a backend fetch without being queued by request collapsing. The hit-for-pass object is stored for the duration specified by its TTL, but subject to a minimum of 120 and a maximum of 3690 seconds. |
| 4 | Cache and deliver | All other cases (return(deliver) either explicitly or implicitly). | The new response is served to the end user, used to satisfy queued requests, and stored in cache for up to the duration specified by its TTL. |
After parsing the response for freshness information, the readthrough cache decides whether to save the object based on the following criteria, in this order of priority:
| Outcome | Trigger | Result | |
|---|---|---|---|
| 1 | Deliver uncached | The content is deemed uncacheable or has a total TTL2 of zero. Fastly's cache deems a response uncacheable based on its HTTP status and other factors, following the HTTP Caching RFC. The default behavior of the readthrough cache also excludes responses that include a set-cookie header. | The new response is served to the client. The readthrough cache interface uses heuristics to determine whether to record a hit-for-pass object in the cache: requests queued up due to request collapsing are dequeued and forwarded individually to the backend, and this object exists to allow subsequent requests to proceed directly to a backend fetch without being queued by request collapsing. This effectively disables request collapsing until a cacheable response is received. The hit-for-pass object is created in many cases, but for example excludes error statuses, so that the platform does not overload a failing backend. |
| 2 | Cache and deliver | All other cases | The new response is served to the client, used to satisfy queued requests, and stored in cache for up to the duration specified by its TTL. |
- Rust
- JavaScript
- Go
- C++
In a Compute application written in Rust, this behavior can be overridden during an after-send callback:
CandidateResponse::is_cacheable(&self) -> bool- return a value indicating whether the response would be stored into the cache.CandidateResponse::set_cacheable(&mut self)- force the response to be stored in the cache, even if its headers or status would normally prevent that.CandidateResponse::set_uncacheable(&mut self, record_uncacheable: bool)- set the response not to be stored in the cache.- If the
record_uncacheableparameter istrue, a hit-for-pass object is stored into the cache. Otherwise, no record is made in the cache.
- If the
In a Compute application written in JavaScript, this behavior can be overridden by returning a CacheOptions object from the after-send callback, setting a value for cache:
{ cache: true }- force the response to be stored in the cache, even if its headers or status would normally prevent that.{ cache: false }- set the response not to be stored in the cache.{ cache: 'uncacheable' }- set the response not to be stored in the cache, and instead store a hit-for-pass object into the cache.
In a Compute application written in Go, this behavior can be overridden during an after-send callback:
func (cr *CandidateResponse) SetCacheable()- force the response to be stored in the cache, even if its headers or status would normally prevent that.func (cr *CandidateResponse) SetUncacheable()- set the response not to be stored in the cache.func (cr *CandidateResponse) SetUncacheableDisableCollapsing()- set the response not to be stored in the cache, and instead store a hit-for-pass object into the cache.
In this version of the Fastly Compute C++ SDK, this behavior cannot be overridden.
IMPORTANT: Objects may not be stored for the full TTL requested, as they may get evicted earlier in favor of more popular objects, especially if they are large. Objects are not automatically evicted when they reach their TTL, they simply become stale.
If you are experiencing a slow request rate or timeouts on uncacheable resources, it may be because they are forming queues that can be solved by creating a hit-for-pass. For more details, see request collapsing.
Stale objects and revalidation
An object that has reached its TTL becomes stale. If an object is requested while it is stale, it may trigger a revalidation request to the backend. Learn more about staleness and revalidation.
Preventing content from being cached
Since Fastly respects HTTP caching semantics in the readthrough cache, the best way to avoid caching content is to set the appropriate Cache-Control header on responses at the backend.
Preventing caching at the edge and in browsers
Responding with the following header will ensure that the object will not be cached by Fastly (the private directive), and that it will not be cached by any other downstream cache, such as a browser (both private and no-store directives):
Cache-Control: private, no-storeCache at the edge, not in browsers
You may want the content to be cached by Fastly but not by browsers. You can do this purely in the initial HTTP response header from the backend:
Cache-Control: s-maxage=3600, max-age=0- CDN services
- Compute services
In a CDN service, you can apply an override in vcl_fetch:
set beresp.http.Cache-Control = "private, no-store"; # Don't cache in the browserset beresp.ttl = 3600s; # Cache in Fastlyset beresp.ttl -= std.atoi(beresp.http.Age);return(deliver);In a Compute service, you can apply an override in the after-send callback:
- Rust
- JavaScript
- Go
- C++
req.set_after_send(|resp| { resp.set_header("Cache-Control", "private, no-store"); // Don't cache in the browser resp.set_cacheable(); // Cache in Fastly resp.set_ttl(Duration::from_secs(3600) - resp.get_age()); Ok(())});123456789101112const backendResp = await fetch(clientReq, { backend: 'example_backend', cacheOverride: new CacheOverride({ afterSend(resp) { resp.headers.set('Cache-Control', 'private, no-store'); // Don't cache in the browser resp.ttl = 3600 - resp.age; return { cache: true, // Cache in Fastly }; }, }),});12345678910r.CacheOptions.AfterSend = func(cr *fsthttp.CandidateResponse) error { cr.SetHeader("Cache-Control", "private, no-store") // Don't cache in the browser cr.SetCacheable() // Cache in Fastly age, err := cr.Age() if err != nil { return fmt.Errorf("cr.Age(): %w", err) } cr.SetTTL(3600 - age) return nil}In this version of the Fastly Compute C++ SDK, this behavior cannot be overridden.
Cache in browsers, not at the edge
Fastly will not cache private content, making it a good way to apply this kind of differentiated caching policy via a single header attached to the response from your origin server:
Cache-Control: private, max-age=3600- CDN services
- Compute services
In a CDN service, you can also apply the same logic in vcl_fetch:
set beresp.http.Cache-Control = "max-age=3600"; # Cache in the browserreturn(pass); # Don't cache in FastlyIn a Compute service, you can apply an override in the after-send callback:
- Rust
- JavaScript
- Go
- C++
req.set_after_send(|resp| { resp.set_header("Cache-Control", "max-age=3600"); // Cache in the browser resp.set_uncacheable(false); // Don't cache in Fastly Ok(())});1234567891011const backendResp = await fetch(clientReq, { backend: 'example_backend', cacheOverride: new CacheOverride({ afterSend(resp) { resp.headers.set('Cache-Control', 'max-age=3600'); // Cache in the browser return { cache: false, // Don't cache in Fastly }; }, }),});r.CacheOptions.AfterSend = func(cr *fsthttp.CandidateResponse) error { cr.SetHeader("Cache-Control", "max-age=3600") // Cache in the browser cr.SetUncacheable() // Don't cache in Fastly return nil}In this version of the Fastly Compute C++ SDK, this behavior cannot be overridden.
Overriding cache behavior on requests
Sometimes you may know what cache behavior you'd like for the response before forwarding a request to the backend.
For details, see the following sections.
IMPORTANT: As noted in cache outcome above, where requests are flagged to bypass the readthrough cache or have an override TTL of 0, the response will never be cached.
Divergences from RFC 9111
The behavior of Fastly's readthrough cache is based on the HTTP Caching standard (RFC 9111), with some exceptions.
- CDN services
- Compute services
The existence of the
Authorizationheader in the request does not prevent a response from being cached.To prevent requests with an
Authorizationheader from receiving a cached response, add a snippet to thevcl_recvsubroutine:sub vcl_recv { ... }Fastly VCLif (req.http.Authorization) {return(pass);}
The existence of the
Authorizationheader in the request does not prevent a response from being cached.- Rust
- JavaScript
- Go
- C++
To prevent requests with an
Authorizationheader from receiving a cached response, call.set_pass(true)on the requests.if req.get_header("Authorization").is_some() {req.set_pass(true);}req.send("my_backend_name");Refer to the Rust SDK documentation for
set_passto understand the behavior of this function and its interaction with similarset_functions in the SDK.To prevent requests with an
Authorizationheader from receiving a cached response, construct aCacheOverrideobject with the value"pass"and include it in thefetch()call.import { CacheOverride } from "fastly:cache-override";const response = fetch(event.request, {backend: "my_backend_name",cacheOverride: event.request.headers.has("Authorization") ? new CacheOverride("pass") : undefined,});To prevent requests with an
Authorizationheader from receiving a cached response, set the value of thePassfield of theCacheOptionsfield on theRequestobject totrue.if r.Header.Get("Authorization") != "" {r.CacheOptions.Pass = true}resp, err := r.Send(ctx, "my_backend_name")To prevent requests with an
Authorizationheader from receiving a cached response, call.set_pass(true)on the requests.auto auth = req.get_header("Authorization");if (auth && *auth) {req.set_pass(true);}req.send("my_backend_name");HINT: Note that
req.get_header("Authorization")returnsexpected<optional<HeaderValue>>. Since bothexpectedandoptionaloverrideoperator bool, you can check for the presence of a header value using the conciseif (result && *result)syntax.Refer to the C++ documentation for
set_passto understand the behavior of this function and its interaction with similarset_functions in the SDK.When the
Surrogate-ControlandCache-Controlheaders are both present on a response, Compute targetsSurrogate-ControlbeforeCache-Controland honors the first targeted header that parses successfully. This may cause values ofSurrogate-Controlto take priority over more strict values ofCache-Controlon a shared-cache response.Cache-Control: must-revalidateandCache-Control: proxy-revalidateare ignored in Compute. Once a response is stale, it can still be reused during a stale-serving path such asstale-while-revalidate.Because Compute is designed to handle requests coming from untrusted end users, request-side HTTP attributes do not directly affect cache behavior. For example:
- Request-side cache controls, such as the
Cache-Controlrequest header do not affect whether the readthrough cache will store the response. Note, however, that any cache controls on the response do affect cache behavior. - Successful responses to "unsafe" HTTP methods (
PUT,DELETE, etc.) do not cause the corresponding cachedGETresponse to be invalidated.
- Request-side cache controls, such as the
Related content
- "Total TTL" is the sum of
beresp.ttland the greater ofberesp.stale_while_revalidateandberesp.stale_if_error.↩ - "Total TTL" is the sum of
resp.get_ttl()and the greater ofresp.get_stale_while_revalidate()andresp.get_stale_if_error().↩