Fastly guide
Fastly Caching with VCL: A Safe Production Tutorial
Treat cache behavior as a release: define the response contract, make the smallest VCL change, activate it deliberately, and prove the resulting behavior before expanding scope.
Fastly can make a public response fast and inexpensive to serve, but an incorrect cache key or TTL can expose one user's representation to another or preserve stale content longer than the product allows. This tutorial uses a narrow, observable rollout for DevOps and web engineers operating a Fastly VCL service.
Do not assume every Cache-Control directive has its usual effect
Fastly documents caching semantics that diverge from a generic interpretation of HTTP Cache-Control. In particular, its cache-control guide lists only public, private, max-age, and s-maxage as directives that influence Fastly caching; directives such as no-cache, no-store, and must-revalidate are passed downstream but do not, by themselves, control Fastly caching. Read Fastly's current documentation before relying on a header policy.
Prerequisites
Before changing production traffic, have all of the following:
- Access that can create and activate a new version of the correct Fastly service. Confirm its service ID, domains, and the team that owns the origin.
- A known-good active version and a named person authorized to reactivate it. Fastly service configurations are versioned; activation is a separate operational action from editing.
- An origin test URL or a low-risk public object whose response is stable, non-personalized, and safe to cache.
- A way to compare origin load, error rate, cache status, and a representative user journey before and after the change.
- An origin contract for the route: allowed methods, authentication behavior, cookies, query parameters, content variants, invalidation owner, and maximum acceptable staleness.
Do not start with login, checkout, account, search results, authorization-dependent APIs, or any response that sets or reflects user state. A cache improvement that changes correctness is not an improvement.
1. Map the service and backend activation path
In Fastly, a service contains the delivery configuration. A backend describes where Fastly fetches from when it needs the origin. Make the relationship explicit before writing VCL:
- Identify the service version currently serving the hostname and preserve its version number as the immediate rollback target.
- Confirm which backend serves the test route, including its address, host-header expectations, health behavior, TLS settings, timeouts, and any existing shielding configuration.
- Clone or otherwise create an editable service version according to the Fastly service versioning guidance. Do not edit a production configuration without knowing the version you will activate and the version you will restore.
- Add the backend or VCL change only to the new version, review the generated diff with the origin owner, then activate that version in a planned window. Fastly's backend guide covers backend configuration concepts and behavior.
Activation changes live traffic. Creating a draft version does not. Keep a short change record containing the service ID, old and candidate versions, routes affected, expected cache status, validation request, and rollback version.
2. Define the effective cache contract first
Write the intended policy in plain language before translating it into headers or VCL. For example: “GET /assets/* is public, varies only by normalized path, stays fresh at Fastly for one hour, may stay stale for five minutes while revalidating, and must be purgeable by release.”
Fastly determines freshness from origin response headers in documented precedence: Surrogate-Control, then Cache-Control: s-maxage, then Cache-Control: max-age, then Expires. Surrogate-Control is useful when browsers need a shorter policy than Fastly. For the exact precedence and documented divergences from RFC 9111, see About cache control headers and Cache freshness.
VCL may override the edge lifetime. In vcl_fetch, beresp.ttl controls Fastly's cached-object TTL; rewriting beresp.http.Cache-Control there does not retroactively change the TTL Fastly already calculated. Conversely, changing only beresp.ttl does not set browser behavior. Keep edge and browser policies intentionally separate.
sub vcl_fetch {
if (bereq.url.path ~ "^/assets/") {
set beresp.ttl = 1h;
set beresp.stale_while_revalidate = 5m;
}
}
This example is a starting pattern, not a copy-and-paste policy. Set stale behavior only when the content owner accepts serving an older representation. Preserve or deliberately set browser-facing headers separately, and test the response produced by your actual origin.
3. Keep the cache key minimal and safe
Fastly's default cache key uses the URL and Host header. Change it only when the application contract requires it. Each added dimension reduces reuse; each omitted representation dimension can return the wrong content.
For every candidate route, document these questions:
- Which path normalization is safe? Do not collapse paths if the origin distinguishes them.
- Which query parameters alter the representation? Remove known tracking parameters only after proving they do not affect the response.
- Does locale, device format, experiment assignment, or a request header change the response? Prefer a controlled
Varypolicy where appropriate instead of an unreviewed custom key. - Can cookies,
Authorization, a session header, or an account identifier influence the body? If yes, do not place that response in a shared public cache without a deliberately isolated key and security review.
Avoid using a raw cookie or authorization value in a shared key. It fragments the cache, risks sensitive data in operational surfaces, and can still be wrong if other identity inputs are omitted. Fastly's caching best practices discusses default key behavior and the tradeoff between fragmentation and unsafe coalescing.
4. Use pass deliberately, not as a debugging shortcut
return(pass) is a correctness control for requests or responses that must not be served from the normal cache path. A request pass in vcl_recv skips lookup and request collapsing. A response pass in vcl_fetch occurs after a lookup and origin fetch; Fastly documents that this creates a hit-for-pass object whose TTL affects subsequent requests.
sub vcl_recv {
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
if (req.http.Authorization || req.http.Cookie ~ "session=") {
return (pass);
}
}
The route and cookie names above are examples. Do not add broad cookie passes without measuring the loss of cacheability, and do not remove them until the response contract proves the object is public. For the difference between request and response pass behavior, including hit-for-pass, consult Fastly VCL best practices.
If you need to force a one-time refetch while retaining normal caching behavior, Fastly documents req.hash_always_miss as different from return(pass). Use it only in a narrowly scoped, temporary test and remove it after validation.
5. Add shielding only after the base path is correct
Shielding sends edge misses through a chosen Fastly shield POP before the origin. It can protect the origin and improve reuse across edge POPs, but it changes the request path and can cause VCL to run more than once. Add it after the unshielded cache behavior is understood, not in the same first change.
When evaluating shielding:
- Pick and test a shield location that is appropriate for the origin and its network path.
- Verify origin access controls permit the shielded request path and origin logs retain a usable request correlation ID.
- Make response-header mutations safe for double execution. Fastly recommends distinguishing the client-connected POP when applying client-facing response changes.
- Interpret cache diagnostics in the shielding context: an edge miss followed by a shield hit avoids an origin request, while headline cache-hit calculations may include both events.
Read Shielding before enabling it. It documents double execution, cache-status interpretation, and the risk that response changes made at the wrong point can be cached downstream.
6. Test, activate, and validate in stages
Use a low-risk route and a candidate service version first. Do not make a broad cache-key change and a broad TTL change in the same release.
- Capture a baseline response from the test URL, including status, body hash,
Cache-Control,Age,X-Cache,X-Cache-Hits,X-Served-By, and origin request count. - Exercise expected variants: a plain anonymous request, each intended locale or representation, a request with a tracking parameter, and an authenticated or cookie-bearing request that must pass.
- Activate the candidate version only after the expected results are written down. Send a small number of controlled requests, then compare body hashes and headers with the baseline.
- Confirm the expected sequence: a cold object can miss, a repeated equivalent request should become a hit within its TTL, and a request that must pass should not reuse the public object. Check the application journey as well as headers.
- If shielding is enabled, validate both edge and shield diagnostics and confirm that the origin sees fewer equivalent requests rather than simply different cache headers.
- Expand traffic or routes gradually, watch cache-hit ratio alongside origin request rate, latency, errors, and business conversions, then document the observed behavior.
Fastly's Checking cache guide explains curl-based checks and the debugging headers. Treat Fastly-Debug as diagnostic access: it can expose information normally removed from responses, so use it only from authorized tooling and do not expose it to clients.
Rollback
Rollback should be faster than diagnosis for a suspected cache leak, stale critical content, or origin regression:
- Stop expanding the rollout and record timestamps, request IDs, affected URL patterns, and the candidate service version.
- Reactivate the known-good service version. Verify the active version, then rerun the same controlled requests.
- If incorrect objects may remain cached, perform a targeted purge only after confirming its scope and ownership. Purging does not fix an unsafe key by itself; restore safe behavior first.
- Confirm anonymous and authenticated journeys, origin load, error rate, and cache diagnostics have returned to the expected state.
- Preserve the evidence and correct the candidate version offline. Do not re-enable it based only on a single cache hit or miss.
Common pitfalls
- Assuming
no-cacheorno-storemeans Fastly will not cache: Fastly documents different semantics. Use its documented controls and test the effective result. - Changing a response header and assuming the edge TTL changed: In VCL, use
beresp.ttlfor Fastly's TTL; browser headers are a separate policy. - Caching a response that varies on identity: A missing key dimension can disclose data. Pass first; design a safe cache contract later.
- Adding every query parameter, cookie, or header to the key: This converts one reusable object into many misses and can overload the origin.
- Using
passeverywhere to make a symptom disappear: It can remove request collapsing and hide the underlying response-contract error while increasing origin traffic. - Enabling shielding with client-facing response mutations: The service may execute twice, and a mutation at the shield can be stored by edge POPs.
- Validating only
X-Cache: A hit proves neither the body nor the variant is correct. Compare content, headers, application behavior, and origin telemetry.
Primary Fastly references
- Working with service versions
- Working with backends
- About cache control headers
- Cache freshness
- Writing VCL code
- VCL best practices
- Shielding
- Checking cache
Make Fastly caching safer to operate
Optimi can help review cache contracts, VCL changes, origin protection, and rollout telemetry before a production change.
Discuss Fastly delivery