Worker
Runs a JavaScript file on a separate thread with its own event loop and V8 isolate.
new Worker(filename[, options])- Added in Node.js v10.5.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$filename | <string> | <URL> | Path to the Worker's main script or module. Must be an absolute path, or a relative path starting with ./ or ../, or a WHATWG URL object using the file: or data: protocol. If options.eval is true, this is a string of JavaScript code instead of a path. |
$optionsoptional | <Object> | Optional settings for the new thread. |
$options.workerDataoptional | <any> | Any JavaScript value that is cloned with the structured clone algorithm and made available as workerData inside the thread. An error is thrown if the value cannot be cloned. |
$options.evaloptional | <boolean> | If true and the first argument is a string, that string is interpreted as a script executed once the worker is online. |
$options.argvoptional | <any[]> | List of arguments stringified and appended to process.argv inside the worker, as if they had been passed on the command line. |
$options.envoptional | <Object>= process.env | Initial value of process.env inside the thread. The special value worker.SHARE_ENV makes parent and child share one environment object. |
$options.execArgvoptional | <string[]>= inherited from the parent thread | Node CLI options passed to the worker, exposed as process.execArgv inside it. V8 options such as --max-old-space-size and process-wide options such as --title are not supported. |
$options.stdinoptional | <boolean> | If true, worker.stdin is a writable stream whose contents appear as process.stdin inside the worker. By default no data is provided. |
$options.stdoutoptional | <boolean> | If true, worker.stdout is not automatically piped through to process.stdout in the parent. |
$options.stderroptional | <boolean> | If true, worker.stderr is not automatically piped through to process.stderr in the parent. |
$options.trackUnmanagedFdsoptional | <boolean>= true | If true, the Worker tracks raw file descriptors opened with fs.open() and closes them when it exits. Inherited by all nested Workers. |
$options.transferListoptional | <Object[]> | Required when one or more MessagePort-like objects are passed in workerData, otherwise ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST is thrown. |
$options.resourceLimitsoptional | <Object> | Resource limits for the new JS engine instance: maxOldGenerationSizeMb, maxYoungGenerationSizeMb, codeRangeSizeMb and stackSizeMb (default 4). Reaching a limit terminates the Worker. The limits affect only the JS engine, not external data such as ArrayBuffers. |
$options.nameoptional | <string>= 'WorkerThread' | Name used in the thread name and worker title for debugging, producing a title of the form [worker ${id}] ${name}. Truncated to the operating system's maximum thread-name length. |
Return value
<Worker>A Worker instance, an EventEmitter that emits 'online', 'message', 'messageerror', 'error' and 'exit'.
Errors
Throws ERR_WORKER_PATH if filename is neither absolute nor a relative path starting with ./ or ../, and a DataCloneError if workerData contains a value the structured clone algorithm cannot copy, such as a function.
Changelog
- PHP v19.8.0, v18.16.0Added the name option, which adds a name to the worker title for debugging.
- PHP v14.9.0trackUnmanagedFds now defaults to true, so file descriptors opened with fs.open() are closed when the thread exits.
- PHP v14.9.0filename may be a WHATWG URL object using the data: protocol.
- PHP v13.13.0, v12.17.0The transferList option was introduced.
- PHP v13.12.0, v12.17.0filename may be a WHATWG URL object using the file: protocol.
- PHP v13.4.0, v12.16.0The argv option was introduced.
- PHP v13.2.0, v12.16.0The resourceLimits option was introduced.
A Worker runs a separate script on its own OS thread, with its own V8 isolate, event loop and module cache. Nothing is shared except values you explicitly transfer, plus SharedArrayBuffer memory, so ordinary objects can never be raced on — but there are also no shared globals, no shared require cache and no shared open handles.
When to reach for it
Only for CPU-bound work that would otherwise block the event loop: hashing, compression, image resizing, parsing a very large document, template compilation. I/O is already asynchronous, so moving a database query onto a thread makes it slower rather than faster.
What trips people up
- Threads are not cheap. Each costs several megabytes and tens of milliseconds of startup. Create a small pool once at boot and feed it messages; do not spawn one per request.
- The path is resolved against the process working directory, not the calling module. A bare
'worker.js'throwsERR_WORKER_PATH; write'./worker.js', or better, build an absolute path from__dirname. - An exception inside the thread does not crash the process — it surfaces on the
'error'event. Without a listener the failure is invisible. - The process stays alive while any worker is running. Call
worker.terminate(), which returns a promise, or let the thread's script finish.
Example
const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');
if (isMainThread) {
// Re-run this same file on a second thread.
const worker = new Worker(__filename, { workerData: { n: 32 } });
worker.on('message', (result) => console.log('fib(32) =', result));
worker.on('error', (err) => console.error('worker failed:', err.message));
worker.on('exit', (code) => console.log('exit code:', code));
} else {
const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2));
parentPort.postMessage(fib(workerData.n));
}
// Prints:
// fib(32) = 2178309
// exit code: 0When to use it
- Hash or encrypt a batch of files without stalling an HTTP server's event loop
- Resize or transcode uploaded images in a fixed pool of background threads
- Parse a multi-hundred-megabyte CSV or JSON file off the main thread
- Run untrusted or long-running computation that you can terminate on a timeout
Frequently asked questions
When should I use a worker thread instead of a child process?
Use a thread when the work is CPU-bound JavaScript and you want to share memory cheaply through ArrayBuffer transfers or SharedArrayBuffer; use a child process when you need a separate crash domain, a different runtime or binary, or full process isolation. Threads start faster and use far less memory, but an out-of-memory condition in one thread can still abort the whole process.
Why do I get ERR_WORKER_PATH when the file clearly exists?
Because the filename must be absolute or start with ./ or ../ — a bare 'worker.js' is rejected on purpose so it is never confused with a module specifier. Use path.join(__dirname, 'worker.js') so the path does not depend on the process working directory either.
Why does my program hang after the worker finishes its job?
A live Worker keeps the event loop alive, so the process will not exit until the thread's script returns or you call worker.terminate(). For long-lived pool threads, call worker.unref() so they do not by themselves hold the process open.
More examples
A fixed pool that answers requests
Four threads are started once and reused round-robin. Each call returns a promise that settles on the next message from the chosen thread. Note that listeners are removed after every job — attaching a fresh 'message' handler per job without removing it is the most common leak in worker pools, and it eventually trips the MaxListenersExceededWarning.
// pool.js
import { Worker } from 'node:worker_threads';
import path from 'node:path';
function createPool(size) {
const workers = [];
for (let i = 0; i < size; i++) {
const w = new Worker(path.join(__dirname, 'hash-worker.js'), { name: `hash-${i}` });
w.unref(); // do not hold the process open
workers.push(w);
}
let next = 0;
return {
run(job) {
const w = workers[next++ % workers.length];
return new Promise((resolve, reject) => {
const onMessage = (msg) => { cleanup(); resolve(msg); };
const onError = (err) => { cleanup(); reject(err); };
const cleanup = () => {
w.off('message', onMessage);
w.off('error', onError);
};
w.on('message', onMessage);
w.on('error', onError);
w.postMessage(job);
});
},
close: () => Promise.all(workers.map((w) => w.terminate())),
};
}
const pool = createPool(4);
const results = await Promise.all([
pool.run('alpha'),
pool.run('beta'),
pool.run('gamma'),
]);
console.log(results.length, 'digests');
await pool.close();
console.log('pool closed');
// Prints:
// 3 digests
// pool closedRunning a string of code with eval
With eval: true the first argument is source text rather than a path, which is handy for short generated kernels and for tests that should not touch the filesystem. resourceLimits caps the thread's old-space heap at 64 MB; exceeding it terminates that thread only, and the parent sees an ERR_WORKER_OUT_OF_MEMORY on the 'error' event rather than a process-wide crash.
const { Worker } = require('node:worker_threads');
const source = `
const { parentPort, workerData } = require('node:worker_threads');
const total = workerData.values.reduce((a, b) => a + b, 0);
parentPort.postMessage(total);
`;
const worker = new Worker(source, {
eval: true,
workerData: { values: [1, 2, 3, 4, 5] },
resourceLimits: { maxOldGenerationSizeMb: 64 },
});
worker.on('message', (sum) => console.log('sum:', sum));
worker.on('exit', (code) => console.log('exit code:', code));
// Prints:
// sum: 15
// exit code: 0Terminating a thread that runs too long
A tight loop cannot be interrupted by a message, because the thread never returns to its event loop to read one. terminate() stops it at the V8 level and, since v12.5.0, returns a promise that resolves with the exit code. This is the only reliable escape hatch for untrusted or runaway CPU work; there is no cooperative cancellation.
import { Worker } from 'node:worker_threads';
const worker = new Worker('while (true) {}', { eval: true });
const timer = setTimeout(async () => {
const code = await worker.terminate();
console.log('terminated, exit code:', code);
}, 100);
worker.on('exit', () => clearTimeout(timer));
// Prints:
// terminated, exit code: 1
Discussion