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.