Skip to main content
Worker threads are stable and recommended for CPU-intensive JavaScript operations. For I/O-bound work, use Node.js’s built-in async operations instead.

Overview

The node:worker_threads module enables parallel execution of JavaScript. Workers are useful for performing CPU-intensive tasks without blocking the main event loop.
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
// or
const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');

Key Differences from Other Concurrency Models

  • vs child_process: Workers can share memory via SharedArrayBuffer and transfer ArrayBuffer instances
  • vs cluster: Workers share memory, cluster processes do not
  • Use case: CPU-intensive JavaScript operations, not I/O

Worker Class

The Worker class represents an independent JavaScript execution thread.

Constructor

new Worker(filename[, options])

filename
string | URL
required
Path to the Worker’s main script. Must be absolute or relative (starting with ./ or ../), or a WHATWG URL using file: or data: protocol
options
object
Configuration options
argv
array
List of arguments stringified and appended to process.argv in the worker
env
object | SHARE_ENV
Environment variables. Use SHARE_ENV to share with parent. Default: process.env
eval
boolean
default:"false"
If true, interpret the first argument as a script to execute
execArgv
array
Node.js CLI options for the worker. Default: inherited from parent
stdin
boolean
default:"false"
If true, worker.stdin provides a writable stream
stdout
boolean
default:"false"
If true, worker.stdout is not piped to parent
stderr
boolean
default:"false"
If true, worker.stderr is not piped to parent
workerData
any
Any JavaScript value cloned and available as workerData in the worker
trackUnmanagedFds
boolean
default:"true"
Track raw file descriptors and close them when worker exits
transferList
array
If MessagePort-like objects are in workerData, list them here
resourceLimits
object
JS engine resource limits
maxOldGenerationSizeMb
number
Maximum size of main heap in MB
maxYoungGenerationSizeMb
number
Maximum size of recently created objects heap in MB
codeRangeSizeMb
number
Pre-allocated memory range for generated code
stackSizeMb
number
default:"4"
Default maximum stack size for the thread
name
string
default:"WorkerThread"
Name for the worker (for debugging). Maximum sizes: Windows 32,767, macOS 64, Linux 16 characters
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';

if (!isMainThread) {
  const { parse } = await import('some-js-parsing-library');
  const script = workerData;
  parentPort.postMessage(parse(script));
}

export default function parseJSAsync(script) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(new URL(import.meta.url), {
      workerData: script,
    });
    worker.on('message', resolve);
    worker.once('error', reject);
    worker.once('exit', (code) => {
      if (code !== 0)
        reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}

Events

Event: ‘error’

err
Error
Emitted if the worker thread throws an uncaught exception. The worker is terminated when this happens.

Event: ‘exit’

exitCode
integer
Emitted once the worker has stopped. If exited via process.exit(), this is the exit code. If terminated, the code is 1.

Event: ‘message’

value
any
Emitted when the worker thread calls parentPort.postMessage().

Event: ‘messageerror’

error
Error
Emitted when deserializing a message failed.

Event: ‘online’

Emitted when the worker thread has started executing JavaScript code.

Instance Methods

postMessage(value[, transferList])

Send a message to the worker thread.
value
any
required
The value to send
transferList
array
List of ArrayBuffer, MessagePort, and FileHandle objects to transfer
const worker = new Worker('./worker.js');
worker.postMessage({ hello: 'world' });

// Transfer an ArrayBuffer
const buffer = new ArrayBuffer(10);
worker.postMessage({ buffer }, [buffer]);
// buffer is now unusable in the parent

terminate()

Stops all JavaScript execution in the worker thread.
return
Promise<number>
Promise that resolves with the exit code
const worker = new Worker('./worker.js');
setTimeout(() => {
  worker.terminate();
}, 5000);

ref()

Opposite of unref(). Keeps the event loop active while the worker is running.

unref()

Allows the thread to exit if this is the only active handle.

getHeapSnapshot()

Returns a readable stream for a V8 heap snapshot of the worker.
return
Promise<stream.Readable>
A readable stream containing the snapshot

Properties

threadId

threadId
integer
Unique identifier for this worker thread

threadName

threadName
string | null
The name of the worker thread if running

resourceLimits

resourceLimits
object
Resource limits for this worker

stdin, stdout, stderr

Writable/readable streams if the corresponding options were set.

Module Functions

isMainThread

isMainThread
boolean
true if code is not running inside a Worker thread
import { Worker, isMainThread } from 'node:worker_threads';

if (isMainThread) {
  // This re-loads the current file inside a Worker instance
  new Worker(new URL(import.meta.url));
} else {
  console.log('Inside Worker!');
  console.log(isMainThread);  // Prints 'false'
}

isInternalThread

isInternalThread
boolean
true if running inside an internal Worker thread (e.g., loader thread)

parentPort

parentPort
MessagePort | null
MessagePort for communication with the parent thread. null in the main thread
import { Worker, isMainThread, parentPort } from 'node:worker_threads';

if (isMainThread) {
  const worker = new Worker(new URL(import.meta.url));
  worker.once('message', (message) => {
    console.log(message);  // Prints 'Hello, world!'
  });
  worker.postMessage('Hello, world!');
} else {
  parentPort.once('message', (message) => {
    parentPort.postMessage(message);
  });
}

threadId

threadId
integer
Integer identifier for the current thread

threadName

threadName
string | null
String identifier for the current thread, or null

workerData

workerData
any
Clone of the data passed to the Worker constructor
import { Worker, isMainThread, workerData } from 'node:worker_threads';

if (isMainThread) {
  const worker = new Worker(new URL(import.meta.url), { 
    workerData: 'Hello, world!' 
  });
} else {
  console.log(workerData);  // Prints 'Hello, world!'
}

resourceLimits

resourceLimits
object
JS engine resource constraints for this Worker. Empty object in main thread

SHARE_ENV

SHARE_ENV
symbol
Special value for the env option to share environment variables
import { Worker, SHARE_ENV } from 'node:worker_threads';

new Worker('process.env.SET_IN_WORKER = "foo"', { 
  eval: true, 
  env: SHARE_ENV 
})
  .once('exit', () => {
    console.log(process.env.SET_IN_WORKER);  // Prints 'foo'
  });

Environment Data

setEnvironmentData(key[, value])

Sets data available to all new Worker instances.
key
any
required
Any cloneable value usable as a Map key
value
any
Any cloneable value. undefined deletes the key

getEnvironmentData(key)

Retrieves environment data set by the parent.
key
any
required
The key to retrieve
return
any
The cloned data value
import { Worker, isMainThread, setEnvironmentData, getEnvironmentData } 
  from 'node:worker_threads';

if (isMainThread) {
  setEnvironmentData('Hello', 'World!');
  const worker = new Worker(new URL(import.meta.url));
} else {
  console.log(getEnvironmentData('Hello'));  // Prints 'World!'
}

Transfer/Clone Functions

markAsUntransferable(object)

Marks an object as not transferable.
object
any
required
Any arbitrary JavaScript value
import { MessageChannel, markAsUntransferable } from 'node:worker_threads';

const pooledBuffer = new ArrayBuffer(8);
markAsUntransferable(pooledBuffer);

const { port1 } = new MessageChannel();
try {
  port1.postMessage(new Uint8Array(pooledBuffer), [pooledBuffer]);
} catch (error) {
  // error.name === 'DataCloneError'
}

isMarkedAsUntransferable(object)

Checks if an object is marked as untransferable.
object
any
required
Any JavaScript value
return
boolean
true if marked as untransferable

markAsUncloneable(object)

Marks an object as not cloneable.
object
any
required
Any arbitrary JavaScript value
import { markAsUncloneable, MessageChannel } from 'node:worker_threads';

const anyObject = { foo: 'bar' };
markAsUncloneable(anyObject);

const { port1 } = new MessageChannel();
try {
  port1.postMessage(anyObject);
} catch (error) {
  // error.name === 'DataCloneError'
}

Thread Communication

postMessageToThread(threadId, value[, transferList][, timeout])

Active Development - This API is under active development
Sends a value to another worker by thread ID.
threadId
number
required
The target thread ID
value
any
required
The value to send
transferList
array
Objects to transfer
timeout
number
Time to wait in milliseconds. Default: wait forever
return
Promise
Resolves when message is processed
import { postMessageToThread, threadId } from 'node:worker_threads';

process.on('workerMessage', (value, source) => {
  console.log(`${source} -> ${threadId}:`, value);
  postMessageToThread(source, { message: 'pong' });
});

MessageChannel & MessagePort

MessageChannel

Represents an asynchronous, two-way communications channel.
import { MessageChannel } from 'node:worker_threads';

const { port1, port2 } = new MessageChannel();
port1.on('message', (message) => console.log('received', message));
port2.postMessage({ foo: 'bar' });
// Prints: received { foo: 'bar' }

MessagePort

One end of a MessageChannel. Extends EventTarget.

Events

close
Emitted once either side disconnects
message
Emitted for incoming messages. Receives the cloned value
messageerror
Emitted when deserializing fails

Methods

close()
Disables further sending of messages
postMessage(value[, transferList])
Sends a message to the receiving side
ref()
Keeps the event loop active
unref()
Allows the thread to exit
start()
Starts receiving messages (called automatically)
hasRef()
Returns true if the port keeps the event loop active

receiveMessageOnPort(port)

Receives a single message synchronously.
port
MessagePort | BroadcastChannel
required
The port to receive from
return
object | undefined
{ message: value } or undefined if no message available
import { MessageChannel, receiveMessageOnPort } from 'node:worker_threads';

const { port1, port2 } = new MessageChannel();
port1.postMessage({ hello: 'world' });

console.log(receiveMessageOnPort(port2));
// Prints: { message: { hello: 'world' } }
console.log(receiveMessageOnPort(port2));
// Prints: undefined

BroadcastChannel

Allows one-to-many communication with all other BroadcastChannel instances.
import { isMainThread, BroadcastChannel, Worker } from 'node:worker_threads';

const bc = new BroadcastChannel('hello');

if (isMainThread) {
  let c = 0;
  bc.onmessage = (event) => {
    console.log(event.data);
    if (++c === 10) bc.close();
  };
  for (let n = 0; n < 10; n++)
    new Worker(new URL(import.meta.url));
} else {
  bc.postMessage('hello from every worker');
  bc.close();
}

Methods

close()
Closes the connection
postMessage(message)
Broadcasts a message to all channels with the same name
ref()
Keeps the event loop active
unref()
Allows the thread to exit

Properties

onmessage
function
Invoked with a MessageEvent when a message is received
onmessageerror
function
Invoked when a message cannot be deserialized

Lock Manager (Experimental)

The Lock Manager API is experimental and under active development.

locks.request(name[, options], callback)

Request a lock to coordinate resource access across threads.
name
string
required
The name of the lock
options
object
Lock options
mode
string
default:"exclusive"
Either 'exclusive' or 'shared'
ifAvailable
boolean
default:"false"
Only grant if immediately available
steal
boolean
default:"false"
Release existing locks with the same name
signal
AbortSignal
Abort a pending lock request
callback
function
required
Invoked once the lock is granted. Lock is released when function returns or promise settles
return
Promise
Resolves when the lock is released
import { locks } from 'node:worker_threads';

await locks.request('my_resource', async (lock) => {
  // The lock has been acquired
  // Perform work with the protected resource
});
// The lock has been released

locks.query()

Returns a snapshot of currently held and pending locks.
return
Promise<LockManagerSnapshot>
Snapshot with held and pending arrays
import { locks } from 'node:worker_threads';

const snapshot = await locks.query();
for (const lock of snapshot.held) {
  console.log(`held lock: name ${lock.name}, mode ${lock.mode}`);
}
for (const pending of snapshot.pending) {
  console.log(`pending lock: name ${pending.name}, mode ${pending.mode}`);
}

Data Transfer Considerations

Structured Clone Algorithm

Messages use the HTML structured clone algorithm:
  • ✅ Circular references supported
  • ✅ Built-in types: RegExp, BigInt, Map, Set
  • ✅ Typed arrays with ArrayBuffer and SharedArrayBuffer
  • WebAssembly.Module instances
  • ✅ Specific Node.js types: CryptoKey, FileHandle, MessagePort, X509Certificate
  • ❌ Native C++ objects (except listed above)

TypedArrays and Buffers

Be careful when transferring ArrayBuffers:
const ab = new ArrayBuffer(10);
const u1 = new Uint8Array(ab);
const u2 = new Uint16Array(ab);

console.log(u2.length);  // prints 5

port.postMessage(u1, [u1.buffer]);

console.log(u2.length);  // prints 0 - ArrayBuffer was transferred!

Classes and Prototypes

Object cloning doesn’t preserve:
  • Non-enumerable properties
  • Property accessors
  • Object prototypes
  • Private fields
class Foo {
  #a = 1;
  get d() { return 4; }
}

port2.postMessage(new Foo());
// Receives: { c: 3 } - only enumerable properties

Best Practices

Worker Pool PatternDon’t spawn a Worker for each task - use a pool! The overhead of creating Workers exceeds their benefit for small tasks.
Memory LimitsUse resourceLimits to prevent workers from consuming excessive memory. Reaching limits terminates the worker.
Async TrackingWhen implementing worker pools, use AsyncResource for proper async stack traces.

Complete Example: Custom Channel

import { Worker, MessageChannel, MessagePort, isMainThread, parentPort } 
  from 'node:worker_threads';

if (isMainThread) {
  const worker = new Worker(new URL(import.meta.url));
  const subChannel = new MessageChannel();
  
  worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]);
  
  subChannel.port2.on('message', (value) => {
    console.log('received:', value);
  });
} else {
  parentPort.once('message', (value) => {
    value.hereIsYourPort.postMessage('the worker is sending this');
    value.hereIsYourPort.close();
  });
}