Native .node addons compiled on Node 18 will SIGSEGV on Node 20. Lambda layers that “just work” across runtimes are a myth for N-API/ABI-sensitive modules. Version layers per runtime, pin digests in CDK, and fail deploy when the addon ABI does not match process.versions.modules.
⚡ TL;DR: Build native layers inside the exact Lambda runtime image; publish
addon-node20-vX/addon-node22-vXseparately; assert ABI at cold start; block CDK synth if layer runtime ≠ function runtime. Pair with Lambda Cold Starts on Node 20 and Lambda Warm Pools.
Build inside the runtime image
# docker/addon-layer/Dockerfile
ARG RUNTIME=public.ecr.aws/lambda/nodejs:20
FROM ${RUNTIME} AS build
WORKDIR /opt
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# ✅ rebuild native code for this image's Node ABI
RUN npm rebuild better-sqlite3 --build-from-source
RUN mkdir -p /layer/nodejs && cp -R node_modules /layer/nodejs/
FROM scratch
COPY --from=build /layer /
# publish distinct layers
docker build --build-arg RUNTIME=public.ecr.aws/lambda/nodejs:20 -t addon-node20 .
# pack /opt → zip → aws lambda publish-layer-version \
# --compatible-runtimes nodejs20.x \
# --description "better-sqlite3 abi=$(node -p process.versions.modules)"
Fail fast on ABI skew
// lib/assert-abi.ts
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
export function assertAddonAbi(expectedModules: string) {
if (process.versions.modules !== expectedModules) {
throw new Error(
`abi_skew: runtime=${process.versions.modules} layer_built_for=${expectedModules}`,
);
}
// touch the native binding early on cold start
require("better-sqlite3");
}
// handler init
assertAddonAbi(process.env.ADDON_ABI!);
❌ Shipping one “universal” layer for nodejs18.x and nodejs20.x. ✅ Encoding compatible-runtimes + description with ABI number.
CDK pins runtime ↔ layer
const layer = lambda.LayerVersion.fromLayerVersionArn(this, "Addon",
ssm.StringParameter.valueForStringParameter(this, "/lambda/layers/addon-node20"));
new lambda.Function(this, "Fn", {
runtime: lambda.Runtime.NODEJS_20_X, // must match layer
layers: [layer],
environment: { ADDON_ABI: "115" }, // node20 modules version
});
Add a synth-time check: parse layer compatible runtimes from SSM metadata and assert equality. Same hygiene as Cursor Rules for TypeScript Monorepos — encode invariants so agents cannot invent wildcards.
Closing checklist
✅ Dos
– ✅ Build addons in official Lambda Node images
– ✅ One layer stream per runtime major
– ✅ Assert process.versions.modules on init
– ✅ Pin layer ARNs via SSM / version numbers
– ✅ Smoke-test native require in CI (custom runtime container)
❌ Don’ts
– ❌ Don’t reuse Node 18 layers on Node 20
– ❌ Don’t compile on macOS/arm64 laptop for Amazon Linux x86
– ❌ Don’t ignore GLIBC mismatches (AL2 vs AL2023)
– ❌ Don’t put huge addon layers on latency-critical cold paths without measuring
Related reading
- Lambda Cold Starts on Node 20
- Lambda Warm Pools
- Cursor Rules for TypeScript Monorepos
- LLM Coding Agents on AWS
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
