Edge Rendering With the Next.js App Router
When to run code at the edge, when not to, and the caching mental model that makes the App Router finally click.

The promise and the catch
Running code at the edge sounds free: lower latency for users, lower compute cost for you. The catch is that the edge is a constrained runtime. No Node APIs, a tight CPU budget, and a memory ceiling that will quietly bite you the moment you try to hydrate a heavy graph.
The Next.js App Router lets you opt in per route with export const runtime = "edge". That one line is also a contract: everything in that route — every import it pulls in — has to run in the edge runtime. The error you'll hit is not "edge doesn't support X", it's a webpack build failure naming the offending module. Annoying, but better than discovering it in prod.
When the edge actually pays off
Three cases where I've measured a real win:
- Personalization at the top of the page. Geo, A/B variant, auth state — anything where the user's response shape depends on the request. Edge lets you render the shell with the right variant baked in, then stream the rest.
- Auth-gated redirects and middleware. If 100% of your traffic needs a session check before it hits the origin, that check belongs at the edge.
- Tiny, mostly-static pages that depend on a single fast data source. A status page, a config viewer, a per-locale marketing landing.
The common thread is small compute, request-dependent output. If a route spends most of its time doing CPU-bound work or pulling a huge join from Postgres, moving it to the edge is a net loss for everyone except the CDN vendor.
The caching mental model
This is the part that trips everyone up, myself included for the first month. The App Router has four cache layers, and they behave differently at the edge:
- Request memoization — per-request, dedupes fetches within a single render. Free, always on.
- Data cache — survives across requests, keyed by fetch URL + options. This is your friend and your footgun.
- Full-route cache — statically rendered routes get persisted at build time. Edge or node runtime, doesn't matter.
- Router cache — client-side, navigated routes get cached in memory for 5 minutes by default.
The rule I give junior engineers: state explicitly which cache you're targeting. Don't sprinkle cache: "no-store" and revalidate: 60 and hope. Read the route, decide what's static, what's revalidated, and what's dynamic — and then make the runtime choice match. A route that needs fresh data every request is paying the edge latency for nothing if its data source is 300ms away.
A real example
A marketing page that pulls the user's locale from the request and a sitewide nav from a CMS:
export const runtime = "edge";
export const revalidate = 300; // 5-minute data cache
export default async function Page({ params }) {
const { locale } = await params;
const nav = await fetch(`${CMS}/nav/${locale}`).then((r) => r.json());
// ...render
}
Two things worth noting. The revalidate controls the fetch cache, not the route. The route is dynamic at the edge because it reads params. The nav response gets cached for 5 minutes, which is what you want — the CMS doesn't change per request, and the edge.Serializer is going to out-wait your origin anyway.
What I'd skip
Edge functions for long-running webhooks. Edge for websocket connections. Edge for anything that streams a multi-megabyte file. These all work at small scale and then curl up and die at medium scale. Know the ceiling before you commit.

