database · tutorial · sqlite

better-sqlite3 Segfault at Midnight: Fix It

Node segfaults on Ubuntu with no stack trace: ARM-built modules on Linux. Rebuild native, pin the combo, sleep again. My midnight story inside.

The painting The Nightmare: a sleeping woman draped across a bed with a goblin crouched on her chest and a horse's head in the curtains, by Henry Fuseli, 1781
The painting The Nightmare: a sleeping woman draped across a bed with a goblin crouched on her chest and a horse's head in the curtains, by Henry Fuseli, 1781 · Photo: Henry Fuseli, public domain, via Wikimedia Commons

A deploy dies without saying a word

Midnight, launch eve. Push succeeds, hook builds clean, PM2 starts — and the process dies instantly. No error. No stack trace. One word in the logs: Segmentation fault. Restart, dies. Rebuild, dies. The app ran perfectly on Mac twelve minutes earlier; on Ubuntu it committed suicide on sight.

Ninety minutes went to suspecting everything: corrupted database (nope), bad deploy (nope), haunted server (debatable). Then dmesg plus a hunch about architecture led to the truth — better-sqlite3 contained macOS ARM machine code running on Linux x64. The laptop had packed its own biology into the luggage, and the server's immune system shot it on arrival.

Better-sqlite3 segfault fix took five minutes. Finding it took ninety. This post compresses ninety into five for you.

Segfault suspects ranked by actual guilt

Suspect My midnight minutes Actual guilt
Corrupt database file 20 (integrity checks) Innocent
Bad release (revert?) 15 (rolled back, still died) Innocent
Node version mismatch 10 (reinstalled, still died) Accomplice
Haunted server (reboot) 5 (felt productive) Innocent
ARM-built native module 40 (found it, fixed in 5) Guilty

Three things midnight teaches:

  1. Segfaults mean machine-code mismatch, almost always. JavaScript doesn't segfault — native addons do, when their compiled bindings target the wrong OS or architecture. "Segmentation fault" with zero JS stack is the signature: skip the app debugging entirely and interrogate the build provenance first.
  2. node_modules is not portable, ever. The deploy had synced the laptop's node_modules (faster pushes — famous last words). Pure-JS packages travel fine; anything with prebuilds/ or build/ directories carries compiled artifacts for exactly one platform. One native module poisons the whole process at require-time.
  3. Rebuilds must happen where they run. npm ci on the server compiles (or downloads) Linux-x64 bindings natively — correct by construction. My shortcut inverted the rule: build once on Mac, run everywhere. Everywhere rejected it at midnight, on launch eve, as is tradition.

The git-push post preaches box-native builds. This is the ghost story for skeptics.

Better-sqlite3 segfault fix ritual

Five minutes, in order, from the deploy hook that now prevents recurrence.

Step 1 — Confirm the corpse's origin.

# on the VPS (after the crash):
file node_modules/better-sqlite3/build/Release/better_sqlite3.node
# guilty output:  Mach-O 64-bit bundle arm64   (macOS, wrong planet)
# innocent output: ELF 64-bit LSB shared object, x86-64  (Linux, correct)

file reads the binary's true identity in one line — Mach-O means macOS snuck aboard, ELF x86-64 means native and fine.

Step 2 — Rebuild native, gitignore forever.

# server-side resurrection:
npm rebuild better-sqlite3 --build-from-source
pm2 reload boringpush
# permanent prevention (.gitignore, non-negotiable):
node_modules/
*.node
# deploy hook builds with npm ci ON THE BOX, every time, no exceptions

Rebuild compiles (or fetches) correct-platform bindings in place; reload revives the app in seconds. The gitignore lines make recurrence structurally impossible — binaries can never board the push again. My hook now treats shipped node_modules as a deploy failure, full stop.

Step 3 — Pin the combo that works.

# .nvmrc + package.json engines (identical local and server):
22.11.0
"engines": { "node": "22.x" },
"better-sqlite3": "11.10.0"   # verify current; pin exact, upgrade deliberately

Same Node major both sides (ABI compatibility), exact module version (prebuild availability varies by release), upgrades deliberate and staged (never the night before launch). Pinned combos turn "works on my machine" from aspiration into checksum: identical inputs, identical binaries, identical behavior. Boring versions, exciting sleep.

That's the whole promise — binaries native everywhere you land.

The segfault drill card

Check Command Healthy looks like
Binary identity file .../better_sqlite3.node ELF x86-64, never Mach-O
No shipped modules git ls-files | grep node_modules Empty, forever
Native modules built for the server Check your deploy setup Installed on the server, no copy step
Version parity node -v both sides Same major, pinned minor
Midnight readiness This post bookmarked (You're reading it — covered)

Admitting the tax: box-native builds add ~1–3 minutes per deploy (compiling on a small VPS) versus instant shipped-modules pushes. Priced against a ninety-minute midnight: roughly two years of slower deploys to break even on one ghost hunt, and that's before counting launch-eve terror. Slow pushes, fast sleep — the correct trade every single time.

FAQ

Does better-sqlite3 segfault fix apply to other native modules?

Yes — identical ritual for sharp, bcrypt, canvas, sqlite3, anything with prebuilds: file the binary, rebuild on the box, gitignore modules, pin versions. Sharp (image pipelines) is the second-most common midnight segfault after SQLite drivers. The class is "shipped binaries," not any single package — learn the ritual once, apply to every native dependency forever.

Should I switch to sql.js (WASM) to dodge native modules entirely?

Tempting, costly: WASM SQLite runs slower (no native speed), persists awkwardly (memory-first, sync-out complexity), and trades one sharp edge (builds) for chronic ones (durability semantics). Native better-sqlite3 with box builds is faster and simpler once the ritual holds. Dodge the class with process, not the package with compromises — WASM is the answer to a different question (browsers, edge).

How does Docker change the segfault equation?

Containers formalize box-native builds (multi-stage Dockerfile compiles in Linux image, runtime copies artifacts) — segfault-proof by construction when done right. Done wrong (COPY node_modules from Mac stage), Docker reproduces the midnight faithfully inside fancier packaging. The compose post details the clean shape; the law stays identical: binaries are born where they run, container or not.

Can CI (GitHub Actions) prevent shipping bad binaries?

Yes, as backstop: CI on ubuntu-latest runs npm ci + smoke test, catching ARM-poisoned lockfiles before prod (since CI rebuilds Linux-native). But CI is redundant if the hook builds on the box (nothing ships to build wrongly) — belt plus suspenders for teams, either alone suffices solo. The hook is primary; CI is the witness. Never the reverse.

What Node + better-sqlite3 combo is safe right now?

Node 22.x with the latest 11.x/12.x driver (verify current at upgrade time — prebuild matrices shift per release). The .nvmrc plus engines field plus exact pin is the answer, not any specific numbers printed today (they'll age; the pinning discipline won't). Check prebuild availability for your exact combo before upgrading either side — five minutes of verification beats ninety of archaeology.

Why not just use Postgres and skip native SQLite drivers?

Because that trades a solved five-minute ritual for permanent infrastructure (poolers, certs, bills, latency) — the tri-DB posts price that trade honestly. Postgres has no native-module risk and plenty of operational surface; SQLite has one sharp edge with a known ritual. Pick your sharp: scheduled ritual beats chronic overhead for solo scale, every time.

Keep reading

+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)

The Agent-First Shipping Kit

Command from your phone.

Built so autonomous AI agents build what you want without breaking what's already paying you.

$199

Get BoringPush

One-time payment · Lifetime updates · Unlimited projects.

Keep reading