You appear to be offline. Some site functionality may not work.
Prueba Call Us

Geofencing

Your site is available only in certain regions, or offers content that varies between regions. Whether it's at the country level or down to the square kilometer, Fastly's geolocation data offers a way to group and route traffic in a regionally specific way.

  • LEARN

    Start coding from the instructions below. Make the tests pass!

  • PLAY

    Try out a fully functional example. Just press play.

  • USE

    Add this solution to a service in your Fastly account

Illustration of pattern concept

Instructions

Whether you want to separate the US, European and Asian editions of your website, block access to content from regions for which you don't have distribution rights, show content that is hyper-local to the user's physical location, or any combination of these, you are constructing a geofence. In this solution pattern, we'll explore a generic solution that accomodates a myriad of use cases.

You'll quickly see that for simple use cases (eg. 'block all requests from Denmark') much of the solution can be skipped, but the principles remain the same.

  1. Make sure the solution executes in the right place

    Fastly services support a number of features that can cause subroutines to be executed more than once, such as shielding and restarts. You will want to ensure that your code only runs once. Start with this code at the end of recv:

    RECV
    # Only enable...
    if (
      fastly.ff.visits_this_service == 0 && # on edge nodes (not shield)
      req.restarts == 0 # on first VCL pass
    ) {
    
      # Remainder of this tutorial's RECV code goes here
    
    }

    This code checks that the request is not on a shield machine, and that it is on a first pass though the configuration (prior to any restart). You could also use this opportunity to add an on/off switch linked to a key in a configuation table.

    Now that you have sufficient guardrails in place, you can add the logic inside of the IF block that you just created.

  2. Expose Fastly-provided geolocation data to your origin

    Within the configuration of a Fastly service, geographic information about the requesting client is available in a number of different variables. These divide up requests into buckets based on well known classifiers of various levels of granularity:

    • Continent: client.geo.continent provides a convenience value based on an arbitrary classification of countries into continents.
    • Country: client.geo.country_code offers ISO 3166-1 alpha-2 country classification.
    • Point: client.geo.latitude and client.geo.longitude identify the precise point which represents our best guess of the request's origin. This is not the current device location, although a mobile app could use the device’s location services to supply these.

    The classification used by client.geo.continent is geographic, not political, therefore 'EU' refers to an arbitrary definition of the continent of Europe and not to the political entity of the European Union

    The simplest thing you can do is therefore to pass the available location data to your origin. Add this to the recv subroutine:

    RECV
      set req.http.client-geo-country = client.geo.country_code;
      set req.http.client-continent = client.geo.continent_code;
  3. Use the location data on your origin

    The information about the user's region is presented to your origin server in the HTTP header - client-geo-country, for example. You can use this information to adjust the response you generate.

    It's vital that you tell Fastly whether you used the region information when you generated the response. Requests for things like images or scripts might well deliver the same content to a user regardless of whether they're in Bangalore or Birmingham, so you don't want Fastly to cache those separately. But you do want us to cache separately if the responses are tuned according to the location specified in the request. You can do that using a Vary header:

    Vary: client-geo-country

    Now, Fastly needs to store a separate variation of the resource for every possible value of client-geo-region. In practice, we'll only store the most popular few hundred, in each data center, which is fine for countries or continents.

  4. Group countries into custom regions

    Probably the most useful and flexible way to use geotargeting, this approach takes a list of possible values from the variable of your choice and maps them into a (usually smaller) set of region names, including a default 'catch-all' region. This is usually only useful if you are targeting countries, but is especially useful for countries because they vary wildly in size. You almost certainly don't want users in Vatican City (VA, population 1,000) to be unable to benefit from content that we've cached as a result of requests from Italy (IT, population 60 million).

    Start by defining the mapping in the init space:

    INIT
    table region_defs {
      "AT": "region-1",
      "BE": "region-1",
      "BG": "region-1",
      "HR": "region-1",
    
      "IT": "region-2",
      "VA": "region-2",
    
      "NO": "region-3",
      "CN": "region-3",
    
      "_default": "region-other"
    }

    Now you can create another header to add to your origin request in the recv subroutine, after your previous code:

    RECV
      set req.http.client-geo-region = table.lookup(
        region_defs,
        req.http.client-geo-country,
        table.lookup(region_defs,"_default")
      );

    You are laying out this information efficiently in a VCL table. When you ship your solution, you can then manage the data independently of your configuration using an edge dictionary.

  5. Allow regions to be blocked

    If you are creating custom regions, then you have the opportunity to set one of those region names to 'blocked' or some other special string that you can use to identify regions from which requests will not be accepted. If you intend to potentially make use of this, you'll need to support blocked regions in your VCL. Add the following to your recv code:

    RECV
      if (req.http.geo-region-id == "blocked") {
        error 618 "geofence:blocked";
      }

    And catch the error in the error subroutine, to generate a synthetic response:

    ERROR
    if (obj.status == 618 && obj.response == "geofence:blocked") {
      set obj.status = 403;
      set obj.response = "Forbidden";
      set obj.http.content-type = "text/plain";
      synthetic "Sorry, our service is currently not available in your region";
      return (deliver);
    }

    This pattern, known as a 'synthetic response', involves triggering an error from somewhere else in your VCL, catching it in the error subroutine, and then converting the error obj into the response that you want to send to the client. To make sure you trap the right error, it's a good idea to use a non-standard HTTP status code in the 6xx range, and also to set an error 'response text' as part of the error statement. These two pieces of data then become obj.status and obj.response in the error subroutine. Checking both of them will ensure you are trapping the right error.

    Once you know you are processing the error condition that you triggered from your earlier code, you can modify the obj to create the response you want. Normally this includes some or all of the following:

    • Set obj.status to the appropriate HTTP status code
    • Set obj.response to the canonical HTTP response status descriptor that goes with the status code, eg "OK" for 200 (this feature is no longer present in HTTP/2, and has no effect in H2 connections)
    • Add headers using obj.http, such as obj.http.content-type
    • Create content using synthetic. Long strings, e.g. entire HTML pages, can be included here using the {"...."} syntax, which may include newlines.
    • Exit from the error sub by explicitly performing a return(deliver)

    Now change some of your region mappings to be 'blocked' as needed:

    INIT
      ...
      "NO": "blocked",
      ...
  6. Use lat/long coordinates as a grid

    If you pick something that comes as a single value, like country or continent code, the solution is pretty simple, as we've seen above. However, if your input is a latitude and longitude, you need to reduce these to a single value that we can use as a cache key. And since there are an infinite number of possible lat/long combinations, some rounding is essential to provide any hope of caching the response from your origin servers.

    Start by 'snapping' lat/long coordinates to a grid. This requires choosing a granularity for your grid in degrees. 0.1 decimal degrees of longitude or latitiude is about 11km in distance on the surface of the Earth, and encloses an area of about 123 square kilometres. Although there are still 13 million of these covering the whole planet, most of that space is ocean or sparsely inhabited.

    Fastly currently only provides service on one planet, but in future you may need to account for the radius of the planet when calculating the area enclosed by each grid square.

    The conversion of the coordinates to a grid requires some temporary variables, so it is cleaner to do this in a custom subroutine. Add this code to the init space:

    INIT
    sub set_latlng {
      declare local var.chunk_size FLOAT;
      declare local var.lat FLOAT;
      declare local var.long FLOAT;
    
      set var.chunk_size = 0.1;
      set var.lat = client.geo.latitude;
      set var.long = client.geo.longitude;
    
      set var.lat /= var.chunk_size;
      set var.lat = math.floor(var.lat);
      set var.lat *= var.chunk_size;
    
      set var.long /= var.chunk_size;
      set var.long = math.floor(var.long);
      set var.long *= var.chunk_size;
    
      set req.http.client-geo-latlng = var.lat + ", " + var.long;
    }

    The inputs are the latitude and longitude of the client user (in degrees, a number between 0 and 360), and a chunk size (the number of degrees from the north to south and east to west of a grid square). Latitude and longitude are then snapped to the grid by first dividing the input by the chunk size to get the grid index (we use math.floor because the grid index must be a whole number), and then multiplying by the chunk size to get the coordinate of the start of the grid square. The resulting lat/long reference is a point at the north-west corner of the grid square.

    Now call the custom sub at the end of the recv subroutine:

    RECV
    call set_latlng;

    Fastly can store up to a few hundred variants of the same cache key, per data center. Requests from the same location will tend to cluster in their closest data center, meaning the variants in each data center will likely be different and specific to that general area. At a resolution of 0.1 degrees per grid square, that should provide a reasonable cache hit ratio.

    Instead of using variants of a cache key, it would be possible to actually include the geolocation in the cache key itself. This would be a more scalable solution but makes it harder to purge objects from cache later. If you need a higher resolution, consider combining manipulation of the cache key with surrogate key tagging.

  7. Allow the location to be overridden by the client

    Fastly's location data is good, but ultimately cannot be more than a best guess as to the user's location, and for users on wireless connections could be very inaccurate. You might be better off asking them directly. If your client application is a website, consider using the web platform's Geolocation API and setting the result into a cookie. If your client is a native app, you may be able to use the platform's SDK to access geolocation data and send it in a custom header.

    Of course, if the purpose of your geofence is to block certain regions from accessing your site, you should not provide the ability for the client to override the input, but if your intention is to provide region-localized content, especially at a granular level using latitude and longitude, you should definitely consider allowing the client to override the location value. Try this in your set_latlng function:

    INIT
      if (req.http.cookie:latlng ~ "^([\d\.]+),\s*([\d\.]+)$") {
        set var.lat = std.atof(re.group.1);
        set var.long = std.atof(re.group.2);
      } else {
        set var.lat = client.geo.latitude;
        set var.long = client.geo.longitude;
      }
  8. Force geo-vary for some content types

    It's much better for you to control cache variation by sending the Vary header from origin. However, if you can't do that, you can also add the vary rule in VCL. But remember if you do this, it will apply to all requests, not just the ones where location is important.

    FETCH
    add beresp.http.vary = "client-geo-region";

Next steps

While it does not identify a user's physical location, you could look into using the Autonomous System number, which we expose in VCL as client.as.number, as a proxy for grouping users by ISP. Other alternative geographic identifiers include the Fastly data center that is handling the request (server.datacenter) and other variables based on the user's IP address, such as postal code (client.geo.postal_code), ISO-3166 subdivision (client.geo.region) or time zone (client.geo.gmt_offset).

See also

Reference docs:

Blog posts:

Quick install

This solution can be added directly to an existing service in a Fastly account as a set of VCL snippets. The embedded fiddle below shows the complete solution. Feel free to run it, and click the 'INSTALL' tab to customise and upload it to your service:

Click to view the fiddle code

Once you have the code in your service, you can further customise it if you need to.