Node.js Migration & End-of-Life Modernization
Node.js retires every major release on a published schedule, so a legacy version doesn't have a future even though it still runs — Node 16 reached end of life in September 2023, Node 18 on April 30, 2025, and Node 20 on April 30, 2026, leaving Node 22 and 24 as the supported lines. An end-of-life Node version stops getting security patches while its dependencies keep moving. Migrating means moving the application onto a supported LTS, ideally slice by slice with each proven equivalent before cutover.
A legacy Node.js application is rarely just an old runtime. It’s a package.json pinned years ago, a deep node_modules tree where half the packages have since moved on, native modules built against an old ABI, and a body of code that quietly relies on behavior or APIs that have changed or been removed. That’s why “just bump the Node version” is never the whole job — and why staying on an end-of-life Node release is a problem that gets worse on two sides at once, not one that holds steady.
How Node.js’s release clock actually works
Unlike a product with a single retirement date, Node.js retires every release line on a published, predictable cadence:
| Version | End of life | Status |
|---|---|---|
| Node.js 16 | September 11, 2023 | End of life — no patches |
| Node.js 18 | April 30, 2025 | End of life — no patches |
| Node.js 20 | April 30, 2026 | End of life — no patches |
| Node.js 22 | April 30, 2027 | Maintenance LTS — supported |
| Node.js 24 | Active LTS | Supported — a current target |
Dates are from the Node.js project’s release schedule. Each even-numbered version becomes an LTS line for roughly 30 months — about 12 months active, then 18 in maintenance — and then it’s done. The line that matters is the same for each retired version: once a release reaches end of life, no security update ships for a vulnerability found the next day. Node 16, 18, and 20 are all past that line; 22 and 24 are the supported targets. A Node version past its date isn’t “old but fine” — it’s a runtime the maintainers have stopped patching, on a clock that was always public.
What end of life actually means for a Node.js app
Node’s fixed cadence raises the stakes over a one-off version bump, for three reasons.
- The ecosystem moves against you. It isn’t only the runtime that ages — the npm packages your app depends on keep releasing, and newer versions routinely drop support for old Node lines. Stay put and your dependency tree rots: you can’t take security fixes in your libraries without also moving the runtime they now require.
- It collides with compliance. PCI DSS, SOC 2, and HIPAA expect runtimes in scope to be patchable. An end-of-life Node version in a regulated data path is a finding waiting to be written, with no patch line to point to — and Node’s public schedule means there’s no ambiguity about whether you’re past the date.
- The jump compounds. Skip one LTS and the move is routine; skip three and you’re crossing years of breaking API changes, removed globals, and native modules that need rebuilding all at once. The migration that was small last year is larger now, and larger again next year.
The migration options
There is no single right move; there’s a right move for your application, and it turns on how many major versions you’re crossing and how much your dependencies have drifted.
- Move to the next supported LTS when you’re only a line or two behind and the dependencies come along cleanly. The cheapest path — mostly testing and a handful of API adjustments — and often the honest answer for a well-maintained service.
- Upgrade the dependency stack alongside the runtime when key packages changed dramatically or stopped supporting your Node line. The runtime move is the easy part; replacing or re-implementing the packages that didn’t keep pace is where the real work is.
- Re-platform the parts that fight the move where native modules, abandoned packages, or reliance on removed Node behavior block a straight upgrade. Those slices need genuine re-implementation, and the bulk of the difficult work lives here.
The decision is rarely about the Node version number — it’s about the dependency tree and the runtime behaviors the code relies on. That coupling is what turns a version bump into a modernization project.
Modernizing off it, slice by slice
We treat the application the same way we treat any legacy system: not a single risky cutover, but a sequence of small, reversible steps.
A strangler facade lets the legacy Node.js application and the supported-runtime path run side by side during the move. We break the application into slices — a service, a module, a bounded workflow — and before any slice carries live work, we prove it behaves identically to the legacy: same results, same outputs, reconciled against the original. AI-accelerated discovery reads the code, the dependency tree, and the native-module assumptions end to end and captures what the application actually does — including the undocumented behavior and the version-specific quirks that are easy to miss — under senior-engineer review. Work shifts only on green, rollback stays a flag away, and the end-of-life runtime is retired only once nothing depends on it.
When an LTS jump is a DIY job
Not every Node upgrade needs a partner, and we’ll say so. A service that’s only one LTS behind, with a clean dependency tree and a solid test suite, is often a job a capable in-house team can run in an afternoon — and we’ll tell you that on the call rather than manufacture a project. The slice-by-slice approach earns its place when the application is several major versions behind, when critical dependencies never kept pace, when native modules or removed runtime behavior block a straight upgrade, or when the application can’t take downtime. Matching the effort to the actual difficulty is part of the assessment, not an afterthought.
Where to start
The first step is small and bounded: understand the application before committing to a path. A discovery call scopes how far behind the Node version is, which dependencies have stopped supporting it, where the compliance exposure sits, and whether the right move is a straightforward LTS jump or a deeper migration — on evidence, not a sales pitch. Reach the team at sales@modernlift.ai.
Frequently asked questions
- When do Node.js versions reach end of life?
- On a fixed cadence published by the Node.js project. Node 16 reached end of life on September 11, 2023, Node 18 on April 30, 2025, and Node 20 on April 30, 2026. Node 22 is in maintenance LTS through April 30, 2027, and Node 24 is the current active LTS. Each even-numbered LTS line gets roughly 30 months total — about 12 months active, then 18 in maintenance — after which no security patches ship.
- Why can't we just keep running an old Node.js version?
- Because both ends move against you. Once a version is end of life, no security fix ships for a vulnerability found the next day, and meanwhile the npm ecosystem keeps advancing — newer package versions drop support for old Node, native modules stop providing compatible binaries, and your dependency tree quietly rots. The longer you wait, the larger the eventual jump and the more breaking changes pile up between you and a supported line.
- How hard is a Node.js version migration?
- It depends on how many major versions you're crossing and how much your dependencies have moved. Jumping one LTS is often routine; crossing several at once means dealing with breaking API changes, deprecated globals, native-module rebuilds, and packages that changed dramatically or disappeared. Done incrementally, each service or module is moved to the target runtime and proven to behave identically before cutover, rather than flipping the whole application at once.
- Who provides Node.js migration services?
- We do — Node.js migration and modernization services are part of what we do. We move a legacy Node application onto a supported LTS slice by slice, bringing the dependency tree along and proving each service or module equivalent to the legacy before it carries live work. We sell the migration itself, not licenses or tools, and we'll tell you when a one-LTS jump is a job your own team can run rather than manufacture a project.
- How much does a Node.js migration cost?
- It scales with the application, not a list price. The drivers are how many major versions you're crossing, how far the dependency tree has drifted, how many slices the application breaks into, the native modules that need rebuilding, and how much parity has to be proven before each cutover. Our [legacy cost calculator](/legacy-cost-calculator) gives a directional estimate, and a [discovery call](/meet) scopes the real number against your application.
- How do I choose a Node.js modernization partner?
- Judge a partner on how they de-risk the move, not how fast they promise to finish. Look for incremental, slice-by-slice migration over a single risky cutover; proof that each slice behaves identically to the legacy before work shifts; senior-engineer review of AI-accelerated discovery so the version-specific quirks aren't missed; and the honesty to say when a straightforward LTS jump is all you need. Matching the effort to the actual difficulty should be part of the assessment, not an upsell.