
05 October 2026 · 10 min
Node 20 is over: upgrade runtimes without flying blind
Vercel has blocked new Node 20 deployments since 1 October, and GitHub Actions has already moved to Node 24. A practical plan for upgrades that are tested before production.




The old version works — until the next deployment
Since 1 October 2026, Node.js 20 can no longer be selected as the runtime for new Vercel builds and functions. Existing deployments keep running. That is precisely what makes the situation deceptive: the website is green, monitoring is quiet, and the failure appears only when an urgent hotfix needs to ship.
CI has moved on too. Since 23 September, GitHub Actions has run JavaScript actions on Node 24, and the former environment-variable escape hatch for Node 20 is no longer available. Maintainers of custom actions need to set runs.using to node24. Teams consuming actions need versions that support Node 24.
Node 20 itself is End of Life. The Node.js project no longer provides regular updates or security fixes for it. The question is no longer whether an upgrade would be sensible. It is: how do we modernise runtime, build and CI together without turning a Friday evening into an incident?
An application rarely has only one Node version
Many repositories contain several competing sources of truth:
- package.json declares a version in engines;
- .nvmrc or .node-version controls local environments;
- a Dockerfile selects its own base image;
- CI installs Node independently;
- the hosting platform stores a project setting;
- developers have another version installed globally;
- JavaScript actions bring their own embedded runtime.
While those versions roughly align, the inconsistency is easy to miss. During a major upgrade, the absurdity becomes visible: local tests run on Node 24, the image builds on Node 20, a pipeline uses Node 22 and the platform follows an old project setting.
The first step is therefore not a package update but an inventory. Search the entire repository for node, engines, setup-node, FROM node:, .nvmrc, .node-version and platform-specific configuration. Record whether each occurrence controls development, test, build or production.
The result should define one intended runtime. Vercel's current migration guide recommends Node 24, and the Node.js release overview lists 24 as an LTS line. Node 26 is still listed as Current in early October. For a routine production migration, the platform-recommended LTS release is the calmer target.
Reproduce first, repair second
An upgrade test on a laptop proves little when old node_modules, build caches and global tools are present. Start from a fresh environment that matches the eventual build.
A defensible first run includes:
- a fresh checkout;
- Node 24 from the same source CI will use;
- installation through the existing lockfile command, such as npm ci;
- the complete build;
- unit and integration tests;
- startup of the entry points that are actually deployed;
- a small smoke test against the running application.
If installation already fails, that is useful information: the problem is now reproducible. Causes often live outside application code — native add-ons, install scripts, stale toolchains or packages that do not yet accept the new runtime.
Do not immediately delete the lockfile and upgrade every dependency at once. That makes runtime effects almost impossible to separate from package effects. Run the unchanged state on Node 24 first, document failures and update only proven blockers. A broader dependency refresh can follow as a separate change.
Two lanes for an understandable comparison
While the old application is still buildable, a temporary CI matrix for Node 20 and Node 24 is useful. Both lanes run the same commands. This does not create permanent support for Node 20; it reveals what the runtime changes.
Compare more than red and green. Collect:
- installation time and warnings;
- test counts and skipped tests;
- build artifact size and contents;
- startup time and memory in the smoke test;
- warnings for deprecated or removed APIs;
- results from selected API and browser flows.
Remove the matrix once Node 24 is the confirmed default. Otherwise it preserves the obsolete line and doubles ongoing runtime and maintenance.
Test the real server, not only the compiler
A successful frontend build is not a complete runtime test. Node often lives in less visible parts of the product: API routes, server actions, image processing, webhooks, PDF generation, queue workers, cron jobs or build plugins.
Every executable unit needs at least one startup or invocation test. An API can expose a health check plus one representative request. A worker should process a realistic test event. An image service should pass a small file through the same decoder and encoder used in production.
Pay particular attention to boundaries with the outside world:
- TLS connections and certificates;
- database drivers and native libraries;
- file and path operations on Linux;
- ESM and CommonJS boundaries;
- streams, uploads and large responses;
- child processes and CLI tools;
- time, locale and timezone logic.
Not every one of these areas will break on Node 24. They are simply where differences hide that a typecheck cannot observe.
CI actions have their own migration path
GitHub Actions separates your project's Node version from the runtime used by a JavaScript action. actions/setup-node may put your project on Node 24 while an old third-party action was bundled for Node 20.
For workflow users, the migration means:
- inventory every referenced action;
- move to a current major or commit that supports Node 24;
- read release notes for changed inputs and outputs;
- test pull request, push, tag and scheduled workflows;
- remove obsolete exceptions and runtime opt-outs.
Maintainers of custom actions have more work: runs.using: node24, a freshly built distribution bundle and a published release tag. GitHub also notes that Node 24 narrows support for older self-hosted environments: macOS 13.4 and earlier and ARM32 are not officially supported. The runner inventory belongs in the same change.
Decide the version in one place
Good configuration makes drift visible. The engines field in package.json is a useful contract for hosting and package tools. A .nvmrc or .node-version provides the same local starting point. CI should either read that source or explicitly verify that both values agree.
A minimal target state looks like this:
- package.json: Node 24 as the production contract;
- local version file: the same major;
- CI: the same version for install, test and build;
- Docker: a matching, deliberately pinned Node 24 image;
- hosting: no conflicting manual setting;
- documentation: one command to activate and one command to verify.
A health or diagnostic endpoint can temporarily expose process.version so the team can verify the running runtime. That information does not need to remain public. A protected internal check or an unambiguous startup log is enough for rollout.
Do not use caches as evidence
A cache accelerates known inputs. It should not smuggle an old runtime into a new build. Its key should include at least operating system, architecture, Node major and lockfile.
For the first Node 24 run, we would deliberately start dependency and build caches empty. A new cache can be populated afterwards. That costs minutes but avoids the far more expensive question of whether the build passed only because it reused precompiled artifacts from Node 20.
The same applies to Docker layers, Turborepo or Next.js caches and native binary packages. A runtime migration is a cache boundary.
Deployment needs a way back
A safe rollout begins not with “Deploy” but with an answer to: what happens if errors appear only under real traffic?
Before the change, record:
- the last known-good deployment;
- how to reactivate it without rebuilding;
- whether database changes can be rolled back independently;
- which metrics and error rates will be watched;
- who has authority to stop the rollout.
Where the platform allows it, Node 24 should go to Preview or Staging first, then a small traffic share, and only then all production traffic. Rolling back to an existing older artifact can buy time. It does not solve the runtime problem, because a fresh Node 20 deployment remains blocked. The window is for repair, not postponement.
A container is a bridge, not an upgrade
For teams that missed the deadline, Vercel describes a container as an escape route. A custom image can still contain Node 20 because the platform's runtime setting does not apply inside that container.
Technically, that can unblock a deployment. Organisationally, the team now owns the base image, operating-system packages and runtime security. The Node.js project explicitly warns that EOL lines receive no security fixes and face toolchain breakage and growing ecosystem incompatibility.
This bridge therefore needs an expiry date, an owner and an upgrade ticket. “It runs in a container” is not a lifecycle strategy.
An upgrade pull request people can review
A focused pull request should contain:
- every runtime pin moved to Node 24;
- only necessary dependency changes;
- updated JavaScript actions;
- new or expanded smoke tests;
- cache keys that include the Node major;
- short migration and rollback notes;
- evidence from a clean build, tests and Preview.
It should not contain a framework migration, a new bundler, broad refactoring and a hundred unrelated package updates. The smaller the change, the easier it is to attribute a failure to runtime, dependency or deployment.
Runtime maintenance is product work
Node versions are not invisible infrastructure. They determine whether a hotfix can be built, whether dependencies receive patches and whether CI executes the same environment as production.
The current Node 20 shutdown demonstrates a familiar pattern: the old system looks stable until the next necessary change touches its obsolete foundation. Good teams do not wait for that moment. They inventory runtimes, build from scratch, test real entry points and plan the way back.
The goal is not merely a green Node 24 deployment. It is a repeatable upgrade process in which the next runtime release is no longer a surprise.
Sources



