Recv, Fetch, and Deliver Fundamentals /
Varnish Configuration Language ( VCL )

18/03/2024

Varnish state machine flow

When a request comes into Fastly, it’s assigned out from a load balancer to one of the nodes on the POP, and that will act as the delivery node.

Then there is a domain lookup where the request host header, the value there, say example.com is looked up on a giant table on the node to see which service configuration has that matching domain. The attached service configuration is then run and this is what we’re looking at here is all the subroutines that make up that service configuration.

We assume that there’s caching that’s attempting to go on with this request. So starting on our left, when a request comes in, receive is the first subroutine to run. We can follow that dash line as it goes from receive to hash.

After hash is the hash table lookup on the node to see if the item is already cached.

And then it’ll split either between hit or miss

This becomes a decision point:

After we run fetch we have an other process and this is where we actually cache or don’t cache the response object. Either way, whatever comes out of fetch, whether we cache it or not is handed off to then deliver, the deliver subroutine runs, and then we send off the actual response to the end user.

VCL_recv

This is our first subroutine, it’s the first point of contact and it’s used for :

VCL_recv is the best place to make those changes to standardize client’s inputs It’s very similar to a an Node.js express Middleware.

How request come and exit from VCL_recv subroutine ?

How they enter in VCL_recv ?

How they exit in VCL_recv ?

Where requests go after VCL_recv ?

After receive all requests go to hash, but after they go to hash, they are all going to end up at different places.

Default path : Hash then cache table lookup then eihter a cache HIT or cache MISS

Calling Pass in receive : Hash which generates hash key goes to that hash table, but then goes to pass.

Error : Hash ad then straight to error ( as we don’t want to cache errors )

The VCL_recv subroutine in details


sub vcl_recv { #--FASTLY RECV BEGIN if (req.restarts == 0) { if (!req.http.X-Timer) { set req.http.X-Timer = "S" time.start.sec "." time.start.usec_frac; } set req.http.X-Timer = req.http.X-Timer ",VS0"; } declare local var.fastly_req_do_shield BOOL; set var.fastly_req_do_shield = (req.restarts == 0); # default conditions # end default conditions #--FASTLY RECV END if (req.request != "HEAD" && req.request != "GET" && req.request != "FASTLYPURGE") { return(pass); } return(lookup); }

Let’s analyze this code :

    if (req.restarts == 0) {
        if (!req.http.X-Timer) {
        set req.http.X-Timer = "S" time.start.sec "." time.start.usec_frac;
        }
        set req.http.X-Timer = req.http.X-Timer ",VS0";
    }

Here is the inclusion of a timer header req.http.X-Timer

This is used for our timing metrics. We add it at the very beginning once we get it in receive, and it gets tied up later on in deliver at the end. This is only initialized, if the number of restarts is zero.

declare local var.fastly_req_do_shield BOOL;
set var.fastly_req_do_shield = (req.restarts == 0);

Then we have some shield variables that are being declared and set.

  if (req.request != "HEAD" && req.request != "GET" && req.request != "FASTLYPURGE") {
      return(pass);
   }

If the request method is not a head, and the request method is not a get, and the request is not a Fastly purge call, then it’ll return pass. So if it’s not any of these three things, we are not going to cache it, we call the pass behavior.

return(lookup);

Then at the end of the receive subroutine, we have our default transition call, which is returned lookup.

VCL_fetch

How requests enter in VCL_fetch ?

We go to fetch after a PASS or a MISS, if it’s a PASS then we should get the data from our backend. Same thing for MISS, as we don’t have it in the cache we have to grab the data from our backend and then put it in the cache.

VCL_fetch it’s used for :

When we get that backend response before fetch runs, Varnish looks for the presence of any cache control headers that Fastly considers valid. If we find them and we find values that we can use, we will use them to set the TTL for this particular response object.

How requests exit in VCL_fetch ?

To exit VCL fetch, the default transition is return deliver.

By the way we are expecting to cache the responses that we get from the backend, and this would be the default transition to do so.

The VCL_fetch routine in details


sub vcl_fetch { declare local var.fastly_disable_restart_on_error BOOL; #--FASTLY FETCH BEGIN # record which cache ran vcl_fetch for this object and when set beresp.http.Fastly-Debug-Path = "(F " server.identity " " now.sec ") " if(beresp.http.Fastly-Debug-Path, beresp.http.Fastly-Debug-Path, ""); # generic mechanism to vary on something if (req.http.Fastly-Vary-String) { if (beresp.http.Vary) { set beresp.http.Vary = "Fastly-Vary-String, " beresp.http.Vary; } else { set beresp.http.Vary = "Fastly-Vary-String, "; } } #--FASTLY FETCH END if (!var.fastly_disable_restart_on_error) { if ((beresp.status == 500 || beresp.status == 503) && req.restarts < 1 && (req.request == "GET" || req.request == "HEAD")) { restart; } } if(req.restarts > 0 ) { set beresp.http.Fastly-Restarts = req.restarts; } if (beresp.http.Set-Cookie) { set req.http.Fastly-Cachetype = "SETCOOKIE"; return (pass); } if (beresp.http.Cache-Control ~ "private") { set req.http.Fastly-Cachetype = "PRIVATE"; return (pass); } if (beresp.status == 500 || beresp.status == 503) { set req.http.Fastly-Cachetype = "ERROR"; set beresp.ttl = 1s; set beresp.grace = 5s; return (deliver); } if (beresp.http.Expires || beresp.http.Surrogate-Control ~ "max-age" || beresp.http.Cache-Control ~"(?:s-maxage|max-age)") { # keep the ttl here } else { # apply the default ttl set beresp.ttl = 3600s; } return(deliver); }

Let’s explore the basic code in VCL_fetch

set beresp.http.Fastly-Debug-Path = "(F " server.identity " " now.sec ") " if(beresp.http.Fastly-Debug-Path, beresp.http.Fastly-Debug-Path, "");

Fastly debug headers can collect and expose that information when you send those headers as a request header. It’ll add in some additional response debugging headers.

To get this header in your response call one of you url with this header : 'Fastly-Debug': '1'

if (req.http.Fastly-Vary-String) {
    if (beresp.http.Vary) {
      set beresp.http.Vary = "Fastly-Vary-String, "  beresp.http.Vary;
    } else {
      set beresp.http.Vary = "Fastly-Vary-String, ";
    }
  }

A generic mechanism to check if your backend uses the Vary header which is used when the reponse vary based on something ( User-agent for example )

 if (!var.fastly_disable_restart_on_error) {
    if ((beresp.status == 500 || beresp.status == 503) && req.restarts < 1 && (req.request == "GET" || req.request == "HEAD")) {
      restart;
    }
  }

If you get a 500 or a 503 and the number of restarts is less than one and the request type is a get or a head, then it will cause a restart to happen.
This mechanism will be triggered only once (req.restarts < 1)

  if(req.restarts > 0 ) {
    set beresp.http.Fastly-Restarts = req.restarts;
  }

If the number of restarts is greater than zero. So we’ve already restarted. Then we’re going to set a backend response header for Fastly restarts equal to the number of restarts

  if (beresp.http.Set-Cookie) {
    set req.http.Fastly-Cachetype = "SETCOOKIE";
    return (pass);
  }

If there is a set cookie header set by the backend. Then this response is unique to the user and we don’t cache it.

 if (beresp.http.Cache-Control ~ "private") {
    set req.http.Fastly-Cachetype = "PRIVATE";
    return (pass);
  }

If control header has a value of private then we pass.

  if (beresp.status == 500 || beresp.status == 503) {
    set req.http.Fastly-Cachetype = "ERROR";
    set beresp.ttl = 1s;
    set beresp.grace = 5s;
    return (deliver);
  }

If the response from the backend is an error a 500 or a 503 then we’re cashing them for just a second set beresp.ttl = 1s;

This one second gap where we temporarily serve this from cache can help offload the strain to that origin and help it to recover and that’s why we just do a return deliver because we are caching that response, but only for one second.

  if (beresp.http.Expires || beresp.http.Surrogate-Control ~ "max-age" || beresp.http.Cache-Control ~"(?:s-maxage|max-age)") {
    # keep the ttl here
  } else {
        # apply the default ttl
    set beresp.ttl = 3600s;
  }

So if there’s an expires header, if there’s a surrogate control value with max age or a cache control value with either surrogate max age or max age, that implies that we would be able to pull a TTL out of those values and we would keep the TTL

return(deliver);

Then if none of these other conditions trigger a return pass or return deliver, we always have our default return deliver without any condition at the end of the subroutine.

VCL_deliver

Deliver is the last set routine before the log subroutine runs. Deliver runs before the first byte of the response is sent to the user. It’s also the last chance to modify your response, headers…

Deliver runs on every single response no matter if the response comes from the cache etc…

It’s an ideal place for adding debugging information or user specific session data that cannot be shared with other users ( adding debug headers…)

How requests enter in VCL_deliver ?

How requests exit in VCL_deliver ?

Let’s explore the VCL for deliver :


sub vcl_deliver { #--FASTLY DELIVER BEGIN # record the journey of the object, expose it only if req.http.Fastly-Debug. if (req.http.Fastly-Debug || req.http.Fastly-FF) { set resp.http.Fastly-Debug-Path = "(D " server.identity " " now.sec ") " if(resp.http.Fastly-Debug-Path, resp.http.Fastly-Debug-Path, ""); set resp.http.Fastly-Debug-TTL = if(obj.hits > 0, "(H ", "(M ") server.identity if(req.http.Fastly-Tmp-Obj-TTL && req.http.Fastly-Tmp-Obj-Grace, " " req.http.Fastly-Tmp-Obj-TTL " " req.http.Fastly-Tmp-Obj-Grace " ", " - - ") if(resp.http.Age, resp.http.Age, "-") ") " if(resp.http.Fastly-Debug-TTL, resp.http.Fastly-Debug-TTL, ""); set resp.http.Fastly-Debug-Digest = digest.hash_sha256(req.digest); } else { unset resp.http.Fastly-Debug-Path; unset resp.http.Fastly-Debug-TTL; unset resp.http.Fastly-Debug-Digest; } # add or append X-Served-By/X-Cache(-Hits) { if(!resp.http.X-Served-By) { set resp.http.X-Served-By = server.identity; } else { set resp.http.X-Served-By = resp.http.X-Served-By ", " server.identity; } set resp.http.X-Cache = if(resp.http.X-Cache, resp.http.X-Cache ", ","") if(fastly_info.state ~ "HIT(?:-|\z)", "HIT", "MISS"); if(!resp.http.X-Cache-Hits) { set resp.http.X-Cache-Hits = obj.hits; } else { set resp.http.X-Cache-Hits = resp.http.X-Cache-Hits ", " obj.hits; } } if (req.http.X-Timer) { set resp.http.X-Timer = req.http.X-Timer ",VE" time.elapsed.msec; } # VARY FIXUP { # remove before sending to client set resp.http.Vary = regsub(resp.http.Vary, "Fastly-Vary-String, ", ""); if (resp.http.Vary ~ "^\s*$") { unset resp.http.Vary; } } unset resp.http.X-Varnish; # Pop the surrogate headers into the request object so we can reference them later set req.http.Surrogate-Key = resp.http.Surrogate-Key; set req.http.Surrogate-Control = resp.http.Surrogate-Control; # If we are not forwarding or debugging unset the surrogate headers so they are not present in the response if (!req.http.Fastly-FF && !req.http.Fastly-Debug) { unset resp.http.Surrogate-Key; unset resp.http.Surrogate-Control; } if(resp.status == 550) { return(deliver); } #default response conditions #--FASTLY DELIVER END return(deliver); }

Let’s explore the deliver sub routine in details

  if (req.http.Fastly-Debug || req.http.Fastly-FF) {
    set resp.http.Fastly-Debug-Path = "(D " server.identity " " now.sec ") "
       if(resp.http.Fastly-Debug-Path, resp.http.Fastly-Debug-Path, "");
    set resp.http.Fastly-Debug-TTL = if(obj.hits > 0, "(H ", "(M ")
       server.identity
       if(req.http.Fastly-Tmp-Obj-TTL && req.http.Fastly-Tmp-Obj-Grace, " " req.http.Fastly-Tmp-Obj-TTL " " req.http.Fastly-Tmp-Obj-Grace " ", " - - ")
       if(resp.http.Age, resp.http.Age, "-")
       ") "
       if(resp.http.Fastly-Debug-TTL, resp.http.Fastly-Debug-TTL, "");
    set resp.http.Fastly-Debug-Digest = digest.hash_sha256(req.digest);
  } else {
    unset resp.http.Fastly-Debug-Path;
    unset resp.http.Fastly-Debug-TTL;
    unset resp.http.Fastly-Debug-Digest;
  }

Here we set 3 Fastly debug headers :

resp.http.Fastly-Debug-Path : show which nodes the request used ( the delivery and fetch node)

resp.http.Fastly-Debug-TTL : Includes the TTL information

resp.Fastly-Debug-FF : If shielding is on


# add or append X-Served-By/X-Cache(-Hits) { if(!resp.http.X-Served-By) { set resp.http.X-Served-By = server.identity; } else { set resp.http.X-Served-By = resp.http.X-Served-By ", " server.identity; } set resp.http.X-Cache = if(resp.http.X-Cache, resp.http.X-Cache ", ","") if(fastly_info.state ~ "HIT(?:-|\z)", "HIT", "MISS"); if(!resp.http.X-Cache-Hits) { set resp.http.X-Cache-Hits = obj.hits; } else { set resp.http.X-Cache-Hits = resp.http.X-Cache-Hits ", " obj.hits; } }

« `X-Served-By« «  just tells you which node the request passed through. The X cache is going to tell you whether or not it was a hit or a miss. When we looked in cache, did we already have it? Was it hit? If not, was it a miss?

NB : For the X cache response header are hit or miss even when a PASS behavior is called it gets reported out as a miss here on this X cache header

Keep in mind : Miss might be hiding a pass !

http.X-Cache-Hits: The delivery node will say how many times it’s had a hit for this particular object coming out of cache. Keep in mind the delivery nodes gets load balanced across the POP so the X cache hits cna be different.

  if (req.http.X-Timer) {
    set resp.http.X-Timer = req.http.X-Timer ",VE" time.elapsed.msec;
  }

Pulls in the initial X timer value (from VCL_receive) and adds in the time elapsed in milliseconds


# VARY FIXUP { # remove before sending to client set resp.http.Vary = regsub(resp.http.Vary, "Fastly-Vary-String, ", ""); if (resp.http.Vary ~ "^\s*$") { unset resp.http.Vary; } }

if (resp.http.Vary ~ "^\s*$") : if the vary header contains nothing

unset resp.http.X-Varnish;

This is an internal diagnostic header, so it’s just being unset

  # Pop the surrogate headers into the request object so we can reference them later
  set req.http.Surrogate-Key = resp.http.Surrogate-Key;
  set req.http.Surrogate-Control = resp.http.Surrogate-Control;


  # If we are not forwarding or debugging unset the surrogate headers so they are not present in the response
  if (!req.http.Fastly-FF && !req.http.Fastly-Debug) {
    unset resp.http.Surrogate-Key;
    unset resp.http.Surrogate-Control;
  }

Here we put Surrogate-Key and Surrogate-Control from the response ( resp) to the request.

So it can be used by an other POP if this one is not the last one ( if there’s a shield after that one ).

What the doc says about http.Faslty-FF : this header as a simple mechanism to determine whether the current data center is acting as an edge or a shield

VCL deliver conclusion :

To sum it up, VCL deliver completes entirely before sending the response to the user. It occurs for each response separately. This is where you can include debugging details and session data specific to the user that shouldn’t be shared with others. It’s also your final opportunity to restart. The key transition calls here are return, deliver, and restart.

Takeaways