Search nomadLab

Node 20 Is Past End of Life: Two Platforms, Two Clocks

Node 20 stopped getting patches on 30 April 2026. GitHub Actions forced every action onto Node 24 seven weeks later. AWS Lambda pushed its enforcement out to March 2027. Neither date is the one that was announced last year.

Updated

Node.js 20 reached end of life on 30 April 2026. That date comes from the release schedule in the nodejs/Release repo, it has been in there for years, and it is the least interesting date in this post.

What matters is what each platform you deploy to decided that date meant. GitHub gave you seven weeks. AWS gave you ten months, after quietly moving its own deadline. Neither of them is running on Node’s calendar, and neither of them is running on the calendar they published a year ago.

Time from Node 20's end of life to each platform's enforcement date. GitHub Actions forced all actions onto Node 24 on 16 June 2026, 47 days after end of life. AWS Lambda blocks updates to nodejs20.x functions on 3 March 2027, 307 days after end of life. One dead runtime, two enforcement calendars Node 20 EOL, 30 Apr 2026 GitHub Actions 47 days, then every action runs on Node 24 AWS Lambda 307 days, then function updates are blocked today Apr 2026 Apr 2027
Dates from the GitHub Actions changelog and the Lambda runtime table, checked 22 August 2026. Both platforms moved these after first announcing them.

GitHub Actions changed two different Node versions, and people keep merging them

There are two Node runtimes in a workflow run and they moved on separate schedules.

The first is the node on the runner image, the one a run: step gets when it calls node or npm. Node 20 was removed from the toolcache on every image and the default moved to Node 22, rolled out between 19 and 26 May 2026. If a job compiles or tests against whatever node happens to be there, it has been on 22 since May whether you noticed or not. Pin it with setup-node and the question goes away.

The second is the Node that executes the actions themselves, declared in each action’s action.yml as runs.using: node20 or node24. GitHub’s changelog set 16 June 2026 as the day the runner starts executing node20 actions on Node 24 regardless of what their metadata says.

That flip is why so many teams think they are done. The jobs pass, so the box is ticked. But the deprecation warning is keyed to the metadata field, not to which binary ran the code, which produces three states worth telling apart:

What you didRuns onWarning
Old action version declaring node20Node 24, via the forced defaultStill there
Bumped to a version declaring node24Node 24Gone
Set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=trueNode 24, earlier than the defaultStill there

That last row catches people who reach for the environment variable expecting it to clean up their logs. It does not. It moves you onto Node 24 ahead of schedule, which is useful for finding breakage on your own timetable, and that is all it does.

The other lever, ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true, goes the other way and keeps a node20 action on Node 20. GitHub’s changelog says it works only “until we upgrade the runner and remove Node20 later in the fall of 2026.”

Which is the whole date, as published. GitHub has not named a day. If you have seen 16 September 2026 quoted for this, it is not in the changelog and it is not in the community thread where people keep asking, where the answer is that no final removal date has been announced. Budget for autumn and stop looking for a number that does not exist yet.

The fix for most repos is still twenty minutes of version bumps. As of 22 August 2026 the current majors are actions/checkout v7.0.1, actions/setup-node v7.0.0, actions/cache v6.1.0, actions/upload-artifact v7.0.1, and actions/github-script v9.0.0. Pin to the release SHA rather than a floating major, then find the stragglers you never wrote down:

grep -rhoE 'uses:\s*\S+' .github/workflows/ | sed -E 's/uses:\s*//' | sort -u

A warning that survives all of that is coming from a composite action pulling in something else. The annotation names it, so read the whole line before going hunting.

Keep the two Node versions separate in your head while you work. Bumping setup-node’s major fixes the action machinery. Setting node-version: is what decides which Node your own tests run on, and those two have nothing to do with each other.

AWS moved the Lambda cliff ten months to the right

Lambda’s runtime deprecation normally runs on a fixed cadence: no more patches on the deprecation date, no new functions 30 days later, no updates to existing functions 60 days after that. For nodejs20.x that arithmetic gave 30 September 2026, which is the date that got written into a lot of migration tickets last summer.

It is not the date any more. The Lambda runtimes table now shows nodejs20.x deprecated on 30 April 2026, block function create on 1 February 2027, and block function update on 3 March 2027, with a note saying AWS is delaying those dates past the usual 30 and 60 days in response to customer feedback. Every deprecated runtime in that table got the same treatment: Python 3.9, Node 18, Ruby 3.2, all pushed to the same February and March 2027 pair.

So the thing that would have forced your hand in five weeks is now nine months out, and the practical situation is worse rather than better. You can still deploy. Nothing will stop you. Meanwhile the runtime under those functions has had no security patches since April, and the only mechanism that was going to make anyone care about that has been postponed.

Find what you have, per region, because Lambda is regional and the console will happily show you an empty list from the wrong one:

for region in us-east-1 us-west-2 eu-west-1 ap-northeast-2; do
  echo "=== $region"
  aws lambda list-functions --region "$region" \
    --query "Functions[?Runtime=='nodejs20.x'].FunctionName" --output text
done

Then grep your infrastructure code for nodejs20.x, because the functions that bite are the ones nobody typed. CDK ships its own Lambdas for custom resources and log retention, and older versions of aws-cdk-lib pin them to Node 20; bumping the library is what fixes those, not editing your stacks. Lambda@Edge functions replicate out of us-east-1 and hide from a per-region sweep. Anything a third-party construct created is a function you own and did not write.

Where to land, given the dates that come next

RuntimeNode upstream endLambda blocks updates
Node 2030 Apr 20263 Mar 2027
Node 2230 Apr 20271 Jul 2027
Node 2430 Apr 20281 Jul 2028
Node 2630 Apr 2029not scheduled

Node 22 is already in maintenance and has eight months left. Landing there buys you a repeat of this exercise next spring, which is a reasonable trade only if something in your dependency tree genuinely cannot run on 24 yet.

Node 24 is the landing spot. It goes to maintenance on 20 October 2026, a detail worth knowing but not worth waiting on, since maintenance still means patches until April 2028. Node 26 enters active LTS on 28 October 2026 and AWS lists nodejs26.x for November, currently in public preview and explicitly not covered by the Lambda SLA. Preview is not where production goes.

What actually breaks on 24

Native addons are the reliable source of pain. Anything with compiled bindings is built against a specific ABI, so sharp, bcrypt, better-sqlite3 and friends need rebuilding, and a Lambda layer built for Node 20 throws NODE_MODULE_VERSION errors at cold start rather than at deploy time. Rebuild layers on a matching base before you flip any function’s runtime string.

Two things specific to the Lambda runtime. Callback-style handlers are gone: AWS states that “Starting with the Node.js 24 runtime, Lambda no longer supports the callback-based handler signature for asynchronous operations”, and context.callbackWaitsForEmptyEventLoop, context.succeed, context.fail and context.done were removed with it. And the runtime bundles its own copy of AWS SDK v3, which is a different version from the one Node 20 carried. AWS recommends bundling the SDK modules you use rather than relying on the runtime’s copy, which is also the only way to know what version you are testing against.

On the CI side, Node 24 changed what it builds on. MSVC support was dropped in favor of ClangCL on Windows, the macOS minimum moved to 13.5 with Xcode 16.1, ARMv7 dropped to experimental, and 32-bit s390 and ppc are gone. Hosted runners handle all of that for you. Self-hosted runners on old Mac hardware or a Raspberry Pi do not, and that is a hardware conversation, not a version bump.

The rest is the usual: a stricter Undici behind global fetch, and require() of an ES module throwing ERR_REQUIRE_ASYNC_MODULE when that module has a top-level await. Both surface fastest if you run the suite on 24 before you decide anything, which costs one matrix entry.

The pattern under all of this is worth keeping after the Node 20 chore is done. The end-of-life date belongs to the language, and it is the one date nobody enforces. What actually stops your deploy is a row in a vendor’s table, written by a team balancing your migration against their support load, and both of the vendors here moved that row after publishing it. Read the table, not the blog post about the table, and read it again before you promise anyone a date.

Keep reading