[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-en-better-sqlite3-worker-thread-nitro":3},{"id":4,"title":5,"author":6,"body":7,"cover":550,"description":551,"draft":552,"extension":553,"locale":554,"meta":555,"navigation":138,"path":556,"publishedAt":557,"seo":558,"seoTitle":559,"slug":560,"stem":561,"tags":562,"__hash__":564},"blog\u002Fen\u002Fblog\u002Fbetter-sqlite3-worker-thread-nitro.md","better-sqlite3 without blocking the event loop: workers and Nitro","Manuel Tardivo",{"type":8,"value":9,"toc":540},"minimark",[10,41,48,53,64,68,71,81,103,106,372,388,392,423,427,434,438,458,462,496,499,503,514,518,536],[11,12,13,23,24,28,29,32,33,36,37,40],"p",{},[14,15,19],"a",{"href":16,"rel":17},"https:\u002F\u002Fgithub.com\u002FWiseLibs\u002Fbetter-sqlite3",[18],"nofollow",[20,21,22],"code",{},"better-sqlite3"," is ",[25,26,27],"strong",{},"synchronous"," by design: each ",[20,30,31],{},"prepare().run()"," or ",[20,34,35],{},".all()"," holds the JavaScript thread until SQLite returns. Under load even “fast” queries add ",[25,38,39],{},"HTTP tail latency"," because they block Node’s event loop.",[11,42,43,44,47],{},"Upstream docs suggest pushing heavy work into ",[25,45,46],{},"worker threads"," or separate processes. Nitro\u002FNuxt on a Node server preset faces the same reality as Express.",[49,50,52],"h2",{"id":51},"why-it-shows-up-in-metrics","Why it shows up in metrics",[11,54,55,56,59,60,63],{},"Node is single-threaded for JS: while SQLite runs, ",[25,57,58],{},"other concurrent async work stalls",". A burst of requests issuing ",[20,61,62],{},"SELECT"," may push p95 even if each query is only ~2 ms.",[49,65,67],{"id":66},"pattern-tiny-worker-pool","Pattern: tiny worker pool",[11,69,70],{},"Architecture:",[72,73,79],"pre",{"className":74,"code":76,"language":77,"meta":78},[75],"language-text","HTTP handler → serialize job → Worker 1..N (each owns a Database handle)\n","text","",[20,80,76],{"__ignoreMap":78},[11,82,83,84,86,87,90,91,94,95,98,99,102],{},"Each worker imports ",[20,85,22],{}," and opens ",[25,88,89],{},"one"," DB path (or ",[20,92,93],{},":memory:"," in tests). Communicate with ",[20,96,97],{},"parentPort.postMessage"," \u002F ",[20,100,101],{},"workerData",".",[11,104,105],{},"Master sketch:",[72,107,111],{"className":108,"code":109,"language":110,"meta":78,"style":78},"language-typescript shiki shiki-themes github-light github-dark","import { Worker } from 'node:worker_threads'\n\nconst workers = [new Worker('.\u002Fsql-worker.mjs'), new Worker('.\u002Fsql-worker.mjs')]\nlet rr = 0\n\nexport function queryAll(sql: string, params: unknown[]) {\n  const w = workers[rr++ % workers.length]!\n  return new Promise((resolve, reject) => {\n    w.once('message', (m) => (m.err ? reject(m.err) : resolve(m.rows)))\n    w.postMessage({ sql, params })\n  })\n}\n","typescript",[20,112,113,133,140,183,198,203,241,273,305,349,360,366],{"__ignoreMap":78},[114,115,118,122,126,129],"span",{"class":116,"line":117},"line",1,[114,119,121],{"class":120},"szBVR","import",[114,123,125],{"class":124},"sVt8B"," { Worker } ",[114,127,128],{"class":120},"from",[114,130,132],{"class":131},"sZZnC"," 'node:worker_threads'\n",[114,134,136],{"class":116,"line":135},2,[114,137,139],{"emptyLinePlaceholder":138},true,"\n",[114,141,143,146,150,153,156,159,163,166,169,172,174,176,178,180],{"class":116,"line":142},3,[114,144,145],{"class":120},"const",[114,147,149],{"class":148},"sj4cs"," workers",[114,151,152],{"class":120}," =",[114,154,155],{"class":124}," [",[114,157,158],{"class":120},"new",[114,160,162],{"class":161},"sScJk"," Worker",[114,164,165],{"class":124},"(",[114,167,168],{"class":131},"'.\u002Fsql-worker.mjs'",[114,170,171],{"class":124},"), ",[114,173,158],{"class":120},[114,175,162],{"class":161},[114,177,165],{"class":124},[114,179,168],{"class":131},[114,181,182],{"class":124},")]\n",[114,184,186,189,192,195],{"class":116,"line":185},4,[114,187,188],{"class":120},"let",[114,190,191],{"class":124}," rr ",[114,193,194],{"class":120},"=",[114,196,197],{"class":148}," 0\n",[114,199,201],{"class":116,"line":200},5,[114,202,139],{"emptyLinePlaceholder":138},[114,204,206,209,212,215,217,221,224,227,230,233,235,238],{"class":116,"line":205},6,[114,207,208],{"class":120},"export",[114,210,211],{"class":120}," function",[114,213,214],{"class":161}," queryAll",[114,216,165],{"class":124},[114,218,220],{"class":219},"s4XuR","sql",[114,222,223],{"class":120},":",[114,225,226],{"class":148}," string",[114,228,229],{"class":124},", ",[114,231,232],{"class":219},"params",[114,234,223],{"class":120},[114,236,237],{"class":148}," unknown",[114,239,240],{"class":124},"[]) {\n",[114,242,244,247,250,252,255,258,261,264,267,270],{"class":116,"line":243},7,[114,245,246],{"class":120},"  const",[114,248,249],{"class":148}," w",[114,251,152],{"class":120},[114,253,254],{"class":124}," workers[rr",[114,256,257],{"class":120},"++",[114,259,260],{"class":120}," %",[114,262,263],{"class":124}," workers.",[114,265,266],{"class":148},"length",[114,268,269],{"class":124},"]",[114,271,272],{"class":120},"!\n",[114,274,276,279,282,285,288,291,293,296,299,302],{"class":116,"line":275},8,[114,277,278],{"class":120},"  return",[114,280,281],{"class":120}," new",[114,283,284],{"class":148}," Promise",[114,286,287],{"class":124},"((",[114,289,290],{"class":219},"resolve",[114,292,229],{"class":124},[114,294,295],{"class":219},"reject",[114,297,298],{"class":124},") ",[114,300,301],{"class":120},"=>",[114,303,304],{"class":124}," {\n",[114,306,308,311,314,316,319,322,325,327,329,332,335,338,341,343,346],{"class":116,"line":307},9,[114,309,310],{"class":124},"    w.",[114,312,313],{"class":161},"once",[114,315,165],{"class":124},[114,317,318],{"class":131},"'message'",[114,320,321],{"class":124},", (",[114,323,324],{"class":219},"m",[114,326,298],{"class":124},[114,328,301],{"class":120},[114,330,331],{"class":124}," (m.err ",[114,333,334],{"class":120},"?",[114,336,337],{"class":161}," reject",[114,339,340],{"class":124},"(m.err) ",[114,342,223],{"class":120},[114,344,345],{"class":161}," resolve",[114,347,348],{"class":124},"(m.rows)))\n",[114,350,352,354,357],{"class":116,"line":351},10,[114,353,310],{"class":124},[114,355,356],{"class":161},"postMessage",[114,358,359],{"class":124},"({ sql, params })\n",[114,361,363],{"class":116,"line":362},11,[114,364,365],{"class":124},"  })\n",[114,367,369],{"class":116,"line":368},12,[114,370,371],{"class":124},"}\n",[11,373,374,377,378,381,382,384,385,102],{},[20,375,376],{},"sql-worker.mjs"," should open ",[20,379,380],{},"new Database(path, { readonly: true })"," for read-heavy paths; writes should funnel through ",[25,383,89],{}," writer worker or rely on SQLite’s single-writer semantics with short transactions + ",[20,386,387],{},"busyTimeout",[49,389,391],{"id":390},"nitro-lifetime-db-pathing","Nitro: lifetime + DB pathing",[393,394,395,402,415],"ul",{},[396,397,398,401],"li",{},[25,399,400],{},"Pure serverless"," often has an ephemeral \u002F read-only FS—persistent SQLite may be the wrong tool; VPS\u002Fcontainers with volumes work.",[396,403,404,407,408,411,412,102],{},[25,405,406],{},"Warm instances",": reuse workers created during Nitro startup (",[20,409,410],{},"nitro.hooks.ready"," or a module singleton) instead of per-request ",[20,413,414],{},"new Worker",[396,416,417,418,420,421,102],{},"Resolve the DB path ",[25,419,313],{}," absolutely and pass via ",[20,422,101],{},[49,424,426],{"id":425},"errors-and-teardown","Errors and teardown",[11,428,429,430,433],{},"Handle ",[20,431,432],{},"worker.on('error')"," and non-zero exit codes with controlled restart. Half a dead worker pool means half throughput forever.",[49,435,437],{"id":436},"metrics","Metrics",[393,439,440,446,452],{},[396,441,442,445],{},[25,443,444],{},"Queue depth"," waiting for idle workers.",[396,447,448,451],{},[25,449,450],{},"Worker round-trip"," vs raw SQL time (message passing is not free).",[396,453,454,457],{},[25,455,456],{},"WAL checkpoints"," for large append-heavy files.",[49,459,461],{"id":460},"wal-one-writer-many-readers","WAL: one writer, many readers",[11,463,464,465,468,469,471,472,475,476,479,480,482,483,487,488,495],{},"With ",[25,466,467],{},"journal_mode=WAL",", SQLite lets readers run while writes are serialized. ",[20,470,22],{}," guidance from upstream issues\u002FREADME boils down to ",[25,473,474],{},"one dedicated writer"," plus ",[25,477,478],{},"read-only"," handles (often one per worker) for heavy ",[20,481,62],{}," traffic—exactly what you model when only one worker owns mutations. Read the ",[14,484,486],{"href":16,"rel":485},[18],"README"," and ",[14,489,492],{"href":490,"rel":491},"https:\u002F\u002Fgithub.com\u002FWiseLibs\u002Fbetter-sqlite3\u002Fblob\u002Fmaster\u002Fdocs\u002Fthreads.md",[18],[20,493,494],{},"docs\u002Fthreads.md"," before sharing handles across workers.",[11,497,498],{},"Without WAL, rollback journals block readers during long commits.",[49,500,502],{"id":501},"alternatives","Alternatives",[393,504,505,511],{},[396,506,507,510],{},[25,508,509],{},"libsql"," \u002F Turso over HTTP—different operational model.",[396,512,513],{},"Other async-native drivers—out of scope for better-sqlite3 fans.",[49,515,517],{"id":516},"summary","Summary",[11,519,520,522,523,526,527,531,532,535],{},[20,521,22],{}," offers great synchronous DX; for concurrent HTTP servers ",[25,524,525],{},"move the blocking syscall off the event loop"," with dedicated workers and monitor queue depth + worker health. Read the upstream ",[14,528,530],{"href":490,"rel":529},[18],"threading notes"," before expecting magic from ",[20,533,534],{},"shared"," caches.",[537,538,539],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":78,"searchDepth":135,"depth":135,"links":541},[542,543,544,545,546,547,548,549],{"id":51,"depth":135,"text":52},{"id":66,"depth":135,"text":67},{"id":390,"depth":135,"text":391},{"id":425,"depth":135,"text":426},{"id":436,"depth":135,"text":437},{"id":460,"depth":135,"text":461},{"id":501,"depth":135,"text":502},{"id":516,"depth":135,"text":517},"\u002Fblog\u002F08_better_sqlite3_worker_thread_nitro_1x.webp","Worker-thread patterns for synchronous SQLite queries in Node or Nitro apps, and what to monitor once that database is in production.",false,"md","en",{},"\u002Fen\u002Fblog\u002Fbetter-sqlite3-worker-thread-nitro","2026-06-04",{"title":5,"description":551},"better-sqlite3: worker threads on Nitro","better-sqlite3-worker-thread-nitro","en\u002Fblog\u002Fbetter-sqlite3-worker-thread-nitro",[563],"mvp","QpXyRfa-OisWv-taHNV017TCevtP5G6AhAHEzoTgieo"]