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

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 via a few steps:

  1. Identify a source of location data
  2. Normalize the value to reduce cache fragmentation
  3. Map the value to a set of custom regions (optional)
  4. Block any that you want to block
  5. Pass the resulting region name to your origin server and support caching the responses for different regions separately

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. Define some variables

    This solution requires a few custom variables to be defined at the top of the recv subroutine:

    RECV
    declare local var.sourceType STRING;
    declare local var.mapped STRING;

    We'll come on to why we need these in a minute.

  2. 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.

  3. Choose a source of location data

    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 an 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 proprietary and if precision is needed we recommend using a custom mapping of client.geo.country_code to continent name based on your own preferred definition of continents.

  4. Normalise the raw location data

    If you pick something that comes as a single value, like country or continent code, the starting point is pretty simple: assign the appropriate variable to a custom HTTP header. 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.

    The following code supports all the common location data sources, but you can remove the if conditions and just include the one you are using, if you like:

    RECV
      set var.sourceType = "latlng";  # Choose 'latlng', 'country' or 'continent'
    
      if (var.sourceType == "country") {
        set req.http.geo-region-id = client.geo.country_code;
    
      } else if (var.sourceType == "continent") {
        set req.http.geo-region-id = client.geo.continent_code;
    
      } else if (var.sourceType == "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.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.geo-region-id = var.lat + "," + var.long;
      }

    If you are using latitude and longitude, caching is an immediate problem, because the number of permutations of possible latitudes and longitudes is infinite. To acheive some measure of cache efficiency, you will need to 'snap' lat/long co-ordinates 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, 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 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.

    Let's look at how we normalise the infinite range of co-ordinates into a finite number of grid squares. 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 co-ordinate of the start of the grid square. The resulting lat/long reference is a point at the north-west corner of the grid square.

    If you are using countries, the number of those already lines up well with the number of variations we can store. And there are only seven continents, so that's certainly no problem.

  5. Optional: 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. 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-localised content, especially at a granular level using latitude and longitude, you should definitely consider allowing the client to override the location value:

    RECV
      if (req.http.cookie:geo-region-id) {
        set req.http.geo-region-id = req.http.cookie:geo-region-id;
      }
  6. Optional: Group countries into custom regions and define blocked 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. This allows three possible outcomes for any given location: a specific region name, a default 'catch-all' region, or a block directive. This is usually only useful if you are targeting countries. 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",
    
      "GF": "region-2",
      "GP": "region-2",
      "MQ": "region-2",
      "ME": "region-2",
    
      "NO": "blocked",
      "CN": "blocked",
    
      "_default": "region-other"
    }

    Then perform the lookup in the recv subroutine, after your previous code:

    RECV
      if (var.sourceType == "country") {
        set var.mapped = table.lookup(
          region_defs,
          req.http.geo-region-id,
          table.lookup(region_defs, "_default")
        );
        if (var.mapped) {
          set req.http.geo-region-id = var.mapped;
        }
      }
    

    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.

  7. Optional: deal with blocked regions

    If your region identifier is being created by mapping input location data to a set of predefined region names, 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:

    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 7xx 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. Normallty 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)
  8. Use the location data on your origin

    The information about the user's region is presented to your origin server in the HTTP header - geo-region-id in our example code here. You can use this information to adjust the response you generate.

    It's vital that you tell Fastly whether you paid any attention to 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: geo-region-id

    Now, Fastly needs to store a separate variation of the resource for every possible region ID. In practice, we'll only store the most popular few hundred, in each data center.

  9. Optional: 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 = "geo-region-id";

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 to your needs, but if you keep it unmodified, it will be eligible for automatic upgrades if this recommended solution is improved in future.

All code provided through the Fastly Developer Library is provided under both the BSD and MIT open source licenses.

Get in touch

Help us make this resource more useful for the entire Fastly community. Email your questions, requests, and big ideas to developers@fastly.com — or reach out and let us know what you’re working on.