Lambda Versioned Layers: Native Node Addons Without ABI Surprises

Lambda Versioned Layers: Native Node Addons Without ABI Surprises

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-vX separately; 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

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply