Lesson 74 of 81
Next.js runs anywhere Node.js runs — but several of its headline features were built against Vercel's managed infrastructure first, and self-hosting means picking up the plumbing Vercel used to hide from you.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Next.js is open source, and the standard output build mode produces a real Node.js server you can run anywhere node runs — your own VM, a Kubernetes cluster, a plain Docker container. That much is genuinely portable. What isn't automatically portable is the infrastructure behind a handful of features that Vercel built to work seamlessly on its own platform: on-demand image optimization and Incremental Static Regeneration (ISR) both quietly depend on stateful, multi-instance-aware infrastructure that Vercel provides for free, and that a self-hosted deployment has to provide itself.
This isn't "self-hosting is worse" — it's that the two deployment targets solve the same problems with different amounts of assembly required. Vercel's pitch is that ISR, image optimization, and the edge network are already wired up; self-hosting's pitch is control and cost, in exchange for wiring some of that up yourself.
By default, the on-disk ISR cache lives in .next/cache on whichever server instance handled the render. That's fine with exactly one instance. The moment you scale a self-hosted deployment horizontally — two or more Node processes behind a load balancer, which is the normal way to add capacity outside Vercel — each instance keeps its own cache. A revalidation triggered on instance A doesn't touch instance B's copy, so the same route can serve stale content from one instance and fresh content from another, depending on which one the load balancer happened to route to.
Vercel avoids this because its ISR cache isn't per-instance at all — it's centralized. Self-hosting the same behavior means implementing a custom cache handler backed by shared storage:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheHandler: require.resolve("./cache-handler.js"),
cacheMaxMemorySize: 0, // disable the default in-memory cache; defer fully to the handler
};
export default nextConfig;
// cache-handler.js — minimal shape; a real one talks to Redis or similar
const redis = require("./redis-client");
module.exports = class CacheHandler {
async get(key) {
return redis.get(key); // shared across every instance
}
async set(key, data, ctx) {
await redis.set(key, data, ctx);
}
async revalidateTag(tag) {
await redis.invalidateByTag(tag);
}
};
With every instance pointed at the same Redis, a revalidation triggered anywhere is visible everywhere immediately — which is the behavior Vercel gives you by default, and self-hosting has to opt into explicitly.
sharp is a real, separate install for self-hosted image optimization, not a transitive dependency that comes along for free with next. Forgetting it is a common first-deploy surprise.revalidatePath/revalidateTag.output mode you pick. Whether you deploy the default server output or output: 'standalone' (covered in the deployment lesson), the ISR-cache-sharing and image-optimization gaps exist either way — output mode is about how the build artifact is packaged, not about which managed services back it."We're self-hosting on three Node instances behind a load balancer, and ISR revalidation seems to randomly not take effect" is a debugging scenario that separates candidates who've only ever deployed to Vercel from ones who understand what Vercel was actually doing underneath ISR — the fix (a shared cache handler) only makes sense once you know the default cache is per-instance.