Node.js 20 stopped receiving security fixes on 30 April 2026. If a server, container image or CI job still runs it, every new Node vulnerability from now on is one you carry yourself. The October 2026 security releases, for example, patched the 22.x, 24.x and 26.x lines, and Node 20 was not among them. This guide shows what changes between Node 20 and Node 24, the handful of things that actually break, how to find them in your code before you switch, and how to roll the upgrade out.
Which version should you move to?
| Version | Status (end of October 2026) | Security support ends |
|---|---|---|
| Node 20 | End of life | 30 April 2026 |
| Node 22 | Maintenance LTS | 30 April 2027 |
| Node 24 "Krypton" | Maintenance LTS since 20 October 2026 | 30 April 2028 |
| Node 26 "Lithium" | LTS since 28 October 2026 | 30 April 2029 |
Dates are from the Node.js release schedule (the nodejs/Release repository) and match endoflife.date. Node 24 is a solid target for an existing application: it has been an LTS line since October 2025, so tools and libraries have had time to add support. Node 26 has just entered LTS and gives you the longest support window, so it is a reasonable choice for new projects, as long as your dependencies (especially native ones) support it. This article is about 20 to 24, and it notes where Node 26 differs.
What changes between Node 20 and Node 24
I installed Node 20.20.2, 24.22.2 and 26.13.0 side by side and read these values from process.versions:
| Node 20.20.2 | Node 24.22.2 | Node 26.13.0 | |
|---|---|---|---|
| V8 | 11.3 | 13.6 | 14.6 |
| OpenSSL | 3.0.19 | 3.5.9 | 3.5.9 |
Undici (fetch) | 6.24 | 7.30 | 8.12 |
Native module ABI (process.versions.modules) | 115 | 137 | 147 |
The Node 24 release notes add npm 11 (Node 20 ships npm 10), and these new capabilities, which I confirmed are missing in Node 20 and present in 24:
-
URLPatternis a global (typeof URLPatternisundefinedin 20 andfunctionin 24). -
Float16Array,RegExp.escape()andError.isError()exist in 24 and not in 20. - The permission model flag is now
--permission. -
AsyncLocalStorageuses a more efficient implementation by default.
One thing that is not new: require() of an ES module already works on Node 20.20.2. I tested it, and it returned the module without a flag. If you were avoiding that on Node 20, check your own version; on current 20.x releases it works.
What actually breaks
Most applications need no code changes. The cases below are the ones I could reproduce, each tested on Node 20 and Node 24.
| Code | Node 20 | Node 24 | Fix |
|---|---|---|---|
tls.createSecurePair() | Function exists | Removed (undefined, calling it throws) | Use tls.TLSSocket |
dirent.path from readdir(..., {withFileTypes: true}) | String | Removed (undefined) | Use dirent.parentPath |
--experimental-permission flag | Works | "bad option", the process will not start | Use --permission |
url.parse() | Silent | Runtime deprecation warning DEP0169 | Use new URL() |
fs.F_OK, R_OK, W_OK, X_OK | Silent | Warning DEP0176 (still works) | Use fs.constants.F_OK etc. |
spawn() / execFile() with an args array and shell: true | Silent | Warning DEP0190 | Drop shell: true, or build the command string yourself |
The first three can stop your app or script. The rest only print warnings today but are on the road to removal, and Node 26 shows how that ends: in Node 26.13.0, fs.F_OK is already undefined, so code reading it no longer works at all. The Node 24 release notes list other removals and deprecations, such as SlowBuffer, Zlib classes used without new, and 32-bit s390 and ppc support, but those are rare in application code. If you build Node from source, the release notes also raise the minimum macOS to 13.5 and require ClangCL on Windows.
The fixes in code
// url.parse() -> WHATWG URL const u = new URL(req.url, 'http://localhost'); u.hostname; u.port; u.pathname; u.searchParams.get('y'); // dirent.path -> dirent.parentPath const full = require('node:path').join(entry.parentPath, entry.name); // fs.F_OK -> fs.constants.F_OK fs.access(file, fs.constants.F_OK, cb); // spawn without shell: arguments are passed safely, no quoting needed require('node:child_process').spawnSync('echo', ['hi']); One difference to watch with url.parse(). It can return the query as an object when you pass true as the second argument, while new URL() gives you searchParams instead. Replace those lines by hand rather than with a search and replace.
Find the problems before you switch
1. Search the code
grep -rnE "url\.parse\(|\.createSecurePair|fs\.(F_OK|R_OK|W_OK|X_OK)|\{ *(F_OK|R_OK|W_OK|X_OK)|new Buffer\(|\bSlowBuffer\b|experimental-permission|\b(dirent|entry|ent|e)\.path\b|shell: *true" \ src scripts package.json Dockerfile .github --include=*.js --include=*.mjs --include=*.cjs --include=*.ts --include=package.json --include=Dockerfile --include=*.yml I ran this against a sample file and it matched each old usage and skipped the modern ones such as fs.constants.F_OK and parentPath. It will also report false positives, like a variable named e.path that has nothing to do with fs, so read each hit. Your dependencies can also trigger these warnings, which the next step covers.
2. Run your tests with deprecations turned into errors
NODE_OPTIONS=--throw-deprecation npm test With --throw-deprecation, a deprecated API throws instead of warning, so the first one fails the run. I confirmed the process exits with code 1. To find where a warning comes from, whether in your code or in node_modules, use:
node --trace-deprecation app.js This prints the stack for each deprecation warning. For stricter checks, --pending-deprecation turns on extra deprecation warnings, such as the one for new Buffer() (DEP0005), which I saw with that flag.
3. Rebuild native dependencies
Packages with compiled addons, such as image, database or hashing libraries, are built for one ABI version. Node 20 is ABI 115 and Node 24 is 137, so the old binaries will not load. Start from a clean install under the new version:
rm -rf node_modules npm ci npm rebuild # only needed if an addon did not install a prebuilt binary for Node 24 If an addon fails to build or load, check that package's release notes for Node 24 support and update it. Do the same for your build tools (Vite, webpack, test runners): each documents the Node versions it supports.
Roll it out
Local and server
# nvm nvm install 24 nvm alias default 24 node -v # fnm fnm install 24 && fnm default 24 # pin it for the project echo "24" > .nvmrc On Debian or Ubuntu servers installed through NodeSource, switch the repository to the Node 24 setup script and reinstall the package. Restart every long-running process afterwards, because a running process keeps using the Node binary it started with. Check the version a running service really uses, not just the one on your shell's PATH.
package.json
{ "engines": { "node": ">=24" } } With engines set, npm warns when someone runs the project on an older Node. Add engine-strict=true to .npmrc if you want it to refuse instead.
Docker
FROM node:24-slim WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY . . CMD ["node", "server.js"] Change the tag in every Dockerfile, including build stages, and rebuild without cache so npm ci compiles native modules against the new Node.
CI
# GitHub Actions - uses: actions/setup-node@v4 with: node-version: 24 Use the current major version of the action. Update any test matrix that still lists 20, and keep 22 and 24 in the matrix while you migrate.
A safe order of work
- Run the grep and the
--throw-deprecationtest run on Node 20 itself to see what you will hit, then fix the code. All the fixes above work on Node 20 too, so you can ship them first. - Switch CI to Node 24 and get the test suite green.
- Deploy to staging with the new Node and watch logs for deprecation warnings and native module errors.
- Roll production forward one service or one instance group at a time, so you can roll back by redeploying the old image.
Frequently asked questions
Is Node.js 20 still safe to use?
No. It reached end of life on 30 April 2026, so it no longer receives security fixes. Vulnerabilities found since then are not patched in the 20.x line.
Should I upgrade to Node 24 or Node 26?
Either is supported. Node 24 is the safer choice for an existing app because the ecosystem has had a year with it. Node 26 became LTS on 28 October 2026 and is supported until 30 April 2029, so consider it for new projects after checking that your native dependencies support it.
Can I skip from Node 20 straight to 24?
Yes. You do not have to install 22 first, but the removals and deprecations of both releases apply at once, which is why it helps to run the checks above on Node 20 first.
Will my npm packages work on Node 24?
Pure JavaScript packages usually do. Packages with native addons must be reinstalled or rebuilt for the new ABI, and some need a newer version.
What happens to Node 24 after October 2026?
It is in Maintenance LTS: it still gets critical fixes and security updates, but not new features, until 30 April 2028.
Sources
- Node.js Release Working Group: release schedule
- endoflife.date: Node.js
- Node.js 24.0.0 release notes
- Node.js 26.13.0 "Lithium" reaches LTS
Originally published on DEV Talk.

