Published 2026-04-28. Updated 2026-05-21 — Phase 1 EOL has passed; this guide is now positioned as the cleanup path before the Sep 30 Phase 3 cliff (after which AWS blocks updates to existingnodejs20.xfunctions). For teams running AWS Lambda functions onnodejs20.x. Source: AWS Lambda runtimes official docs.
nodejs20.x (Phase 1).nodejs20.x (Phase 2).nodejs20.x (Phase 3, the hard cliff).nodejs22.x is a semver-breaking change. Most code works. A specific set of patterns will break. This post documents all of them.lambda-lifeline that automates the whole migration: scan, codemod, audit, IaC patch, canary deploy with auto-rollback.AWS runs a 3-phase deprecation process for every Lambda runtime. Phase 1 ends security patches. Phase 2 blocks creating new functions on the deprecated runtime. Phase 3 blocks *updating* existing functions. The phases are always spaced ~3-4 months apart, and once Phase 3 hits, your only option is a full deploy with a new runtime.
Node.js 20 itself is officially supported by the Node.js Foundation until April 2026 (Node.js release schedule). AWS Lambda aligns to the upstream EOL date, so April 30, 2026 is the start of the 3-phase countdown.
Node.js 22 is the next LTS. Upstream maintenance continues until April 2027. That's the target runtime.
These are the patterns that actually trip migrations. Most Node 20 code runs on Node 22 unchanged — these are the exceptions.
Node 20 accepted the stage-3 TC39 proposal syntax:
// Node 20 — valid
import config from './config.json' assert { type: 'json' };
const schema = await import('./schema.json', { assert: { type: 'json' } });
Node 22 implements the finalized stage-4 spec, which renamed assert to with:
// Node 22 — valid
import config from './config.json' with { type: 'json' };
const schema = await import('./schema.json', { with: { type: 'json' } });
The old syntax is a hard parse error in Node 22. Every JSON import and every CSS/WASM import statement in your codebase needs rewriting. This is the #1 breakage pattern.
Anything with a native .node binary needs a rebuild or a version bump. The packages that actually ship native bindings and historically lag:
| Package | Minimum Node 22-compatible version |
|---|---|
sharp | 0.33.0 |
bcrypt | 5.1.1 |
better-sqlite3 | 11.0.0 |
canvas | 2.11.2 |
node-sass | — *(dead project, switch to sass)* |
grpc | — *(dead, use @grpc/grpc-js)* |
fibers | — *(dead, use native async)* |
If your package.json pins any of these below the minimum, npm install on Node 22 will either fail to compile or crash at runtime.
Node 22 changed how custom CAs are loaded. If you set NODE_EXTRA_CA_CERTS on your Lambda environment, nothing changes — it still works. If you were relying on the old bundled cert store behavior without setting that env var, TLS connections to private CAs will suddenly fail. The fix is to always set NODE_EXTRA_CA_CERTS=/etc/pki/ca-trust/source/anchors/your-ca.pem in the function environment.
Buffer.prototype.toString() negative indicesNode 20 silently tolerated negative start/end arguments. Node 22 throws RangeError. If you have:
buf.toString('utf8', -10, -1);
That's now a runtime error. Switch to:
buf.toString('utf8', Math.max(0, buf.length - 10), buf.length - 1);
highWaterMark default changeThe default highWaterMark for object-mode streams dropped from 16 to 1. If you had code depending on backpressure thresholds, revisit it.
url.parse() is deprecated harderStill works, but now emits a warning per call. If your CloudWatch logs hit warning thresholds, this is worth rewriting. Use new URL(...) instead.
For a typical medium-sized company, you have:
package.json — dependency versions, especially native bindingsserverless.ymlRuntime references in GitHub Actions, CircleCI, GitLabTracking all of this by hand is where migrations stall.
Find every Lambda function on a deprecated runtime across every region. AWS doesn't give you this in one API call — you have to paginate through ListFunctions per region.
# With lambda-lifeline
npx lambda-lifeline scan --regions us-east-1,us-east-2,eu-west-1 --format json --out inventory.json
# With plain AWS CLI
for region in us-east-1 us-east-2 eu-west-1; do
aws lambda list-functions --region $region \
--query "Functions[?contains(Runtime, 'nodejs') && Runtime != 'nodejs22.x'].[FunctionName,Runtime,LastModified]" \
--output table
done
You will almost certainly find functions nobody on your team remembers deploying.
For the import assert → import with change, the naive sed approach:
find . -name '*.js' -o -name '*.mjs' -o -name '*.ts' | \
xargs sed -i 's/assert { type:/with { type:/g'
This over-triggers on assert(...) function calls. Use a proper AST tool or lambda-lifeline codemod which only matches the import-assertion grammar.
npx lambda-lifeline codemod src/ --apply
package.json native bindingsRun through your direct and transitive dependencies and check each one's Node 22 compatibility:
npx lambda-lifeline audit package.json
Output:
[high] sharp declared=^0.31.0 · needs >=0.33.0
[high] bcrypt declared=^5.0.0 · needs >=5.1.1
[critical] node-sass declared=^6.0.1 · no Node 22 support (dead project)
For SAM templates:
# Before
Runtime: nodejs20.x
# After
Runtime: nodejs22.x
For CDK (TypeScript):
// Before
runtime: lambda.Runtime.NODEJS_20_X,
// After
runtime: lambda.Runtime.NODEJS_22_X,
For Terraform:
# Before
runtime = "nodejs20.x"
# After
runtime = "nodejs22.x"
The lambda-lifeline iac command handles all four formats (SAM, CDK, Terraform, Serverless) including CloudFormation Globals: blocks and CDK enum references.
npx lambda-lifeline iac infrastructure/ --apply
The critical step. Do not cut over atomically. Use Lambda versions + weighted alias routing:
If any alarm trips at any stage, auto-rollback the alias to the stable version:
npx lambda-lifeline deploy \
--function payment-webhook \
--alias live \
--stages 5,25,50,100 \
--dwell 60 \
--alarm arn:aws:cloudwatch:us-east-1:1234:alarm:PaymentErrors \
--apply
The --alarm flag is required when --apply is set. No alarm, no deploy.
Always have a tested rollback. Manual rollback is just re-pointing the alias:
npx lambda-lifeline rollback --function payment-webhook --alias live --apply
Or with plain AWS CLI:
aws lambda update-alias \
--function-name payment-webhook \
--name live \
--function-version 47 # previous stable
(node:123) [DEP0XXX] DeprecationWarning: .... Clean these up before the next LTS.lambda-lifelineWe kept getting the same "your runtime is deprecated" email from AWS every 6-8 months. Each time, the migration work was unglamorous, had huge blast radius, and had no integrated tool. CloudQuery gives you inventory. AWS Migration Hub is for cross-region lift-and-shift. aws-samples gives you snippets. Nothing combined scan + codemod + IaC patch + tested deploy + rollback for a specific deprecation.
So we scoped it. lambda-lifeline only migrates Node.js Lambda runtimes. It does nothing else. It has 24 tests covering every command, every output format, every exit-code path. It defaults to dry-run. The canary deploy refuses to run without an alarm ARN.
Source: https://github.com/ntoledo319/EOLkits/tree/main/kits/lambda-lifeline
It's MIT-licensed. Fork it, use it, resell it.
*If you work at a company that runs Lambda on Node 20 and needs help migrating, the lambda-lifeline kit is free. The Team and Enterprise tiers add a printable PDF runbook, a captioned video walkthrough, expanded dependency tables, custom codemod rules for your codebase, and priority Slack support.*
Don't migrate by hand. The free EOLkits scanner finds every deprecated Lambda runtime and the dependency breaks above in your own config — in your browser, nothing uploaded. Then fix it with the MIT CLIs, or get a hash-anchored audit (30-day money-back) or a done-for-you migration PR.