---
title: VCL best practices
summary: null
url: >-
  https://www.fastly.com/documentation/guides/full-site-delivery/fastly-vcl/vcl-best-practices
---

As a service grows, small choices about your VCL add up. This guide covers common patterns and pitfalls when writing Fastly VCL, with guidance on how to avoid subtle bugs and keep your configuration maintainable.

For caching mechanics, request flow, and shielding, see [Managing caching and request flow in VCL](https://www.fastly.com/documentation/guides/full-site-delivery/fastly-vcl/managing-caching-and-request-flow). For optimization techniques specific to programmatically generated VCL, see [Optimizing generated VCL](https://www.fastly.com/documentation/guides/full-site-delivery/fastly-vcl/optimizing-generated-vcl).

## Organize and comment your code

CDN services can quickly become hard to maintain as you add more and more logic to them. For any repetitive code, consider using [custom subroutines](https://www.fastly.com/documentation/reference/vcl/subroutines/#custom-subroutines) with descriptive names to help explain your code. Use comments to reference ticket numbers or URLs (for example, Zendesk, Jira, or GitHub issues), to make it easier for those who come to the code in future to understand the context:

```vcl
# https://mycompany.atlassian.net/browse/PROJECT-935
sub add_system17_auth_header {
  if (table.contains(credentials, "system17_token")) {
    set bereq.http.authorization = "Bearer " + table.lookup(credentials, "system17_token");
  }
}

sub vcl_miss {
#FASTLY miss
  call add_system17_auth_header;
  return(fetch);
}

sub vcl_pass {
#FASTLY pass
  call add_system17_auth_header;
  return(pass);
}

sub vcl_fetch {
#FASTLY fetch

  # If the response is setting a cookie, make sure it is not cached
  if (beresp.http.Set-Cookie) {
    return(pass);
  }

  set beresp.ttl = 2592000s; # 30 days
  return(deliver);
}
```

If you choose to use [custom VCL](https://www.fastly.com/documentation/guides/full-site-delivery/fastly-vcl/about-fastly-vcl/#custom-vcl), it is usually better to capture all your logic within custom VCL files rather than combining custom VCL with [VCL-generative objects](https://www.fastly.com/documentation/guides/full-site-delivery/fastly-vcl/about-fastly-vcl/#vcl-generative-objects) or [VCL snippets](https://www.fastly.com/documentation/guides/full-site-delivery/fastly-vcl/about-fastly-vcl/#vcl-snippets).

## Don't use regsub for data extraction

Often you have a large lump of data in a single string. For example, you might have a session cookie that looks like this:

```text
uid=12345:sess=01234567-89abcdef-012:name=sjones:remember=1
```

It doesn't really matter what each of these tokens are or what format the string is in. The point is that you want to extract just one of them. A common but misguided way to do this is with the `regsub` function:

```vcl
set var.result = regsub(
  req.http.cookie:auth,
  "^.*?:name=([^:]*):.*?$", "\1"
);
```

This replaces the entire string with the text that appears after 'name=', up to but not including the next colon, and in this case, it would yield the result "sjones". However, if the format of the string is not what you expected, for example, because it contains less than three colons or does not have a `name=` section, then the return value is the _entire input string_. This is dangerous because you might start to leak data that you don't intend to.

### Solution 1: Use the if() function

The `if()` function provides a ternary construct that you can use along with regular expression capture variables:

```vcl
set var.result = if(
  req.http.cookie:auth ~ "^(?:.*:)?name=([^:]*):", # Expression
  re.group.1, # Value to return if true
  ""          # Value to return if false
);
```

This time, if the regex fails to match, the return value is an explicitly declared default (the empty string in the example above). The regex is also simpler because it's no longer necessary to anchor the pattern to both the beginning and end of the string.

### Solution 2: Use subfield()

If the input string is key-value pairs, as it is in the example, a better solution is to use `subfield()`:

```vcl
set var.result = subfield(req.http.cookie:auth, "name", ":");
```

## Use the `:` operator for cookies or other header subfields

In the example above, the `:` operator is used to access the cookie (`req.http.cookie:auth`). While `if` and `subfield` are good at extracting subfields from the content of a single cookie, extracting the cookie itself from the inbound `Cookie` header can take advantage of subfield-accessor syntax. If an HTTP header is in the common format (`key=value, key=value`), then use the colon in combination with the header name to access the subfield.

This also works well with headers such as `Cache-Control`, and can be used to write individual header fields, as well as read them:

```vcl
set resp.http.Cache-Control:max-age = "3600";
```

You can even use this syntax to add tokens to headers that are simple token lists (`key, key, key`), rather than key-value pairs, by setting the value to an empty string. For example, the `Vary` header is a comma separated list of other header names. You could add a header to the vary list without overwriting the existing ones:

```vcl
# If the Vary header value is "My-Header", then after the
# following statement runs, it will be "My-Header, Accept-Encoding"
set resp.http.Vary:Accept-Encoding = "";
```

## Write cookies without overwriting existing ones

To write cookies, construct a `Set-Cookie` header on the client response, normally in `vcl_deliver`. Using `set` will overwrite any existing header with the same name, so if you may be setting multiple cookies in the same response, use `add` instead. It's also wise, when setting cookies on a response, to prevent the client or any downstream entity from caching it.

If your service uses shielding, these headers are also added to the response from the shield POP to the edge POP, which will likely prevent the edge from caching it. To avoid this, restrict the header changes to the first Fastly node using `fastly.ff.visits_this_service`. See [Case 3: Modifying responses](https://www.fastly.com/documentation/guides/full-site-delivery/fastly-vcl/managing-caching-and-request-flow/#case-3-modifying-responses) for an example.

```vcl
add resp.http.Set-Cookie = "auth=52b93cff.165826435.d783dad8-ebb9-4475-b6fb-68ce83f90f12; max-age=86400; path=/";
set resp.http.Cache-Control = "private, no-store";
```

## Test for empty strings explicitly

In many programming languages, implicit casting of a string to a boolean will yield `false` if the string is one of a few 'falsy' values, like "0", an empty string, or null. In VCL, only strings that are _not set_ are falsy, so, for example, this does not do what you might think it does:

```vcl
if (req.url.qs) {
  # Do something if the request has a query string
  # (but actually this will ALWAYS EXECUTE)
}
```

The above code executes on all requests — because if the inbound request has no query string, `req.url.qs` is an empty string, which, when used in a `BOOL` context, is `true`.

Instead, check explicitly that it is not an empty string, or if you want a solution that considers both NULL and an empty string to be falsy, use the `std.strlen` function, which returns 0 in both cases:

```vcl
if (std.strlen(req.url.qs) > 0) {
  # Do something if the request has a query string
}
```

## Use Accept-Language for language selection

Often Fastly customers offer content in a number of languages, and want to make life easier for users by delivering their preferred language automatically. But we sometimes see that being done using our geolocation features:

```vcl
# Assume badly that everyone in Mexico reads Spanish and everyone in
# the United Kingdom reads English
if (client.geo.country_code == "mx") {                  # Mexico
  set req.url = querystring.add(req.url, "lang", "es"); # ...Spanish
} else if (client.geo.country_code == "gb") {           # UK
  set req.url = querystring.add(req.url, "lang", "en"); # ...English
}
```

It's a reasonable bet that a user connecting to your site from Mexico would be able to read Spanish, but maybe the user is visiting from somewhere else, and using their hotel's WiFi. Just because they moved to a different country doesn't mean they suddenly speak its language.

Instead, use the `Accept-Language` header, which is set by browsers based on the language settings of the user's computer, and benefit from Fastly's support for normalization of the `Accept-Language` format:

```vcl
set req.url = querystring.add(
  req.url,
  "lang",
  accept.language_lookup("en:de:fr:nl:es", "en", req.http.Accept-Language)
);
```

This code will read the end user's `Accept-Language` header, normalize it within a set of languages that your site supports, and then add the final choice to the query string.

Modifying the query string in this way will create separate cache keys for each language variant, and that can make content harder to purge from the cache. For a more complete solution to varying content by language, consider using a `Vary` header.
