Skip to content
Cloud & Infra Advanced Tutorial

Migrate Off a Deprecated AWS Lambda Runtime Without Downtime

A canary-based runbook for moving Lambda fleets to a supported runtime with instant rollback.

Emeka Okafor
Emeka Okafor
Security Editor · Jul 22, 2026 · 5 min read
Migrate Off a Deprecated AWS Lambda Runtime Without Downtime

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:* and cloudwatch:GetMetricStatistics.
  • A function on a deprecated runtime. Examples use checkout-worker on nodejs20.x; swap in your function name and runtimes.
  • Deprecation dates, verified against the Lambda runtimes table (July 2026): nodejs20.x deprecated Apr 30, 2026; python3.9 Dec 15, 2025; ruby3.2 Mar 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, use date -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 AutoPublishAlias plus a DeploymentPreference like Linear10PercentEvery2Minutes has 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.10 and dotnet8 deprecate 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

  1. Lambda runtimes and deprecation schedule — docs.aws.amazon.com
  2. Implement Lambda canary deployments using a weighted alias — docs.aws.amazon.com
  3. Retrieve data about Lambda functions that use a deprecated runtime — docs.aws.amazon.com
  4. aws lambda wait - AWS CLI Command Reference — docs.aws.amazon.com
Emeka Okafor
Written by
Emeka Okafor · Security Editor

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

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading