APIs for Edge Computing and CDN
A traditional API runs in one region (or a handful) and every client, no matter where they are in the world, pays the network latency of reaching it. Edge computing flips that: your code runs in dozens or hundreds of points of presence spread across the globe, physically close to the user making the request, and a content delivery network's routing layer sends each request to the nearest one. This chapter looks at what changes when your Go code runs at the edge instead of in a single data center, using Fastly's Compute platform — which runs WebAssembly and has an official Go SDK — as the concrete example.
Why the edge is a different execution model
A CDN edge node isn't a smaller version of your regular server; it's a fundamentally different runtime, usually a WebAssembly sandbox with strict limits on execution time (single-digit milliseconds to low seconds, not the open-ended lifetime of a normal HTTP handler) and no persistent local disk. Fastly's Compute platform compiles Go to WebAssembly via TinyGo and executes it in exactly this kind of sandboxed, short-lived environment, using the github.com/fastly/compute-sdk-go package instead of net/http directly:
package main
import (
"context"
"io"
"github.com/fastly/compute-sdk-go/fsthttp"
)
func main() {
fsthttp.ServeFunc(func(
ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request,
) {
if r.URL.Path == "/health" {
w.WriteHeader(fsthttp.StatusOK)
io.WriteString(w, "ok")
return
}
backendResp, err := r.Send(ctx, "origin")
if err != nil {
w.WriteHeader(fsthttp.StatusBadGateway)
return
}
defer backendResp.Body.Close()
w.Header().Set("X-Served-By", "edge")
w.WriteHeader(backendResp.StatusCode)
io.Copy(w, backendResp.Body)
})
}
fsthttp.ServeFunc replaces http.ListenAndServe entirely — there's no port to bind, no listener, because the platform itself is the server and invokes your compiled function per request. r.Send(ctx, "origin") forwards the request to a named backend configured outside your code (in Fastly's service configuration), which is how edge code typically talks to your actual origin server when it needs data it can't answer itself.
fsthttp mirrors net/http's shape closely enough to be readable, but the underlying execution model (WebAssembly, no goroutine scheduler in the usual sense, strict CPU-time budgets) means code that spawns background goroutines or expects to run past the response being sent will not behave the way it does on a normal server. Edge functions should do one focused thing per request and return.What edge code is actually good for
Edge functions excel at request/response transformations that don't need your full application: rewriting a URL before it reaches origin, adding security headers, serving a cached response entirely from the edge without a round trip to origin at all, running A/B test bucket assignment based on a cookie, or rejecting obviously malicious requests before they cost your origin anything. All of these share a property: they need little or no state beyond what's in the request itself, and they benefit enormously from running physically close to the user rather than in one central region.
fsthttp.ServeFunc(func(
ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request,
) {
bucket := abTestBucket(r.Header.Get("Cookie"))
r.Header.Set("X-AB-Bucket", bucket)
resp, err := r.Send(ctx, "origin")
if err != nil {
w.WriteHeader(fsthttp.StatusBadGateway)
return
}
defer resp.Body.Close()
w.Header().Set("Set-Cookie", "ab_bucket="+bucket)
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
})
This assigns and persists an A/B test bucket entirely at the edge, without your origin server ever needing to know the mechanism exists — origin just receives a header telling it which variant to render.
Caching at the edge
CDNs have always cached static assets; edge compute lets you cache dynamic API responses intelligently, using standard HTTP caching headers your Go origin already emits:
func productHandler(w http.ResponseWriter, r *http.Request) {
product := fetchProduct(r.PathValue("id"))
w.Header().Set("Cache-Control",
"public, max-age=60, stale-while-revalidate=300")
writeJSON(w, http.StatusOK, product)
}
stale-while-revalidate tells the CDN it can serve a slightly stale cached response immediately while it revalidates against origin in the background — a meaningful latency win for data that doesn't need to be perfectly fresh on every single request, like a product listing that changes rarely. The edge layer honors these headers without your edge function needing custom caching logic at all, as long as your origin sets them correctly.
Where the origin still matters
Edge computing doesn't replace your Go API running in a data center — it sits in front of it, handling the requests that don't need your full application logic and forwarding everything else to origin exactly as this chapter's first example does. The design skill is deciding which slice of your traffic genuinely benefits from running at the edge (cacheable reads, header rewriting, request validation, A/B assignment) versus what still needs the full context only your origin server has (a database transaction, complex business logic, anything requiring strong consistency).
Rejecting bad requests before they reach origin
Edge functions are also the cheapest place to reject traffic your origin should never have to process at all — malformed requests, obviously abusive patterns, or requests missing a required header your API always needs. Doing this at the edge means the rejection happens close to the attacker or misbehaving client, not after a round trip across the globe to your origin and back:
fsthttp.ServeFunc(func(
ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request,
) {
if r.Header.Get("X-Api-Key") == "" {
w.WriteHeader(fsthttp.StatusUnauthorized)
io.WriteString(w, `{"error":"missing api key"}`)
return
}
resp, err := r.Send(ctx, "origin")
if err != nil {
w.WriteHeader(fsthttp.StatusBadGateway)
return
}
defer resp.Body.Close()
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
})
This is a small example of a broader principle: any check that doesn't require your origin's database or business logic — authentication header presence, basic input shape validation, geographic restrictions — is a candidate for pushing to the edge, freeing your origin's capacity for the requests that genuinely need it.
The edge is not a faster version of your server — it's a different, more limited place to run a small, well-chosen slice of your logic closer to the user, with everything else still deferring to origin.
Frequently Asked Questions
Can I just take my existing net/http handlers and run them on Fastly Compute?
Not directly — fsthttp mirrors net/http's shape closely enough to read familiarly, but the underlying execution model is a WebAssembly sandbox with strict CPU-time budgets and no goroutine scheduler in the usual sense, as the Warning above spells out. Code that spawns background goroutines or assumes it can keep running after the response is sent needs to be redesigned around "do one focused thing per request and return," not just recompiled.
Why does the edge function call r.Send(ctx, "origin") instead of making an ordinary outbound HTTP request?
Because "origin" refers to a backend configured outside your code, in Fastly's service configuration, and r.Send is the SDK's way of routing through that configuration rather than opening an arbitrary connection from inside the sandbox. This is also what keeps the mechanism working within the platform's tight execution-time limits — the backend is already known and pre-configured rather than resolved and dialed fresh on every request.
If my API returns personalized content per user, is it safe to just add edge caching for a latency win?
Not without matching your cache key to what actually varies — that's the exact trap the DeepDive on cache keys describes. Caching personalized recommendations on the URL alone will serve one user's data to another, while over-varying (say, on a cookie that doesn't actually affect the response) fragments the cache into near-zero hit rate; the Vary header needs to reflect the real variance, no more and no less.
Doesn't rejecting requests at the edge duplicate validation I already do in my origin API? It duplicates the check, but deliberately, because the cost profile is different — a request missing an API key or matching an obviously abusive pattern is cheapest to reject close to the client, before it costs a round trip across the globe to origin and back. The chapter frames this as any check that doesn't need your origin's database or business logic being a candidate for pushing to the edge, freeing origin capacity for requests that genuinely need its full context.
How is stale-while-revalidate different from a plain max-age?
max-age=60 alone just says the response is fresh for 60 seconds and then must be refetched from origin before being served again. Adding stale-while-revalidate=300 lets the CDN keep serving that now-stale response immediately for up to 300 more seconds while it quietly refetches from origin in the background, which is a meaningful latency win for data like a product listing that doesn't need to be perfectly fresh on every single request.