Skip to main content
Logo
Overview

AWS Lambda Node.js 20 End of Support: Migrate to Node 22

July 3, 2026
9 min read

If you’ve got Lambda functions running on nodejs20.x, you’re on a clock. AWS stopped patching that runtime on April 30, 2026, you can’t create new functions on it anymore, and the one that actually bites — you lose the ability to update those functions — lands on September 30, 2026.

That last date is the one that matters. After it, a function on nodejs20.x keeps running and can still be invoked, but you can’t push a code change or tweak its config. So the day you need to ship a hotfix for a production bug, you’ll be stuck upgrading the runtime and fixing the bug at the same time, under pressure, at the worst possible moment. I’ve been on the wrong end of that exact situation with an old Python runtime. It’s not fun.

The good news is the migration to Node.js 22 is usually boring, which is what you want. Most of the work is finding every function and changing a runtime string. Let me walk through the timeline, how to inventory what you’ve got, and the runtime change in each of the common IaC tools.

The three-phase timeline (and what still works after)

AWS deprecates Lambda runtimes in stages rather than pulling the plug all at once. For nodejs20.x:

  • April 30, 2026 — Security patches and OS updates stop. Your functions are now running on a frozen, unsupported runtime. You also lose eligibility for AWS technical support on anything runtime-related.
  • June 1, 2026 — You can no longer create new functions on nodejs20.x. Existing ones are untouched. This date has already passed, so if you tried to spin up a new nodejs20.x function today it’d be rejected.
  • September 30, 2026 — You can no longer update existing functions on nodejs20.x. Code deploys, memory changes, environment variable edits, layer swaps — all blocked with a InvalidParameterValueException.

After September 30, functions still execute. Invocations work, triggers fire, nothing goes dark. But the function is effectively frozen in amber. You can delete it and you can invoke it, and that’s about it.

This same schedule already played out for Node.js 18 and 16 (AWS extended those to March 2026), so the pattern is well understood. What’s different this time is that a lot of teams standardized on Node 20 when it was the shiny new LTS, which means the blast radius is bigger than the 18/16 cutoff was.

Find every nodejs20.x function first

You can’t migrate what you can’t see. The annoying part of Lambda cleanup is always that functions sprawl across accounts and regions, and half of them were deployed by someone who left two years ago.

The fastest single-account, single-region check is the CLI:

aws lambda list-functions \
  --query "Functions[?Runtime=='nodejs20.x'].[FunctionName,LastModified]" \
  --output table

Regions are the trap here. Lambda is regional, so that command only sees us-east-1 (or whatever your default is). Loop over the regions you actually use:

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

For anything beyond a couple of accounts, don’t do this by hand. AWS Config has a managed rule that flags deprecated runtimes, and a Config aggregator gives you an org-wide view in one query. If you’re already running Config across your accounts, that’s the least-effort path — you get a single list instead of a for-loop per account.

And don’t forget the functions that don’t show up in the console the way you expect: Lambda@Edge functions (they replicate out of us-east-1), custom CloudFormation resources, and functions buried inside CDK constructs or third-party modules. Those last ones are the sneaky ones — a function you never wrote directly, pinned to nodejs20.x by a dependency’s default.

The most reliable source of truth, honestly, is your IaC. Grep it:

grep -rn "nodejs20.x" . --include="*.ts" --include="*.yaml" --include="*.yml" --include="*.tf" --include="*.json"

If a function’s runtime isn’t defined in code somewhere, that’s a second problem worth noting down while you’re here.

Changing the runtime string, per tool

Here’s the part that’s genuinely simple. In most projects, upgrading is a one-token change.

AWS SAM / CloudFormation — set it globally so you don’t miss one:

Globals:
  Function:
    Runtime: nodejs22.x

Or per-function under Properties. Then sam build && sam deploy. If you were relying on the SAM CLI’s Node version for local builds, make sure your local Node matches — building a native dependency against Node 20 and deploying it to a Node 22 runtime is how you get “works on my machine, crashes in prod.”

AWS CDK — change the runtime enum:

new lambda.Function(this, 'MyFn', {
  runtime: lambda.Runtime.NODEJS_22_X,
  // ...
});

Two CDK-specific gotchas. First, older CDK versions don’t have NODEJS_22_X in the enum at all — you may need to bump aws-cdk-lib first. Second, CDK ships its own Lambda functions for custom resources and log retention, and those were pinned to Node 20 in older releases. Upgrading the CDK library version is what fixes those, not editing your own code. If you’re on a CDK version from early 2025, upgrade the library and re-synth before you go hunting for stragglers.

Terraform — one attribute:

resource "aws_lambda_function" "my_fn" {
  runtime = "nodejs22.x"
  # ...
}

terraform plan should show a clean in-place update. If the plan wants to replace the function or shows a diff on the deployment package, that usually means the archive hash changed — check that you’re not accidentally rebuilding against a different Node version in the same commit.

Serverless Framework — set the provider default:

provider:
  name: aws
  runtime: nodejs22.x

The pattern across all four is the same: change the string, redeploy, done. The migration risk isn’t in the IaC change. It’s in whether your code runs clean on Node 22.

Breaking changes worth checking before you ship

For most functions, Node 20 to 22 is a non-event. But “most” isn’t “all,” so here’s where things actually break.

Native modules and Lambda layers. Anything with a compiled binary component — sharp, bcrypt, better-sqlite3, node-gyp modules — is compiled against a specific Node ABI. Those need to be rebuilt for Node 22. If you package deps in a Lambda layer, that layer has to be recompiled and re-published; a layer built for Node 20 can throw cryptic NODE_MODULE_VERSION mismatch errors at cold start. Rebuild layers on a Node 22 base (or use a Docker build image that matches) before you flip the function runtime.

AWS SDK v3 baked into the runtime. The Lambda Node 22 runtime bundles a different pinned version of AWS SDK v3 than Node 20 did. If your code imports the SDK from the runtime instead of bundling its own copy, you’re now getting a newer SDK version than you tested against. AWS has aligned the SDK for JavaScript with the Node.js release schedule, so the drift is intentional and predictable — but “predictable drift” is still drift. The safe move for anything important is to bundle your own SDK version and not rely on the runtime’s copy at all. If you do rely on the runtime SDK, skim the changelog for the clients you use.

ESM vs CommonJS. Node 22 is stricter and more ESM-forward than 20. If you’ve got a mix of require and import, or a package.json without an explicit "type" field, this is a good moment to make it explicit. It’s rarely a hard blocker, but ambiguous module resolution is exactly the kind of thing that behaves differently across minor Node versions.

Handler signatures. Async handlers that return a promise are the safe path and always have been. Callback-style handlers still work on Node 22, but they’re on borrowed time — on the Node 24 runtime, callback-based handlers are deprecated outright. If you’re touching this code anyway, converting (event, context, callback) to async (event, context) now saves you the same migration later.

None of these are exotic. But they’re the difference between a runtime bump that’s invisible and one that pages you at 2am because sharp won’t load.

Test, canary, then move on

Don’t flip every function on a Friday afternoon. The sane rollout:

Run your test suite on Node 22 locally first — if your CI is still on Node 20, that’s a separate thing to bump, and you want the tests green on the version you’re actually deploying to. Then pick one low-traffic, non-critical function and migrate it end to end. Watch its CloudWatch metrics for a day: cold start duration, error rate, and init duration especially, since a botched native-module rebuild shows up as init failures.

Lambda aliases and weighted routing let you shift traffic gradually if a function is critical enough to warrant it — publish a new version on Node 22, point 10% of traffic at it, and ramp up as the error rate stays flat. For most functions that’s overkill and a straight redeploy after a good test run is fine. Save the canary ceremony for the handful of functions where a bad deploy actually costs you.

Where this goes next: plan for Node 24, not just 22

Node.js 22 buys you time, but not a lot of it. It’s in maintenance LTS and its Lambda support runs through April 2027 — which means the exact deprecation dance you’re doing now repeats roughly a year from your migration. That’s a short runway.

Node.js 24 has been available as a Lambda runtime since November 2025 (nodejs24.x), it’s in active LTS, and its support runs to around April 2028. So the real question is whether you jump straight to 24 and skip a migration cycle.

My honest take: if your functions are simple and your dependencies are current, go to 24 now and give yourself two years instead of one. The catch is that Node 24 is where callback handlers get deprecated and the Runtime Interface Client was reimplemented, so there’s slightly more surface area for something to behave differently. If you’ve got a big fleet, a lot of native deps, or code you don’t fully trust, Node 22 is the lower-risk landing spot and you take the 24 hop later when you have breathing room. Either way, standardize on one target across your org so you’re not managing three runtime versions at once — that fragmentation is how you end up right back here with a mixed fleet and another deadline.

The move today is small: run the inventory, count what’s on nodejs20.x, and get that number in front of whoever owns the September calendar. The migration is easy. Getting caught by the cliff with a production bug you can’t deploy a fix for is not.

Sources: