SuperAPI AI Directory

amplify-lambda-node24-upgrade

Deployment & operations

View repository

Updated 10-15 19:58

About this skill

Amplify Lambda: Node 20 → Node 24 Upgrade

Upgrade an AWS Amplify backend Lambda function from nodejs20.x to nodejs24.x. A correct upgrade is two parts: (1) flip the runtime in the Amplify config, and (2) fix the source code, because the Node 24 Lambda runtime ships a new Runtime Interface Client that removes the callback-style handler API and no longer waits for unresolved promises. Skipping part 2 produces functions that deploy fine but fail or silently drop work at invocation time.

Run this process per function. A repo may contain many functions in different states; do not assume they are uniform.

Workflow

  1. Detect the Amplify generation and locate the runtime setting — see references/amplify-config.md. Confirm the function is actually on Node 20 (or 18/22) before changing anything.

  2. Audit the handler source for breaking changes before flipping the runtime, so you know the blast radius. Run the audit script:

    scripts/audit_node24.sh <path-to-function-or-repo>
    

    It greps for every pattern the new runtime breaks and prints file:line hits. Treat each hit as a required code review, not an auto-fix.

  3. Flip the runtime and shared toolchain — centrally, once. Do these in the orchestrator (not delegated), because they touch shared files and config that parallel fixers would clobber:

    • Flip the runtime to Node 24 in the Amplify config (Gen 1 or Gen 2 — see amplify-config.md). For Gen 2 this is runtime: 24; it is natively supported, so upgrade @aws-amplify/backend rather than dropping to the CDK escape hatch.
    • Upgrading @aws-amplify/backend past ~1.23 swaps in a deployer that loads backend.ts as real ESM — any function dir with its own package.json that lacks "type": "module" will break synth with does not provide an export named '…'. Fix it in the same pass; see "Deployer 2.x ESM module scoping" in amplify-config.md (includes a no-credentials local repro: npx tsx amplify/backend.ts).
    • The CI build machine's Node version is separate from the Lambda runtime — flipping runtime: 24 does not change what Node the Amplify build runs on. Move the build to 24 too (repo amplify.yml nvm install 24 && nvm use 24, or the console live-package override) — see "Build machine Node version" in amplify-config.md.
    • package.json → "engines": { "node": ">=24" } if present; @types/node → ^24 (dev dependency); esbuild/bundler target → node24 (or es2023+); tsconfig target/lib if it pins an older version.
    • Node 24 brings npm 11, which changes enforcement behavior, not just version numbers: e.g. min-release-age (supply-chain package-age gate) is silently ignored by npm 10 and only hard-enforced from npm ≥ 11.10.0 (first bundled in Node 24.14.1). If the repo relies on .npmrc security settings, assert the npm version in CI rather than trusting the config to be honored.
  4. Fix each function's source by delegating to a subagent — don't hand-edit handler code inline. For each function, fill the brief in references/fix-brief.md with that function's directory, its audit hits, and the absolute path to breaking-changes.md, then spawn a subagent (general-purpose) with the filled brief. Each subagent fixes only its own function dir and returns a structured report. The two changes that break the most real Amplify functions, and that the brief makes the subagent hunt for:

    • Callback-style handlers → convert to async.
    • Fire-and-forget promises (logging, metrics, analytics, DynamoDB writes not awaited) → await them, because Lambda no longer drains the event loop. This one isn't greppable — it requires reading the handler, which is why a focused subagent does it well.

    For a repo with many functions and the user opted into orchestration, use the Workflow pipeline variant in fix-brief.md (audit → fix per function, no barrier). Otherwise spawn subagents directly with the Agent tool. Aggregate every report's verify and unresolved items for the user.

  5. Build and test locally, then deploy:

    • Gen 2: npx ampx sandbox (or the pipeline) — watch for type errors from the new @types/node.
    • Gen 1: amplify push.
    • Invoke the function (real event or ampx sandbox / amplify mock function) and confirm any background work (writes, logs, metrics) actually completes — this is where the "no longer waits for promises" change bites and tests must explicitly cover it.
    • When validating via a fresh feature-branch deploy, remember it creates a brand-new stack: it can fail on account-global name collisions (CloudWatch Logs Insights query definitions, named IAM/log resources) that existing branches never re-create. Those failures are orthogonal to the runtime bump — fix or scope the names, don't misattribute them to Node 24.
  6. Report what changed and what the user must verify — fold in every subagent report's verify and unresolved items (especially async side-effects and any crypto.createCipher / url.parse usages that need real refactoring).

Report-only mode (audit any repo, change nothing)

When the goal is just to assess a repo's Node-24 readiness — not to upgrade it — run the audit in report mode and stop:

scripts/audit_node24.sh --report <repo-path>

This makes zero changes and spawns no fixer. It prints a consolidated rollup — Amplify generation(s) detected, function counts, current runtime inventory (how many on 20 vs 18/22 vs already 24), per-category finding counts, and a verdict — followed by the file:line details. Use it to triage many repos, attach to a ticket/PR, or decide which functions are worth upgrading. To actually upgrade, switch to the full workflow above (steps 3–6); do not run the fixer in this mode.

The non-negotiable breaking changes (Node 24 Lambda)

These come from the new runtime, independent of language version. Full detail + fix patterns in references/breaking-changes.md.

Removed / changed Symptom Fix
handler(event, context, callback) callback signature callback is not a function / function hangs to timeout Convert to async (event, context) => { return result }; throw instead of callback(err)
context.callbackWaitsForEmptyEventLoop Property gone; background work dropped Remove the line; await all async side-effects
context.succeed() / context.fail() / context.done() not a function return / throw from an async handler
Lambda no longer waits for unresolved promises Logs/metrics/DB writes silently missing await every promise whose completion matters before returning

Response streaming (awslambda.streamifyResponse(async (event, responseStream, context) => …)) is unchanged.

Notes for running across many repos

  • Never blanket-replace. Changing the runtime string is safe; rewriting handlers is not. Audit first, fix deliberately.
  • A function can be Gen 1 and Gen 2 in the same monorepo — detect per function (amplify-config.md explains the tells).
  • defineFunction({ runtime: 24 }) is natively supported (NodeVersion = 18 | 20 | 22 | 24) on @aws-amplify/backend-function ≥ 1.17.0. If 24 is rejected, upgrade @aws-amplify/backend + @aws-amplify/backend-cli — that's the fix, not the CDK escape hatch (which is reserved for custom CDK functions). Never silently downgrade the target.
  • ESM/CommonJS, OpenSSL, and AL2023 base-image (yum→dnf) concerns only matter for container-image functions or functions that pin module type — covered in breaking-changes.md.