home

Blog


Caching your API at the edge with fine-grained purging controls


Caching API responses at the edge sounds great until you hit the invalidation problem. A TTL that's too short and your origin keeps getting hammered, too long and your users see stale data for who knows how long. If what you're looking for is to cache aggressively and purge surgically, the moment something changes and only what changed, there's a solution for that. Spoiler alert: the usual AWS suspects don't really compete here.

Putting your API behind Fastly

The setup is the usual reverse proxy dance: you create a Fastly service, point it to your API as the origin (or "host"), and move your DNS to Fastly. From that moment on, every request goes through their points of presence (POP) and cacheable responses get stored close to your users.

Now that caching is set up, what about the "fine-grained purging controls"? This is where the Surrogate-Key header comes in. Your origin can tag any response with one or more keys, space-separated, and Fastly will index the cached object under all of them:

HTTP/1.1 200 OK
Content-Type: application/json
Surrogate-Key: products product-42 category-shoes
// Surrogate-Control works as well
Cache-Control: max-age=86400

Think of it as labeling your cache. That response for GET /products/42 is now reachable by "everything product-related", "this specific product" and "everything in the shoes category" at the same time. Also, Fastly strips the header before sending the response to the client, so your users never see your labeling scheme. One key can be attached to thousands of objects, and one object can carry multiple keys, allowing for surgical purging later on.

Purging by keys

When product 42 gets updated, you don't need to guess which URLs contain it, or nuke the entire cache out of despair. You just purge the key:

curl -X POST "https://api.fastly.com/service/$SERVICE_ID/purge/product-42" \
  -H "Fastly-Key: $FASTLY_API_TOKEN"

Every cached object tagged with product-42 is invalidated across the whole network, usually in a few milliseconds. The listing page, the detail endpoint, that search response that happened to include it, all gone in one call while everything else stays warm.

There's a catch, though (of course). URL-based purges on Fastly work out of the box without authentication, so anyone who knows your endpoints could purge them and turn your cache into swiss cheese (rude, but possible). But don't worry, the fix is adding the header http.Fastly-Purge-Requires-Auth: "1" change at your service configuration.

With authenticated purging enabled, every purge request must carry a valid API token with purge permissions, otherwise it gets a 401 and goes home empty-handed.

Customizing further with VCL scripts

If the defaults aren't enough, Fastly lets you write custom VCL (Varnish Configuration Language) scripts that run at the edge, on every request and you can invoke them in hooks of the request lifecycle: vcl_recv when the request arrives, vcl_fetch when the origin responds, vcl_deliver before sending it back, and so on.

That opens a lot of possibilities. For instance, you can normalize query strings so ?a=1&b=2 and ?b=2&a=1 hit the same cache object, vary the cache by an API version header or even generate surrogate keys at the edge when your origin can't be bothered:

# Script injected in the "hash" hook to cache products based on page number
if (req.url.path ~ "^/products") {
    set req.hash += querystring.filter_except(querystring.sort(req.url, true), "page");
} else {
    set req.hash += querystring.remove(req.url);
}

set req.hash += req.http.host;
set req.hash += req.vcl.generation;

return (hash);

You can also cache things Fastly wouldn't cache by default (like POST bodies for search endpoints, if you know what you're doing) or strip cookies that would otherwise bust your cache. Basically, if the behavior you want isn't a checkbox, it's probably a few lines of VCL away.

What about CloudFront and API Gateway caching?

If your stack is on AWS, the natural instinct is to reach for CloudFront or the API Gateway cache. They'll cache your responses just fine, but the control story is a different beast.

CloudFront has no equivalent to surrogate keys. Its Invalidations are path-based, so to invalidate "everything related to product 42" you either track every URL that contains it (good luck with query strings) or throw a /* wildcard and wipe way more than you needed. On top of that, invalidations take minutes to propagate instead of milliseconds, and past the free monthly quota, each path costs money. Purging as part of your write path, the way you'd casually do with Fastly, just isn't how it was designed.

API Gateway is even more restrictive: the cache is a fixed-size instance you pay for hourly, and invalidation is either flushing the entire stage cache or letting clients send Cache-Control: max-age=0 to invalidate a single entry, which you then have to control with an IAM policy so random clients can't do it. No tags, no keys, no grouping, no edge programmability. It works for "cache this GET for 5 minutes", and that's about it.

None of this means these services are bad, they're just built for different use cases. If precise, event-driven invalidation is a requirement, they'll fight you the whole way. It's also worth mentioning that Fastly offers a free tier for their services, so you'll likely be able to set up a few projects without spending anything.

Wrap-up

Fine-grained purging is what turns edge caching from a risky optimization into something you can lean on: tag responses with Surrogate-Key, purge those keys from your write path the instant data changes, lock purging down with Fastly-Purge-Requires-Auth, and reach for VCL when the defaults fall short. Your origin gets quieter, your users get fast and fresh responses, and you stop choosing between the two.

Powered by Simple Blog API