Migrate Off a Deprecated AWS Lambda Runtime Without Downtime
A canary-based runbook for moving Lambda fleets to a supported runtime with instant rollback.
What you'll build / learn
You'll move a production AWS Lambda function from the newly deprecated nodejs20.x runtime to nodejs24.x with zero customer impact — pinning traffic to a published version, canarying the new runtime behind a weighted alias, gating the ramp on CloudWatch error metrics, and keeping a one-command rollback the whole time. The same runbook repeats for every function in your fleet.
Prerequisites
- AWS CLI v2 (verified against 2.36.x — check with
aws --version). CLI v1 handles binary payloads differently; use v2. - An AWS account with permissions for
lambda:*andcloudwatch:GetMetricStatistics. - A function on a deprecated runtime. Examples use
checkout-workeronnodejs20.x; swap in your function name and runtimes. - Deprecation dates, verified against the Lambda runtimes table (July 2026):
nodejs20.xdeprecated Apr 30, 2026;python3.9Dec 15, 2025;ruby3.2Mar 31, 2026. For all three, function updates get blocked Mar 3, 2027 — that's your hard deadline, because after it you can't even deploy the fix. - OS note: the monitoring step uses
date -u -v-30M(macOS/BSD). On Linux, usedate -u -d '30 minutes ago'instead.
1. Inventory every function on the deprecated runtime
Trusted Advisor emails only list $LATEST versions, so pull the full list yourself, per region:
aws lambda list-functions --function-version ALL --region us-east-1 \
--output text --query "Functions[?Runtime=='nodejs20.x'].FunctionArn"
Repeat for each region (and account) you run in. Prioritize by invocation volume — migrate the busiest functions first, since they're the ones where an unpatched runtime bug hurts most.
2. Pin production traffic behind an alias
Weighted routing works only between two published versions — an alias doing traffic shifting can't reference $LATEST — so first freeze the current, known-good build as an immutable rollback target:
BASE=$(aws lambda publish-version --function-name checkout-worker \
--description "nodejs20.x baseline" --query Version --output text)
echo "baseline version: $BASE"
aws lambda create-alias --function-name checkout-worker \
--name live --function-version "$BASE"
Now point every trigger at the alias ARN (...function:checkout-worker:live) instead of the bare function ARN. For an SQS/Kinesis/DynamoDB event source mapping:
aws lambda update-event-source-mapping --uuid <mapping-uuid> \
--function-name arn:aws:lambda:us-east-1:123456789012:function:checkout-worker:live
For API Gateway or EventBridge, edit the integration/target to append :live. This is the step people skip and then wonder why canarying does nothing: invocations against the unqualified ARN hit $LATEST and bypass alias routing entirely. If you already deploy through an alias, skip ahead.
3. Upgrade the runtime and publish a candidate
$LATEST no longer serves traffic, so you can change its runtime safely:
aws lambda update-function-configuration \
--function-name checkout-worker --runtime nodejs24.x
aws lambda wait function-updated-v2 --function-name checkout-worker
CAND=$(aws lambda publish-version --function-name checkout-worker \
--description "nodejs24.x candidate" --query Version --output text)
echo "candidate version: $CAND"
aws lambda wait published-version-active \
--function-name checkout-worker --qualifier "$CAND"
The wait between update and publish matters: config updates are asynchronous, and publishing mid-update fails with a ResourceConflictException.
4. Regression-test the candidate directly
Before any live traffic touches it, invoke the candidate version with a recorded production payload. Save one as testdata/checkout-event.json:
{"orderId": "test-123", "amountCents": 4200}
aws lambda invoke --function-name checkout-worker --qualifier "$CAND" \
--payload file://testdata/checkout-event.json \
--cli-binary-format raw-in-base64-out /dev/stdout
Diff the response against the same invoke with --qualifier "$BASE". Wire this into CI if you have a payload corpus — a shell loop over saved events with response diffing catches most runtime-upgrade breakage (changed default encodings, removed Node APIs) before customers can.
5. Canary 5% of live traffic
Shift a small slice to the candidate. The alias keeps pointing at the baseline; the candidate gets a weight:
aws lambda update-alias --function-name checkout-worker --name live \
--function-version "$BASE" \
--routing-config "AdditionalVersionWeights={$CAND=0.05}"
flowchart LR
T[API GW / SQS / EventBridge] --> A[alias: live]
A -->|95%| B["v7 (baseline) — nodejs20.x"]
A -->|5%| C["v8 (candidate) — nodejs24.x"]
Both versions must share the same execution role and dead-letter-queue config, and note the split is probabilistic — at low request volume the observed percentage swings well away from 5%.
Now watch errors per executed version. Lambda emits metrics with an ExecutedVersion dimension for weighted-alias invocations:
aws cloudwatch get-metric-statistics --namespace AWS/Lambda \
--metric-name Errors --statistics Sum --period 300 \
--dimensions Name=FunctionName,Value=checkout-worker \
Name=Resource,Value=checkout-worker:live \
Name=ExecutedVersion,Value="$CAND" \
--start-time "$(date -u -v-30M +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
If the candidate's error sum stays at your baseline rate for a comfortable soak (an hour of real traffic is a reasonable floor), ramp by re-running update-alias with 0.25, then 0.5. If errors spike, roll back instantly — no redeploy, just drop the weight:
aws lambda update-alias --function-name checkout-worker --name live \
--routing-config AdditionalVersionWeights={}
6. Promote to 100%
Point the alias at the candidate and clear the routing config:
aws lambda update-alias --function-name checkout-worker --name live \
--function-version "$CAND" --routing-config AdditionalVersionWeights={}
The old version stays published, so rollback remains one update-alias away until you're confident. Then repeat steps 2–6 for the next function on your inventory list.
Verify it works
Confirm the alias serves the new runtime:
aws lambda get-function-configuration \
--function-name checkout-worker --qualifier live --query Runtime
Expected output:
"nodejs24.x"
During the canary phase, you can watch the split live — the invoke response reports which version ran:
for i in $(seq 1 20); do
aws lambda invoke --function-name checkout-worker --qualifier live \
--payload file://testdata/checkout-event.json \
--cli-binary-format raw-in-base64-out \
--query ExecutedVersion --output text /dev/null
done | sort | uniq -c
Expected output at a 5% weight (roughly — it's probabilistic):
19 7
1 8
Finally, re-run the inventory command from step 1: the function should no longer appear in the nodejs20.x list.
Troubleshooting
An error occurred (ResourceConflictException) when calling the PublishVersion operation: The operation cannot be performed at this time. An update is in progress for resource: arn:aws:lambda:...
You published a version while the runtime update was still applying. Run aws lambda wait function-updated-v2 --function-name checkout-worker, then publish again.
Invalid base64: "{"orderId": "test-123", "amountCents": 4200}"
AWS CLI v2 expects the payload base64-encoded by default. Add --cli-binary-format raw-in-base64-out to every invoke (or set cli_binary_format = raw-in-base64-out in ~/.aws/config).
Canary weight set, but 100% of traffic still hits the old version.
Your trigger invokes the unqualified function ARN, which resolves to $LATEST and bypasses alias routing. Check CloudWatch Logs START lines — each one prints Version: <n>. Repoint the event source mapping, API Gateway integration, or EventBridge target at the :live alias ARN (step 2).
aws lambda update-alias fails with InvalidParameterValueException when adding the weight.
The two versions must both be published and share the same execution role and DLQ configuration. If you changed the role along with the runtime, align it on both versions before splitting traffic.
Next steps
- Automate the ramp: AWS SAM's
AutoPublishAliasplus aDeploymentPreferencelikeLinear10PercentEvery2Minuteshas AWS CodeDeploy run this exact runbook — including automatic rollback on a CloudWatch alarm. - Track the runtime deprecation schedule and plan the next round early:
python3.10anddotnet8deprecate in late 2026, and Lambda emails you at least 180 days out. - Container-image functions get no deprecation emails at all — audit base images on the same cadence.
Sources & further reading
- Lambda runtimes and deprecation schedule — docs.aws.amazon.com
- Implement Lambda canary deployments using a weighted alias — docs.aws.amazon.com
- Retrieve data about Lambda functions that use a deprecated runtime — docs.aws.amazon.com
- aws lambda wait - AWS CLI Command Reference — docs.aws.amazon.com
Emeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read.
Discussion 0
No comments yet
Be the first to weigh in.