CDN and edge caching

A user in Singapore should not download your logo from a single origin in Virginia on every page load. A CDN (Content Delivery Network) places copies of content at edge points of presence (POPs) near users. Hits never touch your origin; misses fetch once, store, and serve locally. Latency drops, origin load drops, and big static assets stop melting your app tier.

Intuition

Think of airport bookstores versus one warehouse. The warehouse is origin — complete and authoritative. Airport stores stock bestsellers. Most travelers grab a local copy; restock happens when a title sells out. Edge caches are those airport stores for bytes: HTML, JS, images, and sometimes cacheable API GETs.

How it works

Path. DNS (or anycast) sends the client to a nearby POP. On hit, the edge returns bytes. On miss, the edge requests origin, stores according to headers, and responds.

flowchart LR U[User] --> Edge[Edge POP] Edge -->|hit| U Edge -->|miss| Origin[Origin] Origin --> Edge Edge -->|store + return| U

What belongs at the edge.

Content Typical policy
JS, CSS, images, fonts Long TTL + fingerprint in filename (app.a1b2c3.js)
Public marketing pages Medium TTL, purge on publish
Cacheable GET APIs Short s-maxage, careful Vary
Authenticated private data Usually private, no-store — skip shared edge

Cache-Control. Origin (or your FastAPI app) sets headers the CDN honors:

Invalidation / purge. When you ship a blog edit without content-hashed URLs, purge the path (or use surrogate keys). Fingerprinted static assets avoid purges: new deploy = new URL = cache naturally cold.

API caching caveats. Only cache GETs that are identical for all users (or vary correctly on headers like Accept-Language). Never put Authorization responses in a shared edge cache unless you know the CDN keys include auth and your threat model allows it.

In code

Emit CDN-friendly headers from FastAPI for public assets and a cacheable public catalog GET.

from fastapi import FastAPI
from fastapi.responses import JSONResponse, FileResponse

app = FastAPI()


@app.get("/static/app.js")
async def app_js():
    # Fingerprinted filename in real apps; long immutable cache
    return FileResponse(
        "static/app.a1b2c3.js",
        headers={
            "Cache-Control": "public, max-age=31536000, immutable",
        },
    )


@app.get("/api/public/catalog")
async def public_catalog():
    items = [{"sku": "tee-1", "name": "Tee", "price_cents": 2000}]
    return JSONResponse(
        content=items,
        headers={
            # Browsers: 60s; CDN shared cache: 10 minutes
            "Cache-Control": "public, max-age=60, s-maxage=600",
            "Vary": "Accept-Encoding",
        },
    )


@app.get("/api/me")
async def me():
    return JSONResponse(
        content={"email": "user@example.com"},
        headers={"Cache-Control": "private, no-store"},
    )

Purge is usually a CDN API call (pseudocode):

def purge_catalog(cdn_client):
    cdn_client.purge_paths(["/api/public/catalog"])

What goes wrong

One-line summary

CDNs cache bytes near users — use Cache-Control, fingerprinting, and careful rules so the edge speeds you up without serving the wrong person the wrong content.

Key terms