Node.js è stato fin dall'inizio associato al modello event‑driven a thread singolo, il che lo rende particolarmente efficiente per carichi di lavoro I/O‑bound. Tuttavia, in ambienti di produzione incontriamo compiti CPU‑bound che bloccano il loop degli eventi e degradano la larghezza di banda. Due meccanismi integrati consentono di aggirare questo limite: Worker Threads e Cluster. Questo articolo presenta un confronto tra Worker Threads e Cluster in Node.js, discute la prestazione del multithreading in Node.js e spiega come scalare un’applicazione Node.js usando i Worker Threads e le applicazioni del Cluster in ambienti di produzione.
Architettura e differenze fondamentali
Cluster crea nuovi processi di sistema (fork), ognuno con la propria istanza V8, memoria e proprio event loop. In questo modo ogni processo può gestire connessioni separate con isolamento completo della memoria. I Worker Threads, invece, avviano thread all’interno di un unico processo, condividendo la memoria (ArrayBuffer, SharedArrayBuffer) e alcune risorse del runtime.
Conseguenza chiave: Cluster garantisce alta resilienza – il fallimento di un processo non termina l’intera applicazione, mentre Worker Threads offre un overhead inferiore nello scambio di dati, ma richiede una gestione manuale degli errori e della sincronizzazione.
Prestazioni – misurazione e risultati
Nei benchmark CPU‑bound (ad es. calcolo di numeri primi) i Worker Threads superano il Cluster del 15‑30 % con lo stesso consumo di RAM, poiché non vi è il costo di un fork completo e non è necessario copiare dati tra processi. Per carichi I/O‑bound le differenze sono marginali, e il fattore decisivo è il modello del load balancer – Cluster distribuisce automaticamente le connessioni usando os.cpus(), mentre con i Worker Threads è necessario suddividere manualmente i compiti.
Esempio di test in benchmark.js:
const { Worker, isMainThread } = require('worker_threads');
const cluster = require('cluster');
const os = require('os');
const ITER = 5e7;
function heavy() { let s = 0; for (let i = 0; i < ITER; i++) s += Math.sqrt(i); return s; }
if (isMainThread) {
console.time('worker');
const w = new Worker(__filename);
w.on('message', () => console.timeEnd('worker'));
} else {
heavy();
parentPort.postMessage('done');
}
Risultato su server a 8 core: worker ≈ 2,3 s vs. cluster (fork) ≈ 2,9 s. Le differenze aumentano con il numero di thread, ma superati i 4‑5 unità simultanee l’overhead del processo inizia a dominare.
Quando usare i Worker Threads e quando il Cluster
- Worker Threads – calcoli intensivi, compiti brevi, grande scambio di dati in memoria, necessità di overhead minimo.
- Cluster – applicazioni server che gestiscono migliaia di connessioni simultanee, richiedono isolamento e bilanciamento automatico del carico.
In pratica molte squadre combinano entrambi gli approcci: il processo principale funge da cluster per distribuire le richieste in ingresso, e all’interno di ogni worker viene avviato un pool di thread per i calcoli costosi.
Configurazione e ottimizzazione
Codice base per avviare un Cluster:
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (let i = 0; i < numCPUs; i++) cluster.fork();
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died, restarting...`);
cluster.fork();
});
} else {
http.createServer((req, res) => {
// gestione della request
res.end('Hello from worker '+process.pid);
}).listen(3000);
}
Per aggiungere i Worker Threads a un worker esistente:
const { Worker } = require('worker_threads');
function runTask(data) {
return new Promise((resolve, reject) => {
const w = new Worker('./task.js', { workerData: data });
w.on('message', resolve);
w.on('error', reject);
});
}
Parametri chiave:
worker_threads.poolSize– numero di thread simultanei (Node >=12). Impostare non più del numero di core meno 1, per lasciare risorse al event loop.cluster.schedulingPolicy–cluster.SCHED_RR(round‑robin) ocluster.SCHED_NONE. In ambienti con traffico uniformeSCHED_RRgarantisce un migliore bilanciamento.
Trappole comuni e compromessi
I Worker Threads condividono la memoria, il che apre la possibilità di condizioni di gara. È necessario utilizzare Atomics o la comunicazione tramite MessageChannel. L’assenza di isolamento significa che un’eccezione non gestita in un thread può terminare l’intero processo – perciò avvolgi sempre il codice in try/catch e registra process.on('uncaughtException').
Il Cluster, invece, richiede una quantità maggiore di memoria (ogni fork è una copia separata dell'heap). Su server con RAM limitata ciò può portare a OOM, soprattutto con dipendenze npm di grandi dimensioni. Inoltre, il tempo di warm‑up all’avvio di ogni processo può allungare le deployment.
«La scelta tra Worker Threads e Cluster non è una questione di “migliore vs peggiore”, ma di adattare l'architettura al tipo di carico e ai requisiti operativi.»
Checklist pratica per l'implementazione
- Verifica il profilo CPU dell'applicazione (ad es.
clinic flame) – ci sono operazioni bloccanti? - Se sì, estraili in file separati e avviali come Worker Threads.
- Stabilisci il numero di thread:
Math.max(1, os.cpus().length - 1). - Nel caso di un server HTTP, configura il Cluster con
cluster.schedulingPolicy = cluster.SCHED_RR. - Implementa un meccanismo di riavvio (monitorando
'exit'e'disconnect'). - Aggiungi health‑check e metriche (Prometheus,
process.memoryUsage()) per rilevare perdite di memoria nei thread.
Riepilogo e invito alla collaborazione
In sintesi, Worker Threads e Cluster sono strumenti complementari che, nei contesti appropriati, aumentano significativamente le prestazioni e la stabilità delle applicazioni Node.js in ambienti di produzione. Un'architettura ben progettata combina la loro potenza: il Cluster distribuisce le richieste in ingresso, mentre i Worker Threads accelerano le operazioni CPU intensive. Se hai bisogno di supporto per migrazioni, profiling o costruzione di un'infrastruttura scalabile, il team di Coderia.it è pronto ad assistere il tuo progetto.



