Opinions & Insights

How we run Next.js today — and what should change

Image with Next.js logo

A note about this moment

We actually sat on this post for a while, taking time to figure out how best to present the facts and suggestions below, without it appearing as a takedown post. After all, we are in competition with Vercel in the platform space.

Then, the recent security incident around Next.js middleware happened, triggering a small fire on social and drawing criticism about how the incident was handled.

This article is not about that security incident. Rather, it is about the larger context of the current challenges in making sure Next.js runs as intended for customers. Many of these — as well as, arguably, the incident response — are related to the closed nature of how Next.js is maintained. But there is now an honest effort by everyone to tackle this problem.

Our aim here is to provide a better picture of the challenges, and draft a path for collaboration going forward.

Re-introducing Next.js

Next.js is an open-source web development framework, created and governed by Vercel, a cloud provider that offers managed hosting of Next.js as a service.

The framework’s innovations on top of React has made it a preferred tool for many developers for good reason: it has introduced concepts like built-in support for SSR, SSG, ISR, and API routes.

At Netlify, we’re proud to offer comprehensive Next.js support, ensuring developers have deployment choices without sacrificing functionality. However, maintaining this level of support for Next.js comes at a cost and presents unique engineering challenges. These challenges aren’t just limited to Netlify, but across peers like Cloudflare, AWS Amplify Hosting, SST, Google Firebase App Hosting, and Microsoft Azure Static Web Apps.

Let’s take a behind-the-scenes look at the work Netlify’s engineers do to maintain feature parity with Vercel’s platform, and the work we do to stay on top of a steady stream of changes to Next.js.

Challenge #1: no adapter support

A major benefit of open-source software is its portability — developers and organizations should have the freedom to move between different providers without fear of getting locked into a specific vendor. For example, the npm CLI integrates with both npm, Inc.’s registry as well as all third-party registries, as does Docker with DockerHub, among countless examples.

To achieve this, most modern web development frameworks use the concept of adapters, plugins, or presets to tailor the output of the framework to a specific deployment target. Providers need this to provision and configure the infrastructure needed to power your applications. Examples include Remix, Astro, SvelteKit, Gatsby, and Qwik.

A framework (Astro) with first-class support for swappable build adapters, each targeting a deployment environment or provider

Some frameworks go even further: Nuxt, Analog, SolidStart, and TanStack Start all use the same underlying mechanism (Nitro) and thus share deployment target presets. The desired utility and scope of such common foundations is a fast-moving topic with differing opinions playing out in the open, just as it should be.

Nitro-based frameworks also have first-class support for swappable build adapters, but go further by sharing their adapters

These patterns allows frontend developers to keep the core of their code untouched and merely swap the adapter if they decide to deploy to another provider.

These adapters can be maintained by framework authors, hosting providers, the community, or all of the above. Frameworks are typically structured so that anyone can build their own adapter in case one isn’t available for the provider of their choice. This is possible because they have a publicly documented adapter specification. This also means fewer surprises, as changes to the adapter interface follow semantic versioning conventions.

On their end, platform vendors typically have their documented API for how frameworks should interact with the platform, which any framework can use. In our case, that is the Netlify Frameworks API.

How Netlify builds a site using a framework (Astro) with a Netlify build adapter, as an example

The unique challenge with Next.js is that, although Vercel (the platform) has a Build Output API for frameworks since 2022, Next.js itself does not conform to this API and it has no adapter mechanism through which any other actor can support another platform. Rather, Next.js builds use a private, largely undocumented format that is subject to change.

Instead, providers like Netlify, Cloudflare, AWS Amplify Hosting, SST, Google Firebase App Hosting, and Microsoft Azure Static Web Apps must instead read the Vercel-tailored, partly-undocumented build output from disk, translate it to their own format, and write this back to disk.

Next.js builds are only compatible with Vercel, so other platforms (like Netlify) must transform it to their own format after the fact

At Netlify, we automatically run a build plugin (the OpenNext Netlify adapter) after Next.js builds. Coupled with comprehensive automated tests (both the framework’s own tests that we run, and our own test suites), the end result is a robust Next.js experience on Netlify. For the vast majority of sites this works out of the box with zero configuration. However, the implementation would be much simpler, easier to maintain, and easier for the community to contribute to if Next.js followed the established adapter pattern. This has been discussed on and off for some time, but really took off since the OpenNext group was expanded to clearly voice the needs of multiple actors. Now, it’s finally in motion!

Challenge #2: No production-grade documentation for serverless deployments

The Next.js deployment docs list these deployment options:

You can deploy managed Next.js with Vercel, or self-host on a Node.js server, Docker image, or even static HTML files.

Here, “a Node.js server” refers to one Node.js server. A single, unique instance of a Node.js server (with or without Docker) with no horizontal scaling and no zero-downtime deploys is not a viable deployment strategy for serious projects, and fully static sites cover limited Next.js use cases these days. Out of the box, this does not support much of the functionality that makes Next.js powerful at scale: edge middleware, globally persistent page and fetch caching enabling Incremental Static Regeneration and on-demand revalidation, to name a few.

The self-hosting docs briefly touch on some complexities (emphasis ours):

Caching and revalidating pages (using Incremental Static Regeneration (ISR) or newer functions in the App Router) use the same shared cache.

[…]

By default, generated cache assets will be stored in memory (defaults to 50mb) and on disk. If you are hosting Next.js using a container orchestration platform like Kubernetes, each pod will have a copy of the cache. To prevent stale data from being shown since the cache is not shared between pods by default, you can configure the Next.js cache to provide a cache handler and disable in-memory caching.

The issue of stale data is trickier than it seems. For example, as each node has its own cache, if you use revalidatePath in your server action or route handler code, that code would run on just one of your nodes that happens to process that action/route, and only purge the cache for that node.

The docs provide a very high-level overview of how to implement this yourself with a custom CacheHandler implementation. A custom cache handler must concern itself with multiple types of cache entries (page, route, fetch, image, redirect) with separate logic for each, normalize cache keys to and from its backing store, handle on-demand revalidation, handle cache tags, parse and handle various undocumented build manifest files, handle a multitude of edge cases, and more.

Since Next.js is a major framework with wide adoption, we’re committed to fully supporting it — meaning that our engineering team that’s dedicated to frameworks support on our platform has built up the necessary knowledge and implemented our own cache handler and associated tests. If something changes in an upcoming canary version, we find out promptly.

The challenges described above mean that a disproportionate amount of this team’s time could easily be spent on Next.js. Fortunately, we’re using our own platform primitives as much as possible, building on robust features (and iterating on this as necessary) instead of tailoring framework-specific solutions for every requirement.

You should be able to pull this off too

Even with using our own existing capabilities as much as possible, Netlify’s cache handler is ~500 lines of code and ~500 lines of tests — far from trivial. This type of investment is out of reach for most developers considering self-hosting.

Since Next.js abstracts much of these runtime details, there is a strong need to extend the adapter concept to support not just build-time concerns. For example, the CacheHandler interface for platforms should probably be re-imagined to require the least amount of framework logic (which is always subject to change) on the platform side.

This is currently beyond the scope of the build-time adapter discussed between the OpenNext group and Vercel, but we’re optimistic that this increased collaboration in a shared forum will lead to better, easier, simpler, documented deployment options for Next.js developers.

Challenge #3: Undocumented behaviors

The Next.js framework contains a number of undocumented options, features and behaviors. For the platforms to reach feature parity with Vercel, platform vendors currently need to learn about these and take them into account.

For example, Next.js sites deployed to Vercel exercise their own unique Next.js code paths, opted into via the undocumented minimalMode. This disables many of the framework’s core features, which are then presumably reimplemented within the Vercel platform (which is naturally closed-source). Though Netlify does not, some providers have even chosen to leverage this undocumented mode and have reverse-engineered equivalent functionality into their own platforms.

How we approach it: proactive automated testing

Our Next.js Adapter contains an extensive battery of hundreds of tests that we run against Next.js 13, 14, 15, and the latest canary release, all on Linux, macOS, and Windows. Most of these are integration tests that build and run a Next.js site fixture, and many of these are end-to-end tests that deploy the test site to Netlify.

Of course, these are just table stakes. In addition, for each Next.js version we test against, we also clone the source Next.js git repository and run its over 1700 end-to-end integration tests, adjusted to deploy to Netlify. These tests are very extensive and as they are testing through the entire, real experience via Playwright tests running in a browser like a user, these give us very high confidence to ship changes.

We then run this suite of tests daily, generate a report (available publicly) which automatically includes annotations for known issues we’ve tracked on GitHub, and we proactively notify our Frameworks team of any new unknown issues.

This is especially valuable because it tests against not only stable Next.js releases but also “canary” pre-releases. By ensuring we conform to tests against the latest canary release, we’re able to incrementally add support for new features and handle upcoming breaking changes (to both documented and undocumented interfaces) before they’re announced and become generally available. This is really only necessary because of the lack of roadmap transparency and release predictability (see challenges #5 and #6 below).

Challenge #4: Not built on open web standards

At the time of writing, the first four words on the Remix framework website are “Focused on web standards”; Astro’s website advertises “Zero Lock-in”; SvelteKit’s asks you to “learn web standards that work across environments.”

When using Incremental Static Regeneration (ISR), Next.js emits Cache-Control response headers containing invalid Stale-While-Revalidate (SWR) directives such as:

Cache-Control: s-maxage=600, stale-while-revalidate

whereas RFC 5861 requires a Time-To-Live value such as:

Cache-Control: s-maxage=600, stale-while-revalidate=60

This might look like a minor headache, perhaps not worth mentioning here at all. But the impact of such a detail is that the invalid directive is silently ignored by many CDNs. Vendors must first detect that the problem even exists, and then either choose to support noncompliant headers for Next.js’ sake, or include code to transform that header in their Next.js glue code. And while it is now possible to opt into valid headers by default (see GitHub #52251, #61330, #65867, and #65887), this issue was ultimately closed with non-standard headers still returned by default. This is just one example.

In addition, some Next.js features (e.g. ISR) are implemented with an ad hoc solution that is deeply integrated with functionality baked into the Vercel platform. In many of these cases, these features could alternatively be built on top of open web standards (e.g. standard Web APIs) for greater portability. This concern, among others, prompted Kent C. Dodds to pen his thoughtful post “Why I won’t use Next.js”.

Advocating for the open web

Netlify’s mission is to build a better web, together.

We strive to put this into practice by participating in working groups like WinterCG (now WinterTC) and initiatives like OpenNext, drafting RFCs for frameworks and for the community at large, providing a stable, documented platform for partners to build upon, implementing support for new platform standards and APIs as the web continually evolves, and generally advocating for the open web at every opportunity.

But to solve our immediate, concrete needs for Next.js, we needed something more:

Primitives over frameworks

Kent Beck says “First make the change easy (this might be hard), then make the easy change.” In a way, this has been our strategy over the last year. Rather than solving for each new Next.js feature specifically, we aim to identify the underlying platform primitives that would make the feature easy to adopt — not only in Next.js but in any framework or even without one.

For example, rather than implement Next.js Incremental Static Regeneration (ISR) deep within our platform, we implemented support for the standard Stale-While-Revalidate directive and other advanced caching primitives entirely through simple HTTP response headers. As a result, Next.js ISR is implemented in our Next.js Adapter with a few lines of code that simply set headers, and since all other SSR frameworks allow users to set headers this functionality became automatically available to those frameworks — or even without any framework at all.

Challenge #5: Lack of roadmap visibility

Most frameworks publish a public roadmap to keep developers, providers, and integration developers apprised of upcoming new features, deprecations, breaking changes, and so on. See for example: Astro, Remix, Qwik, Nuxt, Angular, and minimal roadmaps such as Vite’s and Svelte’s

Next.js has no public roadmap or otherwise equivalent transparency.

While developers have the luxury of choosing when to upgrade their sites, hosting providers and integration developers must meet the community’s expectation to support new releases ASAP.

In addition, most frameworks’ roadmaps tend to be collaborative exercises with input from its community.

Next.js’s governance model states that “large architectural decisions and features start as a Request for Comments (RFC)”. However, at the time of writing, only four community RFCs have been adopted in eight years, with only one non-Google contributor among those. Ideation, decisions, and roadmaps are all hidden behind closed doors and shared with the public twice yearly at Vercel Conf and Next.js Conf concomitantly with new releases.

In collaboration with the community (OpenNext members and others), going forward we are committing to proposing and contributing to Next.js RFCs to address some of these challenges laid out above, such as the above-mentioned build output adapter. Let’s build together!

Challenge #6: Lack of release predictability

Most frameworks either update their public roadmap regularly (see above), announce upcoming changes publicly as progress is made (e.g. Nuxt), or adhere to a fixed release schedule (e.g. Angular).

None of these is the case for Next.js. Developers, hosting providers, integration developers, and the community at large are left to speculate and read between the lines.

For example, a Next.js 15 Release Candidate was announced in May, but no updates were shared between then and the next Release Candidate five months later (followed days later by Next.js 15 stable), other than what can be gleaned from inspecting the 2,254 commits to the canary branch in between.

How we track Next.js releases and prereleases

We watch Next.js PRs and releases like hawks. In fact, we were spending so much time doing this that we built a little service to automate the grunt work.

Be assured there are teams resorting to the same thing at Cloudflare, AWS Amplify Hosting, SST, Google Firebase App Hosting, Microsoft Azure Static Web Apps, and more.

Fortunately, thanks to Vercel’s outreach, we’ve established lines of communication with the Next.js team. With this in place we hope to gain better insights into upcoming changes to the framework.

Looking forward

Let’s summarize where we are, and the concrete steps taken:

First, we’re committing to collaborating with other providers, with the community, and with the Next.js team. We all share a common goal of providing great experiences for Next.js developers and their sites’ visitors. That is why we joined the OpenNext initiative along with the SST and Cloudflare teams.

Second, thanks to outreach from engineers at Vercel, this has already led to the creation of direct lines of communication with the Next.js core team. We are optimistic that this will help us address challenges at the source.

Third — actions speak louder than words — per the documented Next.js governance model, we will begin drafting RFCs in collaboration with other providers to address some of these challenges. This is already in motion.

We’re looking forward to building a better web, together.

Keep reading

Recent posts

How do the best dev and marketing teams work together?