Fastly’s VCL isn’t just a configuration language; it’s a programmable network edge that lets you run Go, Rust, and JavaScript directly on their servers, right next to the cache.
Here’s a simplified VCL snippet that demonstrates how to serve a static asset from cache, falling back to the origin if it’s not found:
vcl 1.0;
backend default {
.host = "192.0.2.1";
.port = "80";
}
sub vcl_recv {
# If the request is not a GET or HEAD, pass it directly to the origin.
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
# Normalize URLs: remove query strings for cache lookup.
if (req.url ~ "\?") {
set req.url = regsub(req.url, "\?.*$", "");
}
# Cache requests for specific file extensions.
if (req.url ~ "\.(jpg|jpeg|png|gif|css|js|ico|woff|woff2)$") {
return (lookup);
} else {
# For all other requests, pass them to the origin.
return (pass);
}
}
sub vcl_hit {
# If the cached object is still fresh, serve it.
if (now - obj.last_modified < 1h) {
return (deliver);
}
# If the cached object is stale, revalidate with the origin.
return (fetch);
}
sub vcl_miss {
# If the object is not in cache, fetch it from the origin.
return (fetch);
}
sub vcl_fetch {
# Set a default cache expiration of 1 hour if the origin doesn't provide one.
if (resp.http.Cache-Control !~ "max-age" && resp.http.Expires !~ "") {
set resp.http.Cache-Control = "public, max-age=3600";
}
return (deliver);
}
This VCL defines a backend named default pointing to your origin server at 192.0.2.1 on port 80.
In vcl_recv, we first check the request method. If it’s not GET or HEAD, we bypass the cache and return (pass) to send it straight to the origin. Then, we normalize URLs by stripping query strings, ensuring that image.jpg?v=1 and image.jpg?v=2 are treated as the same cache key for image.jpg. We then specifically return (lookup) for requests ending in common static asset extensions. Any other request type will return (pass) to the origin.
The vcl_hit subroutine is executed when Fastly finds a matching object in its cache. It checks if the cached object is still fresh (less than 1 hour old based on obj.last_modified) and if so, return (deliver) to send it directly to the client. If it’s stale, it return (fetch) to revalidate with the origin.
vcl_miss is called when the requested object isn’t in the cache. It simply return (fetch) to retrieve it from the origin.
Finally, vcl_fetch runs after Fastly has fetched an object from the origin. We ensure that if the origin doesn’t specify caching headers (Cache-Control or Expires), we set a default max-age of 1 hour (3600 seconds) using set resp.http.Cache-Control = "public, max-age=3600";. This tells Fastly and intermediate caches how long to keep the object. Then, return (deliver) sends the fetched object to the client.
The most surprising thing about Fastly’s VCL is that it’s not just a passive configuration; it’s a full-fledged programmatic layer where you can inject custom logic, transform requests and responses, and even run code written in other languages.
Consider a real-time analytics scenario. Instead of sending every user interaction back to your origin, you can use VCL to aggregate, sample, or even transform that data at the edge before sending it to a dedicated analytics endpoint. For example, you could use vcl_log to send a subset of requests to a logging service:
sub vcl_log {
# Sample 10% of requests for logging.
if (random() < 0.1) {
log "Request: " + req.url + " from " + client.ip;
}
return (ok);
}
This VCL snippet, when placed in your configuration, will log a message for only 10% of incoming requests. The random() function generates a floating-point number between 0.0 and 1.0, and we compare it against our desired sampling rate (0.1 for 10%). This allows you to reduce the load on your logging infrastructure while still getting a representative sample of your traffic.
The VCL subroutines like vcl_recv, vcl_hit, vcl_miss, vcl_fetch, and vcl_log are hooks into Fastly’s request processing pipeline. You can modify requests before they hit the cache, decide how to handle cache hits and misses, control what gets cached and for how long, and even log or process data at the edge. The backend definition tells Fastly where your origin server lives, and directives like return (pass), return (lookup), return (fetch), and return (deliver) control the flow of the request through the Fastly network.
A critical, and often overlooked, aspect of VCL is its strict parsing. Fastly’s VCL compiler is very particular about syntax. For instance, if you try to use a variable that hasn’t been declared or assigned a value within the current scope, you’ll get a compilation error. This strictness, while sometimes frustrating, prevents unexpected behavior and ensures your edge logic behaves predictably. It’s not uncommon to spend time debugging what seems like a simple typo or a missing semicolon, only to find that the VCL compiler caught a subtle logic flaw before it ever reached production.
The next step after mastering basic caching and request routing is exploring how to implement advanced features like A/B testing or personalized content delivery using VCL.