Node.js — Segfault in a Native Module After a Runtime Upgrade
Investigate Segmentation fault core dumped from native addons after a Node upgrade, verify NODE_MODULE_VERSION mismatches, and rebuild against the current ABI.
Symptoms
- The process dies with Segmentation fault (core dumped) at require time for a native addon such as better-sqlite3 or canvas
- A same-ABI warning appears in logs when it fails gracefully instead of crashing
- The crash follows a Node major version bump while package versions stayed identical
Possible causes
- The prebuilt binary was compiled for a different NODE_MODULE_VERSION ABI than the running runtime
- A cached build directory survived the Node upgrade, so node-gyp never rebuilt for the new headers
- The addon links against a system library version that changed with the base image
- Multiple Node runtimes exist on the host and npm install ran under a different one than the app runs with
Diagnosis steps
Read the ABI mismatch evidence
node -p "process.versions.modules" && cat node_modules/better-sqlite3/package.json | grep -i binaryInterpretation
Comparing the runtime modules version with the addon build target identifies an ABI mismatch even when the crash message itself is only a segfault.
Rebuild from source against current headers
npm rebuild better-sqlite3 --build-from-sourceInterpretation
A successful rebuild followed by a clean require proves the old binary was the culprit.
Confirm a single runtime per environment
which -a node; ls /usr/local/bin/node* /opt/node*/bin/node 2>/dev/nullInterpretation
Multiple runtimes with different ABIs guarantee recurring mismatches between install and execution.
Root cause
The addon .node binary was built for a different Node ABI version than the process loading it. Native addons call into V8 and Node internals directly, so an ABI mismatch corrupts memory on the first call instead of raising a JavaScript error, which is why the symptom is a raw segfault rather than an exception.
Fix
Rebuild the addon for the running runtime with npm rebuild, purge stale build caches after Node upgrades, ensure npm install executes under the exact runtime the app uses, and pin base images so Node major versions do not drift between build and deploy.
Risk note
Pinning the old Node version to match a stale binary accumulates upgrade debt and leaves security patches unapplied. Rebuild instead of downgrading.
Verify the fix
After rebuild, require the addon and run its smoke test in the target image, then verify process.versions.modules matches the prebuilt target or that a from-source build is in place.
Symptom walkthrough
When the mismatch is detectable, Node prints a receipt before any crash:
Error: The module '.../better_sqlite3.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 108. This version of Node.js requires
NODE_MODULE_VERSION 115.
When it is not detected, you get only the bare crash — Segmentation fault (core dumped) — at the require line. Map the two numbers to runtimes (108 is Node 18, 115 is Node 20) to confirm the upgrade path introduced the mismatch. The crash-at-require signature separates ABI problems from crashes inside addon calls, which usually indicate the second class: a system library change.
Fix in practice
Rebuild against the current headers and clear anything cached from the previous ABI:
npm rebuild better-sqlite3 --build-from-source
rm -rf ~/.cache/node-gyp node_modules/.package-lock.json
Guard the environment so the mismatch cannot return:
FROM node:20-bookworm-slim
COPY package*.json ./
RUN npm ci && npm rebuild --build-from-source
Install and run under the same runtime — check with which -a node that no second runtime shadows the intended one. For image-based deploys, pinning the Node major in the base image keeps build and deploy ABIs identical.
Verification
In the target image, require the addon and execute a trivial call (open and close an in-memory database for better-sqlite3). Exit code zero proves the ABI lines up. Then assert process.versions.modules equals the prebuilt target or that the binary timestamp shows a fresh source build. Add the require smoke test to the container entrypoint health check so a future runtime drift fails the deployment before it serves traffic, and do not paper over the crash by downgrading Node — that trades an outage for unpatched security releases.