Enable API caching with surrogate keys

Learn how to cache API responses at the Fastly edge using surrogate keys, improve performance, and purge cached data when it changes.

APIs often deliver dynamic content that changes frequently. Without caching, every request has to reach your origin, which can cause latency and load.

This tutorial shows you how to cache API responses at the Fastly edge using surrogate keys. Surrogate keys let you group related responses and invalidate them together, which is essential for APIs where data is often shared across multiple endpoints. By the end of this tutorial, you will have a working setup that caches API responses safely, improves performance, and keeps your data fresh.

Here's what you need:

  • an API domain name you control and an origin server that serves your API
  • a Fastly account with access to service configuration settings in the Fastly control panel

Step 1: Understand the API

Imagine an online magazine with articles that have comments. Each article can have many comments, and each comment is created by one user. We'll design a RESTful API specification and use it to manipulate and retrieve comments.

The API includes endpoints for basic operations:

  • GET /comments (return all comments)
  • GET /comments/:id (return a specific comment)
  • POST /comments (create a comment)
  • PUT /comments/:id (update a comment)
  • DELETE /comments/:id (delete a comment)

The create, read, update, and delete (CRUD) methods ensure the API can perform its basic operations, but they don't expose the relational aspect of the data. To do so, you would add a couple of relational endpoints:

  • GET /articles/:article_id/comments (comments for an article)
  • GET /users/:user_id/comments (comments by a user)

Endpoints like these allow programmers to get the information they need to do things like render the HTML page for an article, or display comments on a user's profile page. While there are many other possible endpoints we could construct, this set should suffice for the purposes of this tutorial.

As we continue to set things up, let's also assume that the API has been programmed to use an object-relational mapper (ORM), such as ActiveRecord, when interacting with the database.

Step 2: Identify cacheable endpoints

Start by identifying the URLs you want to cache. We recommend splitting the specification endpoints into two groups.

The first group, called accessors, retrieves or accesses the comment data. These are the endpoints you want to cache using Fastly. Using the example, four endpoints match this description:

  • GET /comments
  • GET /comments/:id
  • GET /articles/:article_id/comments
  • GET /users/:user_id/comments

The second group, called mutators, changes or mutates the comment data. These endpoints are always dynamic, and are therefore uncacheable. Using the example, three endpoints match this description:

  • POST /comments
  • PUT /comments/:id
  • DELETE /comments/:id

You should see a pattern emerging. Because the example API is RESTful, we can use a simple rule to identify the accessor and mutator endpoints: GET endpoints can be cached, but PUT, POST, and DELETE endpoints cannot.

When building your API responses for these accessor endpoints, add a Surrogate-Key header so you can later purge by key:

# In your controller or response builder
response.set_header(
"Surrogate-Key",
"comment-#{id} article-#{article_id} user-#{user_id} comments"
)

This groups responses by resource, so related endpoints can all be purged together when data changes.

Once you've gathered this information, you're ready to program the API to send PURGE requests.

Step 3: Prepare your API for purging

Don't be tempted to point at the PUT, POST, and DELETE endpoints as the place where data is modified. In most modern APIs, these endpoints represent an interface to the actual model code responsible for handling the database modifications.

In the example, we assumed that we'd be using an ORM to perform the actual database work. Most ORMs allow programmers to set special callbacks on models that will fire when certain actions have been performed (e.g., before or after validation, or after creating a new record).

For purging, we are interested in whether a model has saved information to the database — whether it's a new record, an update to an existing record, or the deleting of a record. That means we need to add a callback that tells the API to send a PURGE request to Fastly for each of the cacheable endpoints.

For an ActiveRecord comments model, you could do something like this:

class Comment < ApplicationRecord
after_commit :purge_related_cache
private
def purge_related_cache
keys = ["comment-#{id}", "article-#{article_id}", "user-#{user_id}", "comments"]
keys.each { |k| FastlyPurger.purge_key(k, soft: true) }
end
end

When creating the callback, keep in mind that these URLs are being purged because they have content that changes when a comment is changed. The purge code should be triggered after the information has been saved to the database, otherwise a race condition could be created where Fastly fetches the data from the origin server before the data has been saved to the database. This would result in caching the old data instead of the new data.

With the model code in place, the API is now ready to be cached. This requires setting up Fastly by creating a new service, adding the domain for the API to that service, and adding the origin server that powers the API to that service.

Step 4: Create a Fastly service for your API

Follow the instructions for creating a new service. You'll add specific details about your API server when you fill out the Create a new service fields:

  • In the Name field, enter a name for this service that helps you identify it's related to caching your API information (e.g., My API Service).
  • In the Domain field, enter the domain name associated with your API (e.g., api.example.com).
  • In the Address field, enter the IP address or hostname of your API server.

Your API is now reachable through Fastly.

Step 5: Add your API's domain to your Fastly service

Follow these instructions to add the API's domain name to your Fastly service:

  1. Click Edit configuration and then select the option to clone the active version.

  2. Click Create domain.

    The Create a domain page.

  3. Fill out the Create a domain fields as follows:

    • In the Domain Name field, enter the domain name for the API.
    • In the Comment field, enter an optional comment that describes your domain.
  4. Click Create. Your API's domain name appears in the list of domains.

Step 6: Add the origin where your API runs

Follow the instructions for working with hosts. You'll add specific details about your API server when you fill out the Create a host fields:

  • In the Name field, enter a name for the origin server that helps you identify it's related to caching your API information.
  • In the Address field, enter the IP address (or hostname) of the API server.

Step 7: Test end to end

  1. Request a cacheable endpoint twice. The first should return a MISS. The second should return a HIT in the X-Cache header.
  2. Create or update a comment in your application to trigger an after commit purge by key.
  3. Request the same endpoint again. Confirm the updated data is returned and that subsequent requests are served from cache.

Next steps