[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-it-better-sqlite3-worker-thread-nitro":3},{"id":4,"title":5,"author":6,"body":7,"cover":551,"description":552,"draft":553,"extension":554,"locale":555,"meta":556,"navigation":134,"path":557,"publishedAt":558,"seo":559,"seoTitle":560,"slug":561,"stem":562,"tags":563,"__hash__":565},"blog\u002Fit\u002Fblog\u002Fbetter-sqlite3-worker-thread-nitro.md","better-sqlite3 senza bloccare l'event loop: worker e Nitro","Manuel Tardivo",{"type":8,"value":9,"toc":541},"minimark",[10,41,48,53,60,64,67,77,99,102,368,385,389,427,431,442,446,466,470,494,497,501,515,519,537],[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"," è ",[25,26,27],"strong",{},"sincrono"," per design: ogni ",[20,30,31],{},"prepare().run()"," o ",[20,34,35],{},".all()"," occupa il thread JavaScript fino al ritorno della syscall SQLite. Under load, anche query “veloci” accumulano ",[25,38,39],{},"latency HTTP"," perché bloccano l’event loop di Node.",[11,42,43,44,47],{},"Docs ufficiali raccomandano operazioni pesanti in ",[25,45,46],{},"worker thread"," o processo separato. Nitro\u002FNuxt usano Node server preset—stesso problema del classico Express.",[49,50,52],"h2",{"id":51},"perché-è-un-problema-misurabile","Perché è un problema misurabile",[11,54,55,56,59],{},"In Node single-threaded, mentre SQLite lavora, ",[25,57,58],{},"nessun altro"," I\u002FO asincrono dello stesso processo avanza. Un burst di richieste che fanno SELECT contemporaneamente vede p95 salire anche se ogni query dura 2 ms.",[49,61,63],{"id":62},"pattern-pool-di-worker-minimi","Pattern: pool di worker minimi",[11,65,66],{},"Architettura:",[68,69,75],"pre",{"className":70,"code":72,"language":73,"meta":74},[71],"language-text","HTTP handler → serializza job → Worker 1..N (ogni worker possiede una Database instance)\n","text","",[20,76,72],{"__ignoreMap":74},[11,78,79,80,82,83,86,87,90,91,94,95,98],{},"Ogni worker importa ",[20,81,22],{}," e apre ",[25,84,85],{},"un"," file DB (o ",[20,88,89],{},":memory:"," per test). Comunicazione via ",[20,92,93],{},"parentPort.postMessage"," \u002F ",[20,96,97],{},"workerData",".",[11,100,101],{},"Pseudocodice master:",[68,103,107],{"className":104,"code":105,"language":106,"meta":74,"style":74},"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,108,109,129,136,179,194,199,237,269,301,345,356,362],{"__ignoreMap":74},[110,111,114,118,122,125],"span",{"class":112,"line":113},"line",1,[110,115,117],{"class":116},"szBVR","import",[110,119,121],{"class":120},"sVt8B"," { Worker } ",[110,123,124],{"class":116},"from",[110,126,128],{"class":127},"sZZnC"," 'node:worker_threads'\n",[110,130,132],{"class":112,"line":131},2,[110,133,135],{"emptyLinePlaceholder":134},true,"\n",[110,137,139,142,146,149,152,155,159,162,165,168,170,172,174,176],{"class":112,"line":138},3,[110,140,141],{"class":116},"const",[110,143,145],{"class":144},"sj4cs"," workers",[110,147,148],{"class":116}," =",[110,150,151],{"class":120}," [",[110,153,154],{"class":116},"new",[110,156,158],{"class":157},"sScJk"," Worker",[110,160,161],{"class":120},"(",[110,163,164],{"class":127},"'.\u002Fsql-worker.mjs'",[110,166,167],{"class":120},"), ",[110,169,154],{"class":116},[110,171,158],{"class":157},[110,173,161],{"class":120},[110,175,164],{"class":127},[110,177,178],{"class":120},")]\n",[110,180,182,185,188,191],{"class":112,"line":181},4,[110,183,184],{"class":116},"let",[110,186,187],{"class":120}," rr ",[110,189,190],{"class":116},"=",[110,192,193],{"class":144}," 0\n",[110,195,197],{"class":112,"line":196},5,[110,198,135],{"emptyLinePlaceholder":134},[110,200,202,205,208,211,213,217,220,223,226,229,231,234],{"class":112,"line":201},6,[110,203,204],{"class":116},"export",[110,206,207],{"class":116}," function",[110,209,210],{"class":157}," queryAll",[110,212,161],{"class":120},[110,214,216],{"class":215},"s4XuR","sql",[110,218,219],{"class":116},":",[110,221,222],{"class":144}," string",[110,224,225],{"class":120},", ",[110,227,228],{"class":215},"params",[110,230,219],{"class":116},[110,232,233],{"class":144}," unknown",[110,235,236],{"class":120},"[]) {\n",[110,238,240,243,246,248,251,254,257,260,263,266],{"class":112,"line":239},7,[110,241,242],{"class":116},"  const",[110,244,245],{"class":144}," w",[110,247,148],{"class":116},[110,249,250],{"class":120}," workers[rr",[110,252,253],{"class":116},"++",[110,255,256],{"class":116}," %",[110,258,259],{"class":120}," workers.",[110,261,262],{"class":144},"length",[110,264,265],{"class":120},"]",[110,267,268],{"class":116},"!\n",[110,270,272,275,278,281,284,287,289,292,295,298],{"class":112,"line":271},8,[110,273,274],{"class":116},"  return",[110,276,277],{"class":116}," new",[110,279,280],{"class":144}," Promise",[110,282,283],{"class":120},"((",[110,285,286],{"class":215},"resolve",[110,288,225],{"class":120},[110,290,291],{"class":215},"reject",[110,293,294],{"class":120},") ",[110,296,297],{"class":116},"=>",[110,299,300],{"class":120}," {\n",[110,302,304,307,310,312,315,318,321,323,325,328,331,334,337,339,342],{"class":112,"line":303},9,[110,305,306],{"class":120},"    w.",[110,308,309],{"class":157},"once",[110,311,161],{"class":120},[110,313,314],{"class":127},"'message'",[110,316,317],{"class":120},", (",[110,319,320],{"class":215},"m",[110,322,294],{"class":120},[110,324,297],{"class":116},[110,326,327],{"class":120}," (m.err ",[110,329,330],{"class":116},"?",[110,332,333],{"class":157}," reject",[110,335,336],{"class":120},"(m.err) ",[110,338,219],{"class":116},[110,340,341],{"class":157}," resolve",[110,343,344],{"class":120},"(m.rows)))\n",[110,346,348,350,353],{"class":112,"line":347},10,[110,349,306],{"class":120},[110,351,352],{"class":157},"postMessage",[110,354,355],{"class":120},"({ sql, params })\n",[110,357,359],{"class":112,"line":358},11,[110,360,361],{"class":120},"  })\n",[110,363,365],{"class":112,"line":364},12,[110,366,367],{"class":120},"}\n",[11,369,370,373,374,377,378,380,381,384],{},[20,371,372],{},"sql-worker.mjs"," crea ",[20,375,376],{},"new Database(path, { readonly: true })"," dove possibile per letture parallele sicure; per scritture serializza in ",[25,379,85],{}," worker o usa ",[20,382,383],{},"busyTimeout"," e transazioni brevi (SQLite è single-writer).",[49,386,388],{"id":387},"nitro-lifetime-e-path-db","Nitro: lifetime e path DB",[390,391,392,404,418],"ul",{},[393,394,395,396,399,400,403],"li",{},"In ",[25,397,398],{},"serverless puro",", filesystem può essere read-only—SQLite persistente non sempre è sensato; in ",[25,401,402],{},"VPS o container con volume",", funziona.",[393,405,406,409,410,413,414,417],{},[25,407,408],{},"Warm instance",": riusa worker creati all’avvio hook Nitro (",[20,411,412],{},"nitro.hooks.ready"," o modulo singleton) invece di ",[20,415,416],{},"new Worker"," per richiesta.",[393,419,420,421,424,425,98],{},"Path DB: risolvilo ",[25,422,423],{},"assoluto"," una volta, passa via ",[20,426,97],{},[49,428,430],{"id":429},"errori-e-teardown","Errori e teardown",[11,432,433,434,437,438,441],{},"Gestisci ",[20,435,436],{},"worker.on('error')"," e ",[20,439,440],{},"exit"," code ≠ 0 con restart controllato. Un worker morto senza recovery blocca metà throughput.",[49,443,445],{"id":444},"metriche","Metriche",[390,447,448,454,460],{},[393,449,450,453],{},[25,451,452],{},"Queue depth",": richieste in attesa di worker libero.",[393,455,456,459],{},[25,457,458],{},"Round-trip worker"," vs tempo query pura (serializzazione message-passing costa µs–ms).",[393,461,462,465],{},[25,463,464],{},"Checkpoint WAL"," periodico se usi WAL mode su file grande.",[49,467,469],{"id":468},"wal-un-writer-molti-reader","WAL: un writer, molti reader",[11,471,472,473,476,477,479,480,483,484,487,488,493],{},"Con ",[25,474,475],{},"journal_mode=WAL",", SQLite permette letture parallele mentre la scrittura procede in modo serializzato. Nella pratica ",[20,478,22],{},", i maintainer enfatizzano throughput ottimale con ",[25,481,482],{},"thread\u002Fcaller dedicato alla scrittura"," e più connessioni ",[25,485,486],{},"read-only"," per le SELECT—pattern coerente con worker pool dove un solo worker gestisce mutazioni e gli altri servono query di lettura. Vedi discussioni ufficiali su concorrenza nel repo (",[14,489,492],{"href":490,"rel":491},"https:\u002F\u002Fgithub.com\u002FWiseLibs\u002Fbetter-sqlite3#usage-with-worker-threads",[18],"README"," e note threading).",[11,495,496],{},"Senza WAL, locking rollback intralcia anche letture durante commit lunghi.",[49,498,500],{"id":499},"alternative","Alternative",[390,502,503,509],{},[393,504,505,508],{},[25,506,507],{},"libsql"," \u002F Turso per HTTP remoto (modello diverso).",[393,510,511,514],{},[25,512,513],{},"Async driver"," altrove—fuori scope di better-sqlite3.",[49,516,518],{"id":517},"sintesi","Sintesi",[11,520,521,523,524,527,528,533,534,98],{},[20,522,22],{}," è eccellente per DX sincrona; in server HTTP concorrente ",[25,525,526],{},"sposta il synchronous block fuori dall’event loop"," con worker dedicati e misura coda + fail worker. Il ",[14,529,532],{"href":530,"rel":531},"https:\u002F\u002Fgithub.com\u002FWiseLibs\u002Fbetter-sqlite3\u002Fblob\u002Fmaster\u002Fdocs\u002Fthreads.md",[18],"README better-sqlite3"," spiega limiti multithreading nativi—leggerlo prima di fidarti di ",[20,535,536],{},"shared cached",[538,539,540],"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":74,"searchDepth":131,"depth":131,"links":542},[543,544,545,546,547,548,549,550],{"id":51,"depth":131,"text":52},{"id":62,"depth":131,"text":63},{"id":387,"depth":131,"text":388},{"id":429,"depth":131,"text":430},{"id":444,"depth":131,"text":445},{"id":468,"depth":131,"text":469},{"id":499,"depth":131,"text":500},{"id":517,"depth":131,"text":518},"\u002Fblog\u002F08_better_sqlite3_worker_thread_nitro_1x.webp","Pattern con worker thread per query SQLite sincrone in app Node o Nitro, e cosa monitorare quando quel database è già in produzione.",false,"md","it",{},"\u002Fit\u002Fblog\u002Fbetter-sqlite3-worker-thread-nitro","2026-06-04",{"title":5,"description":552},"better-sqlite3: worker thread su Nitro","better-sqlite3-worker-thread-nitro","it\u002Fblog\u002Fbetter-sqlite3-worker-thread-nitro",[564],"mvp","OXgNyfohql11Yy9hD6u1mJbkBt68DfykErPmU3N54iM"]