
Next.js Deploying to different platforms
"Can I deploy Next.js anywhere?" is the wrong question, and it's the one most teams ask. The right question is narrower: does the platform I'm considering support the specific Next.js features my application actually uses? Those are very different questions, and conflating them is how teams end up debugging a broken ISR setup three weeks into production, on a platform that "supports Next.js" in name only.
Next.js doesn't treat a route as either fully static or fully dynamic. It treats static and dynamic as a spectrum that operates at the component level — a single page can have a static shell, a streamed dynamic section, and a Server Action all coexisting. That flexibility is a deliberate design choice, and it comes with a cost: different pieces of that spectrum lean on different infrastructure capabilities from whatever is running your app. A platform that handles a static export beautifully might buffer streamed responses into a single blob, silently degrading Partial Prerendering into something that still works but loses its entire performance advantage. This article walks through what Next.js actually requires, what's "recommended but not required," and how to evaluate a deployment target with your eyes open instead of finding out the hard way.
The One Hard Requirement: A Node.js Server
Strip away every optimization, every CDN integration, every edge trick, and Next.js has exactly one non-negotiable requirement: a Node.js server. That's the whole list.
A single next start process, running on plain Node.js with no additional infrastructure, correctly handles every feature Next.js ships: Server Components, Incremental Static Regeneration, Partial Prerendering, Cache Components, Server Actions, Proxy, and the after() API. None of these require edge compute, a CDN, or a distributed cache to function. They require those things to perform optimally — which is a distinction worth sitting with, because it's the single most misunderstood part of Next.js deployment.
The only extra dependency worth calling out is the sharp package, which Next.js needs for server-side Image Optimization. If your deployment target can install and run sharp alongside your Node.js process (most container-based and traditional server platforms can), you get full image optimization support. Platforms that can't run native binaries at all will need to either proxy image requests elsewhere or use an unoptimized image loader — but that's a platform-specific constraint, not a Next.js one.
# The baseline that satisfies every Next.js feature:
npm run build
npm run start
If a platform can run those two commands and keep the process alive, you have a fully functional Next.js deployment. Everything past this point in the article is about how well it performs, not whether it works.
Two Different Questions: Functional Fidelity vs. Performance Fidelity
This is the distinction that resolves most of the confusion around "does platform X support Next.js." There are two separate axes, and platforms get evaluated on both, but they mean very different things.
Functional fidelity is binary. Every feature either behaves correctly or it doesn't. Next.js maintains an adapter test suite specifically so this isn't a matter of opinion — a platform's adapter either passes the compatibility tests or it doesn't. If it passes, the platform supports Next.js, full stop. There's no "mostly supports" in this dimension.
Performance fidelity is a spectrum, and it's where platforms genuinely differentiate from one another. A feature can be functionally correct while performing very differently depending on the infrastructure underneath it. Two concrete examples:
- Partial Prerendering's static shell is supposed to be served at CDN edge latency, arriving in milliseconds regardless of where the visitor is. On a platform without edge distribution, that same shell is still served correctly — just from your single origin server, at whatever latency that implies for a visitor on the other side of the planet.
- Incremental Static Regeneration is supposed to serve stale content instantly while revalidating in the background, with the revalidation event propagating to every server instance within roughly a second. On a platform without a shared cache layer, each instance revalidates independently — correct, but slower to converge, and briefly inconsistent across instances.
Neither of these is a bug. Both platforms are functionally correct. One of them is faster. This is why "we deployed Next.js and ISR seems to work fine, just a little sluggish across our three instances" isn't a red flag — it's exactly what you'd expect from a platform with functional but not maximal performance fidelity, and it's a completely reasonable trade-off if that platform is cheaper, simpler to operate, or better suited to your team's existing infrastructure.
Keep this distinction in your back pocket for the rest of this article, because the feature matrix below uses exactly this language: "streaming required" and "shared cache recommended" are functional-fidelity requirements, while "edge stitching" is purely a performance-fidelity optimization that changes nothing about correctness.
The Feature Support Matrix
Here's the full breakdown of what each major Next.js feature needs from its host, straight from the current docs:
| Feature | Streaming | Shared Cache | Edge Stitching | Notes |
|---|---|---|---|---|
| Server Components | Required | No | No | Basic streaming support |
| ISR (time-based) | No | Recommended | No | Works per-instance without shared cache |
| ISR (on-demand) | No | Recommended | No | Tag propagation needs shared cache for multi-instance |
| Partial Prerendering | Required | Recommended | Optional | See the PPR Platform Guide for details |
Cache Components (use cache) | Required | Recommended | No | Shared cache enables cross-instance consistency |
| Proxy / Middleware | No | No | No | Runs at edge or origin |
| Server Actions | Required | No | No | POST requests with a streaming response |
after() | No | No | No | Requires graceful shutdown support |
A few things jump out once you actually sit with this table instead of skimming it.
Streaming shows up more than you'd expect. Server Components, Partial Prerendering, Cache Components, and Server Actions all list streaming as required. "Streaming Required" here means the platform has to support chunked transfer encoding or HTTP/2 streaming, and — critically — it must not buffer the entire response before forwarding it to the client. This is the single most common way a platform quietly breaks Next.js: it works, nothing throws an error, but every response gets buffered by a proxy or gateway layer somewhere in the stack, and you never actually get the progressive-rendering benefit you built your UI around. Your Suspense boundaries resolve correctly on the server; they just all arrive at once instead of trickling in. If your app feels no faster after adding streaming, this is the first thing to check — not your code, your infrastructure.
Shared cache is "recommended," almost never "required." This matters because it means you can deploy ISR, Cache Components, and PPR on a single-instance platform with zero shared cache infrastructure and get fully correct behavior. The caveat is "single-instance." The moment you scale horizontally to multiple server processes or containers, each instance maintains its own independent cache unless you wire up a shared backend. That's not a failure — each instance is still serving correct content — but a revalidation triggered on instance A won't be reflected on instance B until B independently revalidates on its own schedule. If you've ever seen a Next.js app where "the cache seems to update on some requests but not others" after scaling out, this is almost always the explanation, and the fix is a shared cacheHandler (for ISR, Route Handlers, patched fetch, and Image Optimization) or cacheHandlers (for 'use cache' entries specifically) — not a code change.
after() has its own, easy-to-miss requirement: graceful shutdown. The after() API lets you run code after a response has already been sent to the client — logging, analytics, cleanup work that shouldn't block the user from seeing their page. If your platform kills the process the instant a request completes, without waiting for pending after() callbacks to finish, that work simply never runs. This isn't listed as needing streaming or shared cache, but it needs something arguably more platform-specific: your host has to actually wait a beat before terminating a worker.
Why Streaming Actually Matters: A Concrete Example
It's easy to read "streaming required" as an abstract checkbox. Here's what it actually looks like in code, and what breaks if the platform underneath silently buffers it.
// app/dashboard/page.tsx
import { Suspense } from "react";
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
{/* This renders immediately — no data dependency */}
<QuickStats />
{/* This streams in once the slow query resolves */}
<Suspense fallback={<p>Loading recent activity...</p>}>
<RecentActivity />
</Suspense>
</div>
);
}
async function RecentActivity() {
const activity = await fetchActivityFromSlowAnalyticsDB();
return <ActivityFeed items={activity} />;
}
On a platform with genuine streaming support, the visitor sees the page shell and QuickStats instantly, with the "Loading recent activity..." fallback rendered immediately, and then RecentActivity pops in whenever its slow query resolves — no blank white screen, no full-page spinner. On a platform that buffers the response, the server still computes everything correctly, but the entire HTML document, fallback and all, gets held until RecentActivity finishes resolving before a single byte reaches the browser. The visible symptom is a page that takes exactly as long to show anything as its single slowest data dependency — which is precisely the behavior Suspense streaming exists to eliminate. Nothing in your code is wrong. The platform between your server and the browser is the culprit, and no amount of debugging your React tree will fix it.
Configuring Shared Cache for Multi-Instance Consistency
If you're running more than one server instance — multiple containers behind a load balancer is the common case — and you're using ISR or Cache Components, wiring up a shared cache backend is the difference between "revalidation happens eventually, per instance" and "revalidation happens once and every instance sees it within about a second."
There are two separate configuration surfaces here, and it's worth keeping them straight because they cover different runtime paths:
cacheHandler (singular) covers server-side cache paths: ISR, Route Handlers, the patched fetch, unstable_cache, and Image Optimization.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
cacheHandler: require.resolve("./cache-handler.js"),
cacheMaxMemorySize: 0, // disable the default in-memory cache when using a custom handler
};
module.exports = nextConfig;
cacheHandlers (plural) configures the backend for the 'use cache' directive specifically — a distinct, newer caching path introduced with Cache Components.
// next.config.js
const nextConfig = {
cacheHandlers: {
default: require.resolve("./use-cache-handler.js"),
},
};
module.exports = nextConfig;
Both of these are extension points, not built-in Redis integrations — you (or your platform) implement the handler against Next.js's cache interface, backed by whatever shared store makes sense: Redis, a managed KV store, a database. Some deployment adapters ship one of these pre-wired for you; if you're self-hosting on a generic container platform, you'll be implementing or adopting one yourself. Skipping this step isn't broken — it's the single-instance-shaped default behavior extended across multiple instances, with the caveat that "instant, coordinated revalidation" becomes "eventually consistent, per instance" until you add it.
CDN Infrastructure: The Building Blocks, Not a Checklist
A lot of platform comparisons stop at "does it have a CDN," which isn't a specific enough question to be useful. The current docs break CDN capability down into the actual primitives that matter for Next.js:
| CDN | Edge Compute | Key-Value / Tags | Blob Storage | PPR Resuming |
|---|---|---|---|---|
| Cloudflare | Workers | KV | R2 | Yes (worker) |
| Akamai | EdgeWorkers | EdgeKV | Object Storage | Yes (worker) |
| Amazon CloudFront | Lambda@Edge | KeyValueStore | S3 | Yes (Lambda) |
| Fastly | Compute | KV Store | Object Storage | Yes (WASM) |
| Azure | Functions | Managed Redis | Blob Storage | Yes (server) |
| Google Cloud | Cloud Run | Various KV | Cloud Storage | Yes (server) |
The important caveat, straight from the docs, is that these are available building blocks, not finished integrations. Having Workers or Lambda@Edge available doesn't mean Next.js automatically uses it — a deployment adapter has to actually be built to wire these primitives into the framework's caching and rendering model. In practice, most community adapters today deploy Next.js as a straightforward Docker container or Node.js process, without reaching for CDN-specific primitives like edge KV or PPR resuming at all. That's not a shortcoming; it's the pragmatic default, and it's exactly the "single Node.js server" baseline from the top of this article, just running on managed infrastructure.
If you're evaluating a CDN specifically for its Next.js story, the question to ask isn't "does this CDN have edge compute" — nearly all of them do now — it's "does an adapter exist that actually uses it for Next.js, and is that adapter verified." Which brings us to adapters themselves.
The Deployment Adapter API
Next.js exposes a public Deployment Adapter API that lets a platform customize how a Next.js app gets built and deployed for its specific infrastructure. Adapters run at build time and transform the standard Next.js build output into whatever shape a given platform needs. This is a genuinely open API — anyone can build an adapter, and there's no special or private access required to do it well.
// next.config.js
const nextConfig = {
adapterPath: require.resolve("./my-platform-adapter.js"),
};
module.exports = nextConfig;
It's worth being precise about where the adapter's responsibility ends and the caching configuration's begins, because they're easy to conflate: the adapter governs build-time output — how your app gets packaged and shaped for deployment. cacheHandler and cacheHandlers govern runtime caching behavior — what happens when a cached value is read or written while your app is actually running. Together, these two surfaces make up the complete platform integration story for Next.js. There's no third, hidden integration point.
What Makes an Adapter "Verified"
Not all adapters are created equal, and Next.js has a specific, two-part bar for what counts as a verified adapter:
- Open source. The adapter's source is publicly available, so both the community and the Next.js core team can actually read it, contribute to it, and confirm what it's doing.
- Runs the compatibility test suite. The platform can run the full Next.js adapter compatibility test suite against its own adapter, which gives everyone visibility into exactly which features are supported, which are in progress, and where the gaps are.
Verified adapters are hosted under the official Next.js GitHub organization and maintained by the platform teams themselves. This is worth calling out explicitly because it directly answers a question that comes up constantly: is Vercel's Next.js support somehow special or privileged, given that Vercel created the framework? The docs are explicit that it isn't — Vercel's adapter uses the exact same public adapter API as every other platform's adapter, with no private framework hooks. A platform can absolutely build a closed-source adapter on top of the same public API and test suite; it simply won't be listed as verified, because the Next.js team has no way to inspect what it can't see the source of. "Not verified" is not the same claim as "doesn't work" — it just means nobody outside that platform's team can vouch for it.
A Practical Framework for Choosing a Platform
The docs give you the requirements; here's how I'd actually walk through choosing a target, in order:
1. List the features your app actually uses. Not every app uses PPR or Cache Components. If you're building a mostly-static marketing site with a handful of dynamic pages, your requirements list is short: Node.js, streaming for Server Components, done. If you're leaning hard into Cache Components and multi-instance ISR, your list is longer, and shared cache stops being optional in any practical sense.
2. Check whether the platform has a verified adapter. This is the fastest signal you have. A verified adapter means the compatibility suite has actually run against it, which tells you functional fidelity is a solved problem for that pairing. No verified adapter doesn't mean "don't use it" — plenty of teams run Next.js successfully on generic Node.js hosts with no framework-specific adapter at all — but it does mean you're on your own for confirming correctness, and you should budget time for it.
3. Decide how much you care about performance fidelity, specifically. If your traffic is regional and latency-sensitive, edge stitching and PPR resuming matter. If you're running an internal tool for a team in one office, they largely don't, and you can save yourself the complexity of chasing edge infrastructure you'll never feel the benefit of.
4. If you're scaling horizontally, treat shared cache as a requirement, not a nice-to-have. The matrix calls it "recommended," and technically it is — your app won't crash without it. But "recommended" is doing some quiet work in that sentence: without it, you're signing up for cache inconsistency across instances as a permanent, expected characteristic of your production environment, not an edge case you'll hit occasionally.
5. Confirm sharp can actually run. This is the most mundane item on this list and the one people forget until their images stop optimizing in production. If your platform is a restrictive serverless environment that can't run native binaries, find that out during evaluation, not during an incident.
Common Mistakes I See Teams Make Here
Assuming "runs on the edge" means "supports every Next.js feature at full fidelity." Edge runtimes are excellent for Proxy and lightweight compute, but plenty of Next.js features — particularly anything touching Node.js-specific APIs or sharp for image optimization — simply aren't available in a pure edge runtime. Read the fine print on what actually executes where.
Treating "recommended" as "optional and therefore ignorable." As covered above, shared cache being "recommended" rather than "required" is a statement about correctness, not about whether you'll be happy with the result once you scale past one instance.
Confusing a platform having a CDN with that CDN's primitives being wired into Next.js. As the table above makes clear, having Workers or Lambda@Edge available on a CDN is necessary but nowhere near sufficient — an adapter has to actually be built to use them for anything Next.js-specific to benefit.
Not budgeting time to run the compatibility test suite yourself when deploying to a platform without a verified adapter. It's public, it's designed for exactly this purpose, and running it once during evaluation is far cheaper than discovering a gap in production.
Forgetting that after() needs graceful shutdown support. This one is genuinely easy to miss because nothing throws an error — your analytics or logging calls inside after() just quietly never execute on platforms that kill workers immediately after the response is sent.
Key Takeaways
| Question | What It Tells You |
|---|---|
| Does the platform run a Node.js server? | Whether Next.js can run at all — the only hard requirement |
| Does it support streaming / no response buffering? | Whether Server Components, PPR, Cache Components, and Server Actions perform correctly |
| Do I need multi-instance shared cache? | Whether ISR and Cache Components stay consistent across instances, not just correct on each one |
| Does a verified adapter exist? | Whether functional fidelity has already been confirmed against the official test suite |
Can it run sharp? | Whether server-side Image Optimization works at all |
Does after() need to survive shutdown? | Whether post-response cleanup work (logging, analytics) actually executes |
Choosing where to deploy a Next.js app isn't really a question of "which platforms support Next.js" — nearly all of them claim to, and at the baseline Node.js-server level, most of them are telling the truth. The real question is which specific features your application depends on, and whether your chosen platform gets you functional correctness on all of them (non-negotiable) and how much performance fidelity on top of that (very much negotiable, and dependent on what your users actually need). Answer that specifically, feature by feature, and the "right" platform stops being a matter of brand reputation and becomes a straightforward matching exercise.


