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.

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:
- 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.
- 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 withprebuilds/orbuild/directories carries compiled artifacts for exactly one platform. One native module poisons the whole process at require-time. - Rebuilds must happen where they run.
npm cion 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 exceptionsRebuild 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 deliberatelySame 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 — the hook this ritual lives in
- Next.js SQLite SaaS Boilerplate (Prod WAL Guide) — the driver choice this post defends
- Next.js 'Killed' on 1GB VPS? The 5-Minute Swapfile Fix — the other midnight ghost, same box
+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)
Command from your phone.
Built so autonomous AI agents build what you want without breaking what's already paying you.
- Built-in analytics — cookie-free mode, $0 forever, no PostHog bill
- SQLite ↔ Supabase: one setting, same field names
- Full digital marketplace & feedback boards included
- 3 payment rails: Stripe, LemonSqueezy & Dodo Payments
- Agent fences: lint gate, read-only data access, auto-rollback
- Ship from your phone via SSH & WireGuard/Tailscale in under a minute
$199
Get BoringPushOne-time payment · Lifetime updates · Unlimited projects.
Keep reading


