Skip to content

Instantly share code, notes, and snippets.

@schickling
Created January 21, 2026 10:36
Show Gist options
  • Select an option

  • Save schickling/bb28e720576dcff0bd2dacd1cd6de5cd to your computer and use it in GitHub Desktop.

Select an option

Save schickling/bb28e720576dcff0bd2dacd1cd6de5cd to your computer and use it in GitHub Desktop.
LiveStore + Bun SIGTRAP crash reproduction guide

SIGTRAP Crash Reproduction: LiveStore + Bun + WASM SQLite

This document provides comprehensive context for reproducing and debugging a SIGTRAP crash that occurs when using LiveStore with Bun.

Problem Description

When creating and destroying ~100-180 LiveStore instances sequentially (each backed by SQLite WASM via wa-sqlite), Bun crashes with a SIGTRAP signal (Trace or breakpoint trap).

Key Characteristics

  • Occurrence: Happens around iteration 100-180 when creating/destroying stores in a loop
  • Concurrency: Crashes even with CONCURRENCY=1 (purely sequential processing)
  • Partial Mitigation: Adding Bun.gc(true) after each store shutdown helps but doesn't fully prevent the crash
  • Intermittent: The exact iteration where it crashes varies between runs
  • Likely Cause: WASM memory/resource exhaustion or improper cleanup

Technical Details

Architecture Stack

Application Code
      ↓
@livestore/livestore (createStorePromise / Store)
      ↓
@livestore/adapter-node (makeAdapter)
      ↓
@livestore/sqlite-wasm (sqliteDbFactory, loadSqlite3Wasm)
      ↓
wa-sqlite (SQLite compiled to WASM)
      ↓
Bun's WASM runtime

What Happens Per Store Lifecycle

Store Creation:

  1. Load SQLite WASM module (loadSqlite3Wasm())
  2. Create SQLite database factory (sqliteDbFactory())
  3. Create in-memory SQLite database
  4. Run schema migrations (create tables)
  5. Initialize client session and sync processor
  6. Boot the store (start background fibers)

Store Shutdown:

  1. Close lifetime scope
  2. Finalize prepared statements
  3. Close SQLite connection
  4. Cleanup Effect fibers/resources

Relevant Code Paths

  • Store creation: packages/@livestore/livestore/src/store/create-store.ts
  • Adapter initialization: packages/@livestore/adapter-node/src/client-session/adapter.ts
  • SQLite WASM loading: packages/@livestore/sqlite-wasm/

Reproduction

Setup

  1. Create a test directory in the livestore repo:
mkdir -p tests/sigtrap-repro
cd tests/sigtrap-repro
  1. Create package.json:
{
  "name": "sigtrap-repro",
  "private": true,
  "type": "module",
  "scripts": {
    "repro": "bun run repro.ts",
    "repro:node": "npx tsx repro.ts",
    "repro:smol": "bun --smol run repro.ts"
  },
  "dependencies": {
    "@livestore/livestore": "file:../../packages/@livestore/livestore",
    "@livestore/adapter-node": "file:../../packages/@livestore/adapter-node",
    "@livestore/common": "file:../../packages/@livestore/common",
    "@livestore/utils": "file:../../packages/@livestore/utils"
  }
}
  1. Create schema.ts:
import { Events, makeSchema, State } from '@livestore/livestore'
import { Schema } from '@livestore/utils/effect'

// Simple table for testing
const items = State.SQLite.table({
  name: 'items',
  columns: {
    id: State.SQLite.text({ primaryKey: true }),
    data: State.SQLite.text(),
    createdAt: State.SQLite.integer({ schema: Schema.DateFromNumber }),
  },
})

// Simple events
export const events = {
  itemAdded: Events.synced({
    name: 'itemAdded',
    schema: Schema.Struct({
      id: Schema.String,
      data: Schema.String,
    }),
  }),
}

// Materializers
const materializers = State.SQLite.materializers(events, {
  itemAdded: ({ id, data }) => items.insert({ id, data, createdAt: Date.now() }),
})

export const tables = { items }
const state = State.SQLite.makeState({ tables, materializers })
export const schema = makeSchema({ state, events })
  1. Create repro.ts:
import * as fs from 'node:fs'
import * as os from 'node:os'
import { createStorePromise } from '@livestore/livestore'
import { makeAdapter } from '@livestore/adapter-node'
import { schema, events } from './schema.ts'

const STORES_DIR = `${os.tmpdir()}/livestore-sigtrap-repro-${Date.now()}`
const TOTAL_ITERATIONS = 300
const ITEMS_PER_STORE = 10

// Track memory usage
function logMemory(label: string) {
  if (typeof Bun !== 'undefined') {
    const usage = process.memoryUsage()
    console.log(`[${label}] Memory - RSS: ${Math.round(usage.rss / 1024 / 1024)}MB, Heap: ${Math.round(usage.heapUsed / 1024 / 1024)}MB`)
  }
}

async function createAndDestroyStore(iteration: number): Promise<void> {
  const storeId = `test-store-${iteration}`

  // Create store
  const store = await createStorePromise({
    storeId,
    schema,
    adapter: makeAdapter({
      storage: { type: 'fs', baseDirectory: STORES_DIR },
    }),
    // Reduce log noise
    logLevel: 'Warning',
  })

  // Insert some data to simulate real usage
  for (let i = 0; i < ITEMS_PER_STORE; i++) {
    store.commit(
      events.itemAdded({
        id: `item-${iteration}-${i}`,
        data: `Test data for iteration ${iteration}, item ${i}. `.repeat(10),
      })
    )
  }

  // Small delay to let async operations settle
  await new Promise((resolve) => setTimeout(resolve, 10))

  // Shutdown store
  await store.shutdownPromise()

  // Attempt garbage collection (helps but doesn't fully prevent crash)
  if (typeof Bun !== 'undefined' && typeof Bun.gc === 'function') {
    Bun.gc(true)
  }
}

async function main() {
  console.log('='.repeat(60))
  console.log('LiveStore SIGTRAP Reproduction Test')
  console.log('='.repeat(60))
  console.log(`Runtime: ${typeof Bun !== 'undefined' ? 'Bun ' + Bun.version : 'Node.js ' + process.version}`)
  console.log(`Platform: ${process.platform} ${process.arch}`)
  console.log(`Stores directory: ${STORES_DIR}`)
  console.log(`Total iterations: ${TOTAL_ITERATIONS}`)
  console.log(`Items per store: ${ITEMS_PER_STORE}`)
  console.log('='.repeat(60))
  console.log('')

  // Clean up previous runs
  if (fs.existsSync(STORES_DIR)) {
    fs.rmSync(STORES_DIR, { recursive: true })
  }
  fs.mkdirSync(STORES_DIR, { recursive: true })

  logMemory('Start')

  const startTime = Date.now()

  for (let i = 0; i < TOTAL_ITERATIONS; i++) {
    const iterStart = Date.now()

    try {
      await createAndDestroyStore(i)

      const iterDuration = Date.now() - iterStart
      console.log(`Iteration ${i + 1}/${TOTAL_ITERATIONS} completed in ${iterDuration}ms`)

      // Log memory every 25 iterations
      if ((i + 1) % 25 === 0) {
        logMemory(`Iteration ${i + 1}`)
      }
    } catch (error) {
      console.error(`Error at iteration ${i + 1}:`, error)
      throw error
    }
  }

  const totalDuration = Date.now() - startTime
  console.log('')
  console.log('='.repeat(60))
  console.log(`SUCCESS: Completed all ${TOTAL_ITERATIONS} iterations without crash!`)
  console.log(`Total time: ${Math.round(totalDuration / 1000)}s`)
  logMemory('End')
  console.log('='.repeat(60))

  // Cleanup
  fs.rmSync(STORES_DIR, { recursive: true })
}

main().catch((error) => {
  console.error('Fatal error:', error)
  process.exit(1)
})
  1. Install dependencies:
bun install

Running the Reproduction

Basic run (expected to crash):

bun run repro.ts

With Bun's small heap mode:

bun --smol run repro.ts

Compare with Node.js (should NOT crash):

npx tsx repro.ts

Expected Behavior

  • Bun: Crashes with SIGTRAP around iteration 100-180, OR stores are immediately marked as shutdown (see "Additional Issue" section below)
  • Node.js: Completes all 300 iterations successfully

Note: The repro test files have been set up in tests/sigtrap-repro/ in this repository. You can run them directly:

cd tests/sigtrap-repro
bun install
bun run repro.ts

Crash Output

Typical crash output looks like:

Iteration 127/300 completed in 45ms
Iteration 128/300 completed in 43ms
[1]    12345 trace trap  bun run repro.ts

Or with more detail:

fish: Job 1, 'bun run repro.ts' terminated by signal SIGTRAP (Trace or breakpoint trap)

Variations to Test

1. With/Without Garbage Collection

Modify the GC call in createAndDestroyStore:

// Option A: Aggressive GC (default)
if (typeof Bun !== 'undefined' && typeof Bun.gc === 'function') {
  Bun.gc(true)  // synchronous, full GC
}

// Option B: No GC
// (comment out the above)

// Option C: Async GC
if (typeof Bun !== 'undefined' && typeof Bun.gc === 'function') {
  Bun.gc(false)  // asynchronous GC
}

2. Different Batch Sizes

Modify ITEMS_PER_STORE:

const ITEMS_PER_STORE = 1    // Minimal
const ITEMS_PER_STORE = 10   // Default
const ITEMS_PER_STORE = 100  // Heavy

3. With Delays Between Iterations

Add delay after shutdown:

await store.shutdownPromise()

// Add delay
await new Promise((resolve) => setTimeout(resolve, 100))

if (typeof Bun !== 'undefined' && typeof Bun.gc === 'function') {
  Bun.gc(true)
}

4. Memory-only Storage

Change storage type:

adapter: makeAdapter({
  storage: { type: 'memory' },  // Instead of 'fs'
}),

5. With Bun's --smol Flag

bun --smol run repro.ts

This uses a smaller default heap which might trigger the issue sooner or change behavior.

Investigation Goals

1. Find Minimal Reproduction

Determine if the issue is:

  • LiveStore-specific (event processing, sync, reactive system)
  • wa-sqlite specific (SQLite WASM bindings)
  • Bun WASM runtime issue (general WASM memory management)
  • Bun SQLite issue (native SQLite bindings interference?)

Isolation tests to create:

// Test A: Pure wa-sqlite without LiveStore
import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
import { sqliteDbFactory } from '@livestore/sqlite-wasm/node'

async function testWaSqliteOnly() {
  for (let i = 0; i < 300; i++) {
    const sqlite3 = await loadSqlite3Wasm()
    const factory = await sqliteDbFactory({ sqlite3 })
    const db = await factory({ _tag: 'in-memory' })
    // Do some operations
    db.exec('CREATE TABLE test (id TEXT PRIMARY KEY)')
    db.exec("INSERT INTO test VALUES ('foo')")
    // Close
    db.close()
    console.log(`wa-sqlite iteration ${i + 1}`)
  }
}
// Test B: WASM module loading only
async function testWasmLoading() {
  for (let i = 0; i < 300; i++) {
    const sqlite3 = await loadSqlite3Wasm()
    // Don't create any databases, just load the module
    console.log(`WASM load iteration ${i + 1}`)
  }
}

2. Identify Root Cause

Possible causes to investigate:

  1. WASM Memory Fragmentation

    • WASM linear memory not being properly freed
    • Memory allocator fragmentation over time
  2. File Descriptor Exhaustion

    • SQLite file handles not being closed
    • Check with lsof -p <pid> during execution
  3. Native Resource Leaks

    • Bun-specific native bindings not cleaning up
    • Worker threads or async handles not released
  4. WASM Instance Accumulation

    • Multiple WASM instances being kept alive
    • Module caching preventing cleanup

3. Determine Where to File Bug

Based on findings:

Additional Issue: Store Premature Shutdown (Bun-specific)

During testing, we discovered a related Bun-specific issue where stores are immediately marked as shutdown after creation. This manifests as:

Error: Store has been shut down (while performing "commit").

This happens even when calling store.commit() immediately after await createStorePromise() returns. This suggests a Bun-specific issue with the Effect runtime lifecycle management.

Symptoms:

  • Store is returned from createStorePromise() but isShutdown flag is already true
  • All subsequent operations fail with "Store has been shut down" error
  • Does NOT occur with Node.js runtime

This may be:

  1. A separate bug from the SIGTRAP issue
  2. A symptom of the same underlying WASM/resource management problem
  3. Related to how Bun handles Effect fibers or scopes

What We've Tried

Approach Result
CONCURRENCY=1 (sequential) Still crashes
Bun.gc(true) after each shutdown Delays crash, doesn't prevent
bun --smol flag Crashes sooner (smaller heap)
Node.js runtime No crash (completes all iterations)
Isolated WASM SQLite tests Needs more investigation
Adding delays between operations Store still prematurely shutdown

Environment Information

Collect this information when reporting:

# Bun version
bun --version

# System info
uname -a

# macOS specific
sw_vers

# Check for other SQLite processes
ps aux | grep sqlite

# During run, check file descriptors
lsof -p $(pgrep -f "bun run repro") | wc -l

Known Environment Details

  • Bun Version: Check with bun --version
  • Platform: macOS (Apple Silicon / M1/M2/M3)
  • LiveStore Version: 0.4.0-dev.22 (from workspace)
  • wa-sqlite: Via @livestore/sqlite-wasm

Debugging Tips

Enable Verbose Logging

const store = await createStorePromise({
  // ...
  logLevel: 'Debug',  // or 'Trace' for maximum verbosity
})

Add WASM Memory Tracking

// If accessible, log WASM memory
function logWasmMemory() {
  // This depends on how wa-sqlite exposes its memory
  // May need to modify @livestore/sqlite-wasm
}

Use Bun's Debugger

bun --inspect run repro.ts

Then connect Chrome DevTools to chrome://inspect.

Core Dump Analysis

On macOS:

# Enable core dumps
ulimit -c unlimited

# Run and let it crash
bun run repro.ts

# Analyze with lldb
lldb -c /cores/core.<pid> $(which bun)

Related Issues

Next Steps

  1. Run the repro script and confirm crash behavior
  2. Test isolated wa-sqlite without LiveStore
  3. Test pure WASM module loading
  4. Monitor file descriptors during execution
  5. Capture and analyze core dump
  6. Investigate the premature shutdown issue (may be Effect + Bun incompatibility)
  7. File bug report with minimal reproduction

Repro Test Location

The reproduction test files are located at:

  • Directory: tests/sigtrap-repro/
  • Schema: tests/sigtrap-repro/schema.ts
  • Repro Script: tests/sigtrap-repro/repro.ts
  • Package: tests/sigtrap-repro/package.json

To run:

cd tests/sigtrap-repro
bun install
bun run repro.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment