---
title: Lifetime and revalidation
summary: null
url: https://www.fastly.com/documentation/guides/concepts/cache/stale
---

> **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](/guides/concepts/cache#customizing-cache-interaction-with-the-backend) for details.

Under general circumstances, an edge cache is incredibly fast and reliable, but that depends on healthy connections to your backends. If backends drop or experience high latency, the cache can become a bottleneck.

Understanding freshness, staleness, and revalidation, as well as judiciously using `stale-while-revalidate` (SWR) and `stale-if-error` (SIE) directives, can help your application act like a local backup system. By serving slightly stale content while fetching fresh data in the background, or by falling back to stale data when the origin fails, you ensure your application remains highly available and resilient to backend outages.

Before diving into the specific implementation flows, note these foundational behaviors:

- **Interface support:** Staleness behaviors and revalidation capabilities depend on the selected cache interface:
  - **[Readthrough cache interface](https://www.fastly.com/documentation/guides/concepts/cache#readthrough-cache):** Full native HTTP caching semantics. Fully supports both SWR and SIE via headers or service configurations.
  - **[Core cache interface](https://www.fastly.com/documentation/guides/concepts/cache#core-cache):** Low-level lifecycle control. Supports manual SWR mechanics, but does not natively manage SIE.
  - **[Simple cache interface](https://www.fastly.com/documentation/guides/concepts/cache/#simple-cache):** Basic key-value lookups. Has no concept of staleness or revalidation, and cannot track or serve expired objects.

- **RFC compliance:** These behaviors implement the HTTP `Cache-Control` header extensions defined in [RFC 5861 - HTTP Cache-Control Extensions for Stale Content](https://httpwg.org/specs/rfc5861.html).

- **Eviction reality:** Content is not guaranteed to be stored for the entire freshness lifetime and, especially in the case of large objects that are not frequently requested, may be evicted to make space for more popular objects.

- **Request collapsing ("leader" and "follower" requests):** When multiple simultaneous requests target the exact same object (whether originating from a single instance or multiple instances across the same POP), the readthrough cache executes [request collapsing](https://www.fastly.com/documentation/guides/concepts/cache/request-collapsing/):
  - **Leader request:** One request is selected by the cache interface to perform the network fetch to the backend. If only a single request exists at any given time, it automatically acts as the leader.
  - **Follower requests:** All other concurrent requests pause and wait. Once the leader's fetch completes, the followers copy and receive the leader's exact response.

## Freshness and Staleness

When content is stored in Fastly's cache, it has a freshness lifetime, also known as a **TTL** or "time to live". This is the period of time during which the cache will serve the content in response to a compatible request, without revalidating it with the origin server.

Once the freshness lifetime expires, the content will no longer be served directly and unconditionally from cache, but the expiry does not prompt the object to be deleted. Instead, it will be marked as **stale**. The way objects are treated once they transition to a stale state can be customized.

Objects may have one of three distinct types of stale state, in addition to being fresh. As an example, consider an object arriving from the backend with the following response header:

```http
Cache-Control: max-age=300, stale-while-revalidate=60, stale-if-error=86400
```

When processed by the [readthrough cache](https://www.fastly.com/documentation/guides/concepts/cache#readthrough-cache) this object will, unless evicted earlier due to lack of use, transit the following states:

![stale phases](/img/stale-phases-precomposed-20260520.png)

### Cdn Services

When a client request matches a stale object, the readthrough cache interface processes the object using the following steps:

1. If the object is within a [`stale-while-revalidate` (SWR)](https://www.fastly.com/documentation/guides/concepts/cache/stale#stale-while-revalidate-eliminate-origin-latency) period, then it will be served immediately to the client. Additionally, an asynchronous fetch to the backend will be scheduled to [revalidate](https://www.fastly.com/documentation/guides/concepts/cache/stale#revalidation) or update the stale object. See [background revalidation process flow](https://www.fastly.com/documentation/guides/concepts/cache/stale#background-revalidation-process-flow) for details on this process.
2. Otherwise, the cache interface sends the request to the backend using a _blocking fetch_.
   - If this backend request was successful:
     - The default behavior is for the cache interface to use the response to revalidate or possibly update the stale object, and then return the response to the client.
     - If the stale object is in a [`stale-if-error` (SIE)](https://www.fastly.com/documentation/guides/concepts/cache/stale#stale-if-error-survive-origin-failure) period, it is possible to use edge code to return the stale content instead. The cached object is not updated in this case. See [explicitly serving stale content](https://www.fastly.com/documentation/guides/concepts/cache/stale#explicitly-serving-stale-content) for details.
   - If the backend request failed:
     - If the stale object is within an SIE period, the stale object is returned to the client.
     - Otherwise, an error describing the failure is returned to the client.

> **HINT:** Setting the `req.hash_always_miss` or `req.hash_ignore_busy` variables to `true` will disable the effects of the `Cache-Control` directives SWR and SIE for that request.

### Compute Services

> **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](/guides/concepts/cache#customizing-cache-interaction-with-the-backend) for details.

When a Compute application's code makes a request to a backend that matches a stale object, the readthrough cache interface processes the object using the following steps:

1. If the stale object is within a [`stale-while-revalidate` (SWR)](https://www.fastly.com/documentation/guides/concepts/cache/stale#stale-while-revalidate-eliminate-origin-latency) period, then it is returned immediately to the application. Additionally, an asynchronous fetch to the backend will be scheduled to [revalidate](https://www.fastly.com/documentation/guides/concepts/cache/stale#revalidation) or update the stale object. See [background revalidation process flow](https://www.fastly.com/documentation/guides/concepts/cache/stale#background-revalidation-process-flow) for details on this process.
2. Otherwise, the cache interface sends the request to the backend using a _blocking fetch_.
   - If this fetch was successful:
     - The default behavior is for the cache interface to use the response to revalidate or possibly update the stale object, and then return the response to the application.
     - If the stale object is in a [`stale-if-error` (SIE)](https://www.fastly.com/documentation/guides/concepts/cache/stale#stale-if-error-survive-origin-failure) period, it is possible to return an error from the after-send hook to return the stale content instead. The cached object is not updated in this case. See [explicitly serving stale content](https://www.fastly.com/documentation/guides/concepts/cache/stale#explicitly-serving-stale-content) for details.
   - If this fetch failed:
     - If the stale object is within an SIE period, the stale object is returned to the application.
     - Otherwise, an error describing the failure (including any override error returned from your after-send hook) is returned to the Compute application.

> **HINT:** Setting `req.set_pass(true)` will disable the effects of the `Cache-Control` directives `stale-while-revalidate` and `stale-if-error` for that request.

## Revalidation

In HTTP caching, revalidation is the process where a cache checks with an upstream server to verify if a stale cached object is still accurate and safe to serve. Instead of blindly deleting expired content, revalidation allows the readthrough cache to sync its contents with the source of truth.

Revalidation occurs when a request is made to the readthrough cache for an object that is stale. Upon completion, the object's freshness lifetime is reset.

Depending on the metadata available, this process happens in one of two ways:

- **Conditional Revalidation:** This occurs when the cached object includes a **validator** (an `ETag` or `Last-Modified` header). The cache uses these to ask the upstream server whether this specific version has changed.
- **Unconditional Revalidation:** This occurs when the cache has no metadata to use as a reference point. Forced to make a standard request, the cache asks the upstream server for the asset from scratch.

> **HINT:** Revalidations triggered as a result of a `stale-while-revalidate` directive happen in the background, _after_ the stale object has already been delivered to the client. See [Stale while revalidate](https://www.fastly.com/documentation/guides/concepts/cache/stale#stale-while-revalidate-eliminate-origin-latency) for details.

### Conditional revalidation where contents have not changed

If the stale object has a validator (an `ETag` or `Last-Modified` header), the readthrough cache interface performs a conditional revalidation by sending the headers `If-None-Match`, `If-Modified-Since`, or both.

If the response to a revalidation request has status `304 Not Modified`, the backend has determined that the contents still match what is cached. This will cause the lifetime of the _existing object_ to be extended based on the [standard rules on calculating cache TTL](https://www.fastly.com/documentation/guides/concepts/cache/cache-freshness) and will reset the object's `Age`.

> **NOTE:** If the initial object's TTL was determined by an `Expires` header and no freshness-related headers are present on a `304` response, the cache will set a TTL of _2 minutes_ (a default TTL) for the existing object. This is because the `Expires` header value identifies a fixed point in time while other freshness header values are given as times relative to the time that the response was received.

### Cdn Services

No other aspects of the existing cached response will be modified. This means that after a successful revalidation, future client requests will receive the headers from the original backend response that populated the cache, rather than the headers returned in the 304 revalidation response.

This kind of response _does not trigger `vcl_fetch`_.

### Compute Services

In a Compute service, the readthrough cache interface invokes the after-send callback, passing in a ["candidate" response object](https://www.fastly.com/documentation/guides/concepts/cache#candidate-response) that represents a complete response _except for the body_. This object has the status code, response headers, and cache policy based on the existing cached response, and reflects any headers and cache policy updates included in the backend revalidation response (for example, to extend the object's lifetime). These values are further merged with any changes you make during the after-send callback, and then finally used to update the cached response object. As a revalidation response does not contain a body, these updates occur "in-place", _without invoking the body-transform callback_, and without changing the cached response body.

### Conditional revalidation where contents have changed, or unconditional revalidation

If the response to a revalidation request has a status other than `304`, the backend has determined that it will provide the newest copy of the data. The readthrough cache will store a copy and reset the object's `Age`.

### Cdn Services

Any response to a revalidation request other than `304` will be processed normally, trigger `vcl_fetch`, and (if cacheable) will replace the stale object in cache.

### Compute Services

In a Compute service, any response to a revalidation request other than `304` will be processed normally, trigger the after-send and body-transform callbacks, and (if cacheable) will replace the stale object in cache.

### Disabling revalidation

In some cases an origin server may be configured to serve responses with `ETag` or `Last-Modified` headers, but you prefer not to allow revalidation.

### Cdn Services

To disable revalidation entirely, remove the `ETag` and `Last-Modified` headers from the response when it's received from the backend. This can be done in `vcl_fetch`:

```vcl context="sub vcl_fetch { ... }"
unset beresp.http.etag;
unset beresp.http.last-modified;
```

Alternatively, you can enable revalidation between client and Fastly's cache, whilst effectively disabling it between Fastly's cache and the backend, by modifying the value of the `ETag` header:

```vcl context="sub vcl_fetch { ... }"
set beresp.http.etag = beresp.http.etag "-fastly";
unset beresp.http.last-modified;
```

### Compute Services

To disable revalidation entirely, remove the `ETag` and `Last-Modified` headers from the response when it's received by from the backend. This can be done in the [after-send](https://www.fastly.com/documentation/guides/concepts/cache#after-send-callback) callback:

### Rust

```rust compile_fail
req.set_after_send(|resp| {
  resp.remove_header("ETag");
  resp.remove_header("Last-Modified");
  Ok(())
});
```

### Javascript

```javascript
res = fetch(url, {
  cacheOverride: new CacheOverride({
    afterSend(resp) {
      resp.headers.delete('ETag');
      resp.headers.delete('Last-Modified');
    }
  })
});
```

### Go

```go
r.CacheOptions.AfterSend = func(cr *CandidateResponse) error {
  cr.DelHeader("ETag")
  cr.DelHeader("Last-Modified")
  return nil
}
```

### Cpp

In this version of the Fastly Compute C++ SDK, this behavior cannot be overridden.

Alternatively, you can enable revalidation between the client and the Fastly edge platform, whilst effectively disabling it between Fastly's cache and the backend, by modifying the value of the `ETag` header:

### Rust

```rust compile_fail
req.set_after_send(|resp| {
  if let Some(etag) = resp.get_header_str("ETag") {
    resp.set_header("ETag", format!("{etag}-fastly"));
  }
  resp.remove_header("Last-Modified");
  Ok(())
});
```

### Javascript

```javascript
res = fetch(url, {
  cacheOverride: new CacheOverride({
    afterSend(resp) {
      const etag = resp.headers.get('ETag');
      if (etag != null) {
        resp.headers.set('ETag', `${etag}-fastly`);
      }
      resp.headers.delete('Last-Modified');
    }
  })
});
```

### Go

```go
r.CacheOptions.AfterSend = func(cr *CandidateResponse) error {
  if _, err := cr.Header("ETag"); err == nil {
    cr.DelHeader("Last-Modified")
  }
  return nil
}
```

### Cpp

In this version of the Fastly Compute C++ SDK, this behavior cannot be overridden.

## Stale while revalidate: Eliminate origin latency

`stale-while-revalidate` tells caches that they may continue to serve a response after it becomes stale for up to the specified number of seconds, provided that they work asynchronously in the background to fetch a new one. For example, an origin server may provide a response with the following headers:

```http
Cache-Control: max-age=300, stale-while-revalidate=60
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
```

Upon receiving this response, Fastly's cache will store and reuse that response for up to 5 minutes (`max-age=300`) as normal. Once that freshness lifetime expires, the `stale-while-revalidate` directive allows it to continue to serve the same content for up to another 60 seconds, provided that time is used to [revalidate](https://www.fastly.com/documentation/guides/concepts/cache/stale#revalidation) the cached content with the origin server in the background. As soon as a new response is available, it will replace the stale content and its own cache freshness rules will take effect. However, if the 60-second revalidation period expires, and it has not been possible to get the updated content, the stale version will no longer be usable in this manner (it may remain stale for a further period of time if it also has a `stale-if-error` directive that represents a longer window period).

### Background revalidation process flow

### Cdn Services

When a VCL service handles a request that matches a stale object still within its SWR window, the readthrough cache interface executes a two-step process:

1. **Immediate Response:** The cache immediately delivers the stale object to both the leader and all follower requests to avoid latency.
2. **Asynchronous Refresh:** The cache schedules the leader request to fork and execute a background fetch to revalidate, or if deemed cacheable, replace the object.
   - **Cacheability:** The background revalidation process will deem the response to be cacheable if the origin fetch succeeds and `beresp.cacheable` is set to `true` at the end of `vcl_fetch`.
   - **Variable Behavior:** VCL subroutines executing within this background revalidation context are explicitly identifiable via the `req.is_background_fetch` VCL variable evaluating to `true`.

Each step invokes VCL subroutines as follows:

![Background revalidation flow](/img/stale-swr-flow-precomposed.png)

### Compute Services

When a Compute application's code makes a request that matches a stale object still within its SWR window, the readthrough cache interface executes a two-step process:

1. **Immediate Response:** The cache immediately delivers the stale object to both the leader and all follower requests to avoid latency.
   - Hook Behavior: Because these requests are fulfilled directly from the cache, they do not invoke [before-send or after-send hooks](https://www.fastly.com/documentation/guides/concepts/cache#customizing-cache-interaction-with-the-backend).
2. **Asynchronous Refresh:** The cache schedules the leader request to execute a background fetch to revalidate, or [if deemed cacheable](https://www.fastly.com/documentation/guides/concepts/cache/cache-freshness/#cache-outcome), replace the object.
   - Hook Behavior: Because this background fetch actively traverses the cache layer, it will invoke any before-send and after-send hooks.

### SWR background fetch failures

If the background revalidation fetch fails or the response is uncacheable, the cache interface applies the following logic:

- **Cache retention:** The stale object is left unmodified in the cache and retains its status for subsequent requests.
- **Revalidation retries:** Subsequent requests will continue to attempt backend revalidation until the SWR period expires.

Failures typically occur due to:

- **Network issues:** The backend is unreachable or a network error occurs.
- **Unhealthy backend:** The backend is currently failing [health checks](https://www.fastly.com/documentation/guides/concepts/healthcheck).
- **Synthetic error (Compute):** Your custom code explicitly returns an error from the after-send hook.

## Stale if error: Survive origin failure

`stale-if-error` (SIE) is an instruction that if a backend is sick (i.e. is currently failing a [health check](https://www.fastly.com/documentation/guides/concepts/healthcheck)) or unreachable, a stale response may be used instead of outputting an error - which helps always guarantee a nice user experience even during periods of server instability.

```http
Cache-Control: max-age=300, stale-if-error=86400
```

In the above example, Fastly's cache will store and serve the fresh content for 5 minutes, just like the previous example, but this time, when the 5 minutes has expired, the next request for this content will _block on a synchronous fetch to origin_. Unlike SWR, SIE doesn't allow for any asynchronous revalidation.

### Cdn Services

In a VCL service, whenever a request is made through the readthrough cache interface during an SIE window:

- If the origin is sick (i.e. is currently failing a [health check](https://www.fastly.com/documentation/guides/concepts/healthcheck)), the stale content will be served automatically.
- If the origin is erroring (i.e. responding with a valid HTTP response such as a `503` "service unavailable"), the Fastly platform invokes `vcl_fetch` and sets `stale.exists` to `true`. The stale content may be used by [explicitly selecting it](https://www.fastly.com/documentation/guides/concepts/cache/stale#explicitly-serving-stale-content).
- If the origin is down/unreachable, the Fastly platform invokes `vcl_error` and sets `stale.exists` to `true`. The stale content may be used by [explicitly selecting it](https://www.fastly.com/documentation/guides/concepts/cache/stale#explicitly-serving-stale-content).

### Compute Services

When a request matches a stale object within its `stale-if-error` window, the cache interface attempts to contact the backend for a fresh copy, falling back to the stale asset only if the backend fetch fails.

1. **Blocking Fetch:** The interface initiates a _blocking fetch_ to the backend.
   - Hook Behavior: This fetch traverses the cache, meaning the leader request invokes any before-send and after-send hooks. Follower requests pause and wait for the leader's outcome.
2. **Outcome Evaluation:** The cache processes the results based on the fetch outcome, as described below.

### Fetch success case

If the origin returns an HTTP response (including 5xx status codes), it is considered successful. If [the response is deemed cacheable](https://www.fastly.com/documentation/guides/concepts/cache/cache-freshness/#cache-outcome), the cache interface updates the stale object in the cache. The leader and all follower requests receive the fresh backend Response.

If the response is a 5xx error or otherwise not usable according to the application, it may be preferable to use the stale content instead. To do this, use the after-send hook to check for the existence of stale content and then [explicitly return an error result](https://www.fastly.com/documentation/guides/concepts/cache/stale#stale-while-revalidate-eliminate-origin-latency). The fetch is considered to have failed, and the cache follows the failure case below, blocking the cache update and returning the stale content to the Compute application.

### Fetch failure case

The interface falls back and delivers the stale object to the application. The stale object is not updated in the cache.

Failures typically occur due to:

- **Network issues:** The backend is unreachable or a network error occurs.
- **Unhealthy backend:** The backend is currently failing [health checks](https://www.fastly.com/documentation/guides/concepts/healthcheck).
- **Synthetic error:** Your custom code explicitly returns an error from the after-send hook.

When your application has received the response, you can discern whether it is a fresh copy received from the backend or a stale copy returned from SIE by examining whether a "masked error" is attached to the Response, distributed as follows:

- The leader: Receives the actual error returned by the fetch (or the custom override error if one is generated by your after-send hook).
- The followers: Receive a distinct `RequestCollapse` error code, signaling that the leader's fetch failed and the cache fallback path was chosen.

### Explicitly serving stale content

<div id="modifying-staleness-related-behavior-in-vcl-services"></div>

Fastly's cache honors staleness-related caching directives as indicated above, and it is possible to use edge code to further control the serving of stale content.

### Cdn Services

- Stale content can be explicitly selected if a cached object exists and is within a `stale-if-error` (SIE) period, in the following subroutines:
  - **In `vcl_fetch`**: if the origin returns a response which is _valid_ HTTP, then Fastly's cache will by default serve the received object, and [if cacheable](https://www.fastly.com/documentation/guides/concepts/cache/cache-freshness/#response-processing), use it to replace the stale object in cache. However, if the response is nonsensical or an error, you may prefer in that scenario to serve the stale content instead, by using `return(deliver_stale)`.
  - **In `vcl_error`**: if, during a fetch to origin, Fastly's cache encounters a _network level_ error, such as finding the origin unreachable or being unable to negotiate an acceptable TLS session, an error will be triggered and VCL control flow will be moved to `vcl_error` directly, without running `vcl_fetch`. By default, this will result in serving an error page generated by the Fastly platform to the end user, but if stale content exists in cache you can opt to use this instead by using `return(deliver_stale)` from `vcl_error`.
  - **In `vcl_miss`**: it is also possible to switch to a stale object in `vcl_miss`, but there is unlikely to be a reason to do so.

- The existence of stale content can be checked during the above subroutines using the `stale.exists` variable, which will only be `true` if a cached object exists and is within an SIE period. Stale content in the _expired_ state cannot be used from VCL.

### Compute Services

- If a backend fetch returns an HTTP response that includes a `stale-while-revalidate` or `stale-if-error` directive, those values can be read and overridden in the **after-send** hook.

- If a backend fetch returns an HTTP response (including 5xx status codes), the cache interface returns the response to the application, and [if cacheable](https://www.fastly.com/documentation/guides/concepts/cache/cache-freshness/#response-processing), uses it to replace the stale object in the cache.

   Stale content can be explicitly selected if a cached object exists and is within a `stale-if-error` (SIE) period. To do this, return an error from the **after-send** hook. The stale content is returned to the Compute application. The cached object is not updated and retains its status for subsequent requests.

- The existence of stale content can be checked during the after-send hook using the `stale_if_error_available()` function, which will be `true` if a cached object exists and is within an SIE period.

## Interaction between stale-while-revalidate and stale-if-error

If a response specifies both `stale-while-revalidate` (SWR) and `stale-if-error` (SIE) directives together, both windows start at the same time: the moment the object's freshness period (`max-age`) expires. Because these windows run simultaneously, the cache decides how to handle requests using a straightforward rule of priority:

- **SWR takes precedence:** As long as the SWR window is active, the cache uses SWR behavior. It immediately returns the stale object to the application to eliminate latency, then triggers an asynchronous background fetch to refresh the data.
- **SIE acts as a fallback:** SIE behavior only kicks in after the SWR window has expired. Once SWR is over, the cache switches to a blocking fetch and will only serve the stale object if that fetch fails.

> **NOTE:** Because SWR takes precedence over SIE in an overlap, if the SWR window is longer than or equal to the SIE window, the SIE behavior will never take effect.

## Expired staleness window

Once the stale period expires, the content can no longer be served to clients or used as a fallback. If the object is requested and the upstream server is sick or a failure is encountered during the fetch, the readthrough cache will return an error.

However, completely expired content is still not immediately deleted. If the object is requested and the expired object contains a validator (an `ETag` or `Last-Modified` header), the readthrough cache will still attempt a conditional revalidation with the upstream server. If the server confirms the content is still valid with a `304 Not Modified` response, the cache will freshen the object and reset its lifetime, successfully avoiding transferring a full body payload from the upstream server despite the asset having fully expired.

## Applying staleness directives only to Fastly's cache

<div id="applying-staleness-directives-to-fastly-only"></div>

`stale-*` directives apply to all caching HTTP clients, not just CDNs and other non-browser clients. While stale-serving in browsers is also useful, if you are trying to apply stale behaviors only to Fastly's cache, consider using the `Surrogate-Control` cache-control header. It functions similarly to `Cache-Control`, but overrides it if the two are both present and is removed by Fastly's cache automatically, so you can control the stale logic for it independently of browsers.

```http
Surrogate-Control: max-age=300, stale-while-revalidate=60, stale-if-error=86400
Cache-Control: max-age=60
```

`Surrogate-Control` has the same spec as `Cache-Control` but Fastly's cache does not support the `s-maxage` directive (in the context of `Surrogate-Control`, `s-maxage` would mean the same thing as `max-age`, so use `Surrogate-Control: max-age`).

## Summary table

### Cdn Services

To summarize, these are the four possible freshness states that a piece of cached content can be in when it is matched by an incoming request:

- **Fresh:** Fastly's cache has a cached copy of the content, and it's within its initial freshness lifetime
- **Stale-while-revalidate (SWR):** Fastly's cache has a stale version of the content, and it's within a SWR period
- **Stale-if-error (SIE):** Fastly's cache has a stale version, and it doesn't qualify for SWR (or that period has already expired), but it's within a SIE period, allowing it to be used automatically if an origin is sick.
- **None:** Fastly's cache doesn't have the content or it's expired (content in this state will still allow for conditional fetches if it has an `ETag` or `Last-Modified` date)

And there are also four possible states that an origin server can be in:

- **Healthy:** The backend is up and working
- **Erroring:** The backend is returning syntactically valid HTTP responses with response status codes in the 5xx range (e.g., `503` "service unavailable")
- **Down:** The backend is unreachable, or is unable to negotiate a TCP connection
- **Sick:** Fastly's cache has marked this origin as unusable because it has consistently been unable to fetch from a health check endpoint. Origins in a _down_ or _erroring_ state that have a health check will eventually be transitioned to _sick_ by the [health check](https://www.fastly.com/documentation/guides/concepts/healthcheck).

This results in 16 possible permutations, whose outcomes can be visualized as a grid:

The three possible outcomes are that the user will see the content they want served from the edge (😀), they'll get the content but including a blocking fetch to origin (😴), or they'll see an unfiltered error (😡), which could be either something generated by Fastly's platform or whatever your origin server returned.

> **HINT:** Some of these scenarios can be improved in VCL services by adjusting the default configuration provided by Fastly platform's to be more aggressive about using stale content. For more details, see the [serving stale](https://www.fastly.com/documentation/solutions/tutorials/full-site-delivery/serving-stale) tutorial.

### Compute Services

> **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](/guides/concepts/cache#customizing-cache-interaction-with-the-backend) for details.

To summarize, these are the four possible freshness states that a piece of cached content can be in when it is matched by an incoming request:

- **Fresh:** Fastly's cache has a cached copy of the content, and it's within its initial freshness lifetime
- **Stale-while-revalidate (SWR):** Fastly's cache has a stale version of the content, and it's within a SWR period
- **Stale-if-error (SIE):** Fastly's cache has a stale version, and it doesn't qualify for SWR (or that period has already expired), but it's within a SIE period, allowing it to be used automatically if an origin is sick.
- **None:** Fastly's cache doesn't have the content or it's expired (content in this state will still allow for conditional fetches if it has an `ETag` or `Last-Modified` date)

And there are also four possible states that an origin server can be in:

- **Healthy:** The backend is up and working
- **Erroring:** The backend is returning syntactically valid HTTP responses with response status codes in the 5xx range (e.g., `503` "service unavailable")
- **Down:** The backend is unreachable, or is unable to negotiate a TCP connection
- **Sick:** Fastly's cache has marked this origin as unusable because it has consistently been unable to fetch from a health check endpoint. Origins in a _down_ or _erroring_ state that have a health check will eventually be transitioned to _sick_ by the [health check](https://www.fastly.com/documentation/guides/concepts/healthcheck).

This results in 16 possible permutations, which can be visualized as a grid to show where good and bad things happen:

The three possible outcomes are that the user will see the content they want served from the edge (😀), they'll get the content but including a blocking fetch to origin (😴), or they'll see an unfiltered error (😡), which could be either something generated by Fastly's platform or whatever your origin server returned.

## Shielding considerations

If you have [shielding](https://www.fastly.com/documentation/guides/concepts/shielding) enabled in your service, the shield POP may serve stale content to the edge POP, which should avoid caching that content as fresh. By default, the right thing happens because the shield POP will send an `Age` header along with the response to the edge POP, and the edge POP will not cache the response because the `Age` already exceeds the object's freshness TTL (specified by a `max-age` directive).

However, in some circumstances, stale content served from a shield POP to an edge POP may be cached as if fresh:

- **Soft purging:** When an object is [purged](https://www.fastly.com/documentation/guides/concepts/cache/purging) with soft purging, the Shield POP marks its local cache entry as stale but continues to serve it while asynchronously fetching a fresh copy from the origin. If an Edge POP requests the object during this revalidation window, it may misinterpret the response headers and cache the stale data with a fresh TTL.
- **Custom code:** If your edge code explicitly manipulates an object's TTL at the edge, it can override the upstream `Age` and `max-age` headers provided by the Shield POP. The Edge POP will enforce the hardcoded code-level TTL, caching the stale object as fresh while ignoring the Shield's age indicators.
- **Conditional GET with 304 response:** When an Edge POP sends a conditional GET request (e.g., validating validators like `If-None-Match` or `If-Modified-Since`) to the Shield POP, the Shield may evaluate its own stale cache and return an `HTTP 304 Not Modified` response. The Edge POP interprets this 304 status as a freshness validation and resets its local cache timer, extending the lifecycle of the stale data.

### Cdn Services

The inadvertent caching of stale content at the edge [POP](https://www.fastly.com/documentation/guides/getting-started/concepts/using-fastlys-global-pop-network) due to these edge cases can be prevented in VCL services by disabling the use of stale content for asynchronous revalidation when a POP is acting as a shield:

```vcl context="sub vcl_recv { ... }"
if (fastly.ff.visits_this_service > 0) {
  set req.max_stale_while_revalidate = 0s;
}
```

This code will continue to allow stale content to be used when an origin is sick. To disable that as well, set `req.max_stale_if_error` to `0s`.

### Compute Services

The inadvertent caching of stale content at the edge [POP](https://www.fastly.com/documentation/guides/getting-started/concepts/using-fastlys-global-pop-network) due to these edge cases can be prevented in Compute services by disabling the use of stale content for asynchronous revalidation when a POP is acting as a shield:

### Rust

```rust compile_fail
// section visible
let shield = match Shield::new("") {
  Ok(v) => v,
  Err(e) => return Ok(Response::from_status(StatusCode::INTERNAL_SERVER_ERROR)
      .with_body(format!("Could not find shield '': {:?}", e))),
};

if !shield.running_on() {
  // If we're not running on the shield POP, look up the encrypted
  // tunnel to that backend, and try to forward it on.
// section-end visible
  let response = match shield.encrypted_backend() {
    Err(e) => Ok(
        Response::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_body(format!(
            "Could not convert shield  into an encrypted backend: {:?}",
            e
        )),
    ),

    Ok(backend) => {
      // We will want to make considerations about caching here.
      match req.send(backend) {
        Err(e) => Ok(Response::from_status(StatusCode::INTERNAL_SERVER_ERROR)
           .with_body(format!("Could not send request to shield: {}", e))),

        Ok(resp) => {
          // If you want to add a header somewhere to mark that you got this
          // from a shield, that can be handy for debugging, and this would
          // be the place to do it.
          Ok(resp)
        }
      }
    }
  };

  return response;
// section visible
}
// if we get here, then we're running on the shield!

let resp = req
    .with_stale_while_revalidate(0) // Set stale-while-revalidate to 0 seconds
    .send(backend)?;
return resp;
// section-end visible
```

### Javascript

```js
// section visible

async function handleRequest(event) {
    const shield = new Shield('');
    if (!shield.runningOn()) {
        // If we're not running on the shield POP, look up the encrypted
        // tunnel to that backend, and try to forward it on.
// section-end visible
        const backend = shield.encryptedBackend();

        // We will want to make considerations about caching here.
        // If you want to add a header somewhere to mark that you got this
        // from a shield, that can be handy for debugging, and this would
        // be the place to do it.
        return await fetch(event.request, { backend });
// section visible
    }
    // if we get here, then we're running on the shield!

    let resp = fetch(event.req, {
            backend
            cacheOverride: new CacheOverride({
                staleWhileRevalidate: 0, // Set stale-while-revalidate to 0 seconds
            }),
        });

    return resp
}
// section-end visible

addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
```

### Go

```go
// section visible
import (
// section-end visible
    "context"
    "fmt"
    "net"

	"github.com/fastly/compute-sdk-go/fsthttp"
// section visible
	"github.com/fastly/compute-sdk-go/shielding"
)
// section-end visible

// section visible
func main() {
    fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) {
        shield, err := shielding.ShieldFromName("")
        if err != nil {
            w.WriteHeader(fsthttp.StatusInternalServerError)
            fmt.Fprintln(w, err.Error())
            return
        }

        if !shield.IsRunningOn() {
            // If we're not running on the shield POP, look up the encrypted
            // tunnel to that backend, and try to forward it on.
// section-end visible
            backend, err := shield.Backend(nil) // Passing nil uses default BackendOptions
            if err != nil {
                w.WriteHeader(fsthttp.StatusInternalServerError)
                fmt.Fprintln(w, err.Error())
                return
            }

            // We will want to make considerations about caching here.
            resp, err := req.Send(ctx, backend)
            if err != nil {
                w.WriteHeader(fsthttp.StatusInternalServerError)
                fmt.Fprintln(w, err.Error())
                return
            }

            // If you want to add a header somewhere to mark that you got this
            // from a shield, that can be handy for debugging, and this would
            // be the place to do it.
            // resp.Header.Set("X-From-Shield", "true")

            w.Header().Reset(resp.Header)
            w.WriteHeader(resp.StatusCode)
            io.Copy(w, resp.Body)
// section visible
        }
        // if we get here, then we're running on the shield!

        r.CacheOptions.StaleWhileRevalidate = 0 // Set stale-while-revalidate to 0 seconds

        resp, err := r.Send(ctx, backend)
        if err != nil {
            w.WriteHeader(fsthttp.StatusBadGateway)
            fmt.Fprintln(w, err.Error())
            return
        }

        w.Header().Reset(resp.Header)
        w.WriteHeader(resp.StatusCode)
        io.Copy(w, resp.Body)
    })
}
// section-end visible
```

### Cpp

Shielding is not available in this version of the Fastly Compute C++ SDK.

Note that the above code will continue to allow stale content to be used when an origin is sick.

To disable that as well, also set the `stale-if-error` lifetime to `0s`:

### Rust

```rust compile_fail
let shield = match Shield::new("") {
  Ok(v) => v,
  Err(e) => return Ok(Response::from_status(StatusCode::INTERNAL_SERVER_ERROR)
      .with_body(format!("Could not find shield '': {:?}", e))),
};

if !shield.running_on() {
  // If we're not running on the shield POP, look up the encrypted
  // tunnel to that backend, and try to forward it on.
  let response = match shield.encrypted_backend() {
    Err(e) => Ok(
        Response::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_body(format!(
            "Could not convert shield  into an encrypted backend: {:?}",
            e
        )),
    ),

    Ok(backend) => {
      // We will want to make considerations about caching here.
      match req.send(backend) {
        Err(e) => Ok(Response::from_status(StatusCode::INTERNAL_SERVER_ERROR)
           .with_body(format!("Could not send request to shield: {}", e))),

        Ok(resp) => {
          // If you want to add a header somewhere to mark that you got this
          // from a shield, that can be handy for debugging, and this would
          // be the place to do it.
          Ok(resp)
        }
      }
    }
  };

  return response;
}
// if we get here, then we're running on the shield!

// section visible
let resp = req
    .with_stale_while_revalidate(0) // Set stale-while-revalidate to 0 seconds
    .with_stale_if_error(0)         // Set stale-if-error to 0 seconds
    .send(backend)?;
return resp;
// section-end visible
```

### Javascript

```js

async function handleRequest(event) {
    const shield = new Shield('');
    if (!shield.runningOn()) {
        // If we're not running on the shield POP, look up the encrypted
        // tunnel to that backend, and try to forward it on.
        const backend = shield.encryptedBackend();

        // We will want to make considerations about caching here.
        // If you want to add a header somewhere to mark that you got this
        // from a shield, that can be handy for debugging, and this would
        // be the place to do it.
        return await fetch(event.request, { backend });
    }
    // if we get here, then we're running on the shield!

// section visible
    let resp = fetch(event.req, {
            backend
            cacheOverride: new CacheOverride({
                staleWhileRevalidate: 0, // Set stale-while-revalidate to 0 seconds
                staleIfError: 0,         // Set stale-if-error to 0 seconds
            }),
        });

    return resp
// section-end visible
}

addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
```

### Go

```go
import (
    "context"
    "fmt"
    "net"

	"github.com/fastly/compute-sdk-go/fsthttp"
	"github.com/fastly/compute-sdk-go/shielding"
)

func main() {
    fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) {
        shield, err := shielding.ShieldFromName("")
        if err != nil {
            w.WriteHeader(fsthttp.StatusInternalServerError)
            fmt.Fprintln(w, err.Error())
            return
        }

        if !shield.IsRunningOn() {
            // If we're not running on the shield POP, look up the encrypted
            // tunnel to that backend, and try to forward it on.
            backend, err := shield.Backend(nil) // Passing nil uses default BackendOptions
            if err != nil {
                w.WriteHeader(fsthttp.StatusInternalServerError)
                fmt.Fprintln(w, err.Error())
                return
            }

            // We will want to make considerations about caching here.
            resp, err := req.Send(ctx, backend)
            if err != nil {
                w.WriteHeader(fsthttp.StatusInternalServerError)
                fmt.Fprintln(w, err.Error())
                return
            }

            // If you want to add a header somewhere to mark that you got this
            // from a shield, that can be handy for debugging, and this would
            // be the place to do it.
            // resp.Header.Set("X-From-Shield", "true")

            w.Header().Reset(resp.Header)
            w.WriteHeader(resp.StatusCode)
            io.Copy(w, resp.Body)
        }
        // if we get here, then we're running on the shield!

// section visible
        r.CacheOptions.StaleWhileRevalidate = 0 // Set stale-while-revalidate to 0 seconds
        r.CacheOptions.StaleIfError  = 0        // Set stale-if-error to 0 seconds

        resp, err := r.Send(ctx, backend)
// section-end visible
        if err != nil {
            w.WriteHeader(fsthttp.StatusBadGateway)
            fmt.Fprintln(w, err.Error())
            return
        }

        w.Header().Reset(resp.Header)
        w.WriteHeader(resp.StatusCode)
        io.Copy(w, resp.Body)
    })
}
```

### Cpp

Shielding is not available in this version of the Fastly Compute C++ SDK.

## Best practices

The following practices are recommended to get the most out of stale content:

### Cdn Services

- Specify a short `stale-while-revalidate` and a long `stale-if-error` value. If your origin is working, you don't want to subject users to content that is significantly out of date. But if your origin is down, you're probably much more willing to serve something old if the alternative is an error page.
- Always include a validator (an `ETag` or `Last-Modified` header) on responses from origin.
- Use [shielding](https://www.fastly.com/documentation/guides/concepts/shielding) to increase the cache hit ratio and increase the probability of having stale objects to serve.
- Always use [soft purges](https://www.fastly.com/documentation/guides/concepts/cache/purging#soft-vs-hard-purging) to ensure stale versions of objects aren’t also evicted.

### Compute Services

- Specify a short `stale-while-revalidate` and a long `stale-if-error` value. If your origin is working, you don't want to subject users to content that is significantly out of date. But if your origin is down, you're probably much more willing to serve something old if the alternative is an error page.
- Always include a validator (an `ETag` or `Last-Modified` header) on responses from origin.
- Always use [soft purges](https://www.fastly.com/documentation/guides/concepts/cache/purging#soft-vs-hard-purging) to ensure stale versions of objects aren’t also evicted.


