# 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.

Source: https://boringpush.com/blog/fix-better-sqlite3-segfault-node22

## 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.**

```bash
# 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.**

```bash
# 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.**

```bash
# .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

- [git push production deploy: Skip the 9-Minute CI Queue](/blog/git-push-production-main-deploy) — the hook this ritual lives in
- [Next.js SQLite SaaS Boilerplate (Prod WAL Guide)](/blog/nextjs-sqlite-saas-boilerplate) — the driver choice this post defends
- [Next.js 'Killed' on 1GB VPS? The 5-Minute Swapfile Fix](/blog/nextjs-oom-build-fix-1gb-vps) — the other midnight ghost, same box
