Skip to content

Worker Function Node

Purpose & Use Cases

The Worker Function node executes JavaScript code in isolated worker threads by default (or child processes when configured), preventing CPU-intensive operations from blocking the Node-RED event loop. Unlike the standard Function node which runs synchronously on the main thread, this node offloads execution to a pool of workers, keeping your Node-RED instance responsive even during heavy computations.

This node is ideal for operations that would otherwise cause Node-RED to become unresponsive: cryptographic operations, prime number calculations, large dataset transformations, image/file processing, and any computation that takes more than 10-20ms to complete. For simple operations like basic math or property transformations, continue using the standard Function node to avoid the ~5-10ms overhead of worker communication (child process mode is higher).

Real-World Applications:

  • Cryptographic operations: Hashing, encryption, key derivation (bcrypt, scrypt, PBKDF2)
  • Data transformations: Processing large JSON datasets, CSV parsing, XML transformations
  • Image processing: Resizing, format conversion, metadata extraction (with sharp, jimp)
  • Mathematical computations: Prime number generation, statistical analysis, matrix operations
  • File operations: Reading/parsing large files, compression/decompression
  • Text processing: Large regex operations, string manipulation on big text blocks
  • Scientific computing: Simulations, numerical analysis, data modeling

Input/Output Specification

Inputs

The node accepts any standard Node-RED message object. The msg object is serialized and sent to the worker for processing.

Supported input types:

  • Primitives: strings, numbers, booleans, null
  • Objects: Plain JavaScript objects (must be serializable)
  • Arrays: Arrays of any serializable types
  • Buffers: Binary data (zero-copy transfer in worker threads, shared memory fallback)
  • Nested structures: Deep object hierarchies are fully supported

Not supported:

  • Functions (cannot be serialized)
  • Symbols
  • Circular references (will cause serialization error)
  • Class instances (converted to plain objects)

Message optimization: The node analyzes your code to detect which msg.* properties are referenced and only transfers those properties to the worker. This optimization reduces serialization overhead for messages with many properties when only a few are used.

Outputs

The node supports 0-10 output ports, configured in the Setup tab.

Single output (default):

javascript
// Return the modified message
msg.payload = msg.payload * 2;
return msg;

Multiple outputs:

javascript
// Return array of messages (one per output)
if (msg.payload > 100) {
    return [msg, null];  // Send to output 1 only
} else {
    return [null, msg];  // Send to output 2 only
}

Stop the flow:

javascript
// Return null to prevent any output
if (!msg.payload) {
    return null;
}
return msg;

Send multiple messages to one output:

javascript
// Return array within array
return [[msg1, msg2, msg3]];  // Three messages to output 1

Configuration Options

Input/Output Paths

PropertyDescriptionDefault
InputSingle input port accepting any msg object1 input
OutputsNumber of output ports (0-10)1 output

Behavior Settings

Outputs

  • Range: 0-10
  • Default: 1
  • Behavior: Determines how many output ports the node exposes. Use array returns to route messages to specific outputs.

Timeout

  • Range: 1,000 - 300,000 ms
  • Default: 30,000 ms (30 seconds)
  • Behavior: Maximum execution time for user code. If exceeded, the worker is terminated and an error is thrown. The timeout starts after message serialization completes.

Runtime

  • Options: worker_threads, child_process
  • Default: worker_threads
  • Behavior: Worker threads are fastest. Use child processes for native modules that require the main thread (e.g., gl), with higher overhead.

Worker Pool Settings

Workers

  • Range: 1-16
  • Default: 3
  • Behavior: Fixed number of workers maintained by this node. Each worker-function node maintains its own independent pool. More workers allow more parallel executions but consume more memory (~10-20MB per worker).

Guidance:

  • 1-2 workers: Low-volume flows or memory-constrained environments
  • 3-4 workers: Typical use cases with moderate parallelism
  • 8-16 workers: High-throughput scenarios with many concurrent messages

Queue Size

  • Range: 10-1,000
  • Default: 100
  • Behavior: Maximum messages that can wait when all workers are busy. When the queue is full, new messages are rejected with a "Task queue full" error. Messages in the queue are processed FIFO (first-in, first-out).

Modules Settings

The Modules section in the Setup tab allows you to add external npm dependencies that will be available in your code.

Adding Modules

  1. Open the node configuration dialog
  2. Switch to the Setup tab
  3. In the Modules section, click the + add button
  4. Enter the Module name (npm package name, e.g., lodash)
  5. Enter the Import as variable name (e.g., _)

Auto-Installation Behavior

When the node starts:

  1. It checks if each configured module is available
  2. If a module is missing, it automatically runs npm install <module> in the Node-RED user directory (~/.node-red)
  3. Installation logs appear in the Node-RED console
  4. If installation fails, an error is logged and the module will be unavailable

Notes:

  • Installation happens synchronously during node initialization
  • The NODE_RED_HOME environment variable can override the install location
  • Scoped packages are supported (e.g., @scope/package)
  • Version specifiers are supported (e.g., lodash@4.17.21)

Using Modules in Code

Configured modules are injected as variables in your code. You do not need to use require() for configured modules.

Example with lodash:

Setup tab configuration:

ModuleImport as
lodash_

Function tab code:

javascript
// _ is available directly - no require needed
const result = _.groupBy(msg.payload, 'category');
msg.payload = result;
return msg;

Example with moment:

Setup tab configuration:

ModuleImport as
momentmoment

Function tab code:

javascript
msg.timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
msg.payload.createdAt = moment(msg.payload.date).fromNow();
return msg;

Example with multiple modules:

Setup tab configuration:

ModuleImport as
lodash_
uuiduuid
crypto-jsCryptoJS

Function tab code:

javascript
// All modules available as variables
msg.payload = _.map(msg.payload, item => ({
    ...item,
    id: uuid.v4(),
    hash: CryptoJS.SHA256(item.name).toString()
}));
return msg;

Variable Name Restrictions

The "Import as" variable name must be:

  • A valid JavaScript identifier (starts with letter, _, or $)
  • Not a reserved name: msg, require, console, setTimeout, setInterval, clearTimeout, clearInterval, Buffer, process

Performance Notes

Worker Thread Overhead

Each message incurs approximately 5-10ms overhead for:

  • Message serialization to worker
  • Worker context setup
  • Result deserialization back to main thread

Note: Child process runtime has higher overhead due to full process IPC and larger memory footprint.

Recommendation: Only use the worker-function node when your code execution time exceeds 10-20ms. For simple operations, the standard function node is more efficient.

Native Backend Subsystem

The node uses Node.js worker_threads module by default, with additional optimizations:

  • Code Caching: User code is compiled once per unique code string and cached (LRU cache with 100 entry limit). Subsequent executions reuse the compiled AsyncFunction.
  • Message Optimization: Only msg.* properties referenced in your code are serialized and sent to workers.
  • Buffer Transfer: Worker threads use zero-copy transfer for eligible Buffers. Child process mode and non-transferable buffers fall back to filesystem-based shared memory.

Processing Characteristics

  • Parallel Execution: Multiple messages process simultaneously up to the worker count
  • Queue Management: Messages exceeding worker capacity are queued (FIFO)
  • Auto-Recovery: Crashed or timed-out workers are automatically replaced
  • Isolation: Each worker runs in a separate V8 isolate with its own memory
  • No Blocking: Main Node-RED event loop remains responsive regardless of worker activity
  • Memory Usage: Each worker uses approximately 10-20MB of memory

Performance Metrics

Every processed message includes timing information in msg.performance[nodeName]:

javascript
msg.performance["my worker function"] = {
    transferToWorkerMs: 1.23,  // Time to restore buffers in worker
    executionMs: 45.67,        // Time to execute user code
    transferToMainMs: 0.89,    // Time to serialize result
    totalMs: 52.34             // End-to-end processing time
}

Metric descriptions:

MetricDescription
transferToWorkerMsTime spent restoring buffers before code execution
executionMsTime spent executing your JavaScript code
transferToMainMsTime spent serializing the result for transfer back to main thread
totalMsTotal wall-clock time from message receipt to output

Shared Memory

How Large Buffers Are Handled

When messages contain Buffer objects, the node prefers zero-copy transfer in worker threads. If that isn't possible (or when running in child process mode), it falls back to shared memory:

  1. Detection: Buffer objects in the message are identified during serialization
  2. Transfer (worker threads): Buffers are transferred by ownership when safe (zero-copy)
  3. Fallback (child process / non-transferable): Buffers are written to shared memory
  4. Restoration: The worker restores Buffers before executing your code
  5. Cleanup: Shared memory files are automatically deleted after task completion

Platform-Specific Behavior

PlatformShared Memory PathCharacteristics
Linux/dev/shmRAM-backed tmpfs, zero disk I/O
macOSos.tmpdir()Disk-based, may involve I/O
Windowsos.tmpdir()Disk-based, may involve I/O

File Naming Convention

Shared memory files follow this pattern:

rosepetal-async-{pid}-{taskId}-{bufferIndex}-{timestamp}-{random}.bin

Cleanup and Orphan Detection

  • Normal cleanup: Files are deleted immediately after task completion
  • Orphan detection: On startup, files older than 1 hour matching the naming pattern are deleted
  • Fallback: If shared memory write fails, buffers are base64-encoded (slower but reliable)

Buffer Performance Tips

  • Zero-copy transfer avoids file I/O for large Buffers in worker threads
  • Shared memory remains the fallback path when transfer isn't available
  • Event loop never blocks, even when processing multi-MB buffers
  • For best performance on Linux, ensure /dev/shm has sufficient space

Limitations

Not Available in Worker Code

FeatureReasonAlternative
node.send()Node instance not availableUse return statement
env.get()Environment helper not availableAccess process.env directly

Context + Node Helpers (Snapshot)

context.get/set, flow.get/set, and global.get/set are available with snapshot semantics. Reads come from the snapshot taken at the start of execution; writes are applied after your function completes. The snapshot only includes literal keys detected in your code (e.g., flow.get("count")). Store-specific context selection is not supported (default store only). node.warn/error/log() are collected and forwarded to the main thread.

Example:

javascript
const count = flow.get('count') || 0;
flow.set('count', count + 1);
node.warn(`count=${count + 1}`);

Serialization Requirements

The msg object must be serializable:

  • No functions: Functions cannot be transferred between threads
  • No symbols: Symbols are not serializable
  • No circular references: Will cause serialization to fail
  • Class instances: Converted to plain objects (prototype chain lost)

Other Limitations

  • ~5-10ms overhead: Not suitable for sub-millisecond operations
  • Memory usage: Each worker consumes ~10-20MB
  • Limited node access: node.warn/error/log() are available, but other node methods are not
  • Snapshot context: Context reads are from the snapshot; updates apply after execution

Real-World Examples

CPU-Intensive Prime Number Calculation

[inject: 50000] -> [worker-function: Calculate Primes] -> [debug]
javascript
// Calculate all prime numbers up to msg.payload
// This would freeze Node-RED if run in standard function node

function isPrime(n) {
    if (n <= 1) return false;
    if (n <= 3) return true;
    if (n % 2 === 0 || n % 3 === 0) return false;
    for (let i = 5; i * i <= n; i += 6) {
        if (n % i === 0 || n % (i + 2) === 0) return false;
    }
    return true;
}

const limit = msg.payload;
const primes = [];

for (let i = 2; i <= limit; i++) {
    if (isPrime(i)) primes.push(i);
}

msg.payload = primes;
msg.count = primes.length;
return msg;

Prevents event loop blocking during heavy computation.

Password Hashing with bcrypt

[http-in: POST /register] -> [worker-function: Hash Password] -> [database] -> [http-response]

Setup tab - Modules:

ModuleImport as
bcryptbcrypt

Function tab:

javascript
// Hash password with bcrypt (CPU-intensive)
const saltRounds = 12;
const hash = await bcrypt.hash(msg.payload.password, saltRounds);

msg.payload.passwordHash = hash;
delete msg.payload.password;  // Remove plaintext
return msg;

bcrypt with 12 rounds takes ~300ms - offloading prevents blocking.

Large JSON Dataset Processing

[file-in: data.json] -> [worker-function: Process Data] -> [split] -> [database]

Setup tab - Modules:

ModuleImport as
lodash_

Function tab:

javascript
// Process large dataset with lodash
const data = JSON.parse(msg.payload);

// Group, filter, and transform (CPU-intensive for large datasets)
const processed = _.chain(data)
    .filter(item => item.status === 'active')
    .groupBy('category')
    .mapValues(items => ({
        count: items.length,
        total: _.sumBy(items, 'value'),
        average: _.meanBy(items, 'value'),
        items: _.sortBy(items, 'date')
    }))
    .value();

msg.payload = processed;
return msg;

Handles datasets with thousands of records without blocking.

Image Metadata Extraction

[file-in: photo.jpg] -> [worker-function: Extract EXIF] -> [debug]

Setup tab - Modules:

ModuleImport as
exif-parserExifParser

Function tab:

javascript
// Extract EXIF metadata from image buffer
const parser = ExifParser.create(msg.payload);
const result = parser.parse();

msg.exif = {
    make: result.tags.Make,
    model: result.tags.Model,
    datetime: result.tags.DateTimeOriginal,
    gps: result.tags.GPSLatitude ? {
        lat: result.tags.GPSLatitude,
        lon: result.tags.GPSLongitude
    } : null,
    dimensions: {
        width: result.imageSize.width,
        height: result.imageSize.height
    }
};

return msg;

Binary buffer transferred efficiently (zero-copy when possible).

Cryptographic Hash Generation

[inject: "secret data"] -> [worker-function: Generate Hashes] -> [debug]

Function tab (no external modules needed):

javascript
const crypto = require('crypto');
const data = msg.payload;

// Generate multiple hash types
msg.hashes = {
    md5: crypto.createHash('md5').update(data).digest('hex'),
    sha1: crypto.createHash('sha1').update(data).digest('hex'),
    sha256: crypto.createHash('sha256').update(data).digest('hex'),
    sha512: crypto.createHash('sha512').update(data).digest('hex')
};

return msg;

Uses built-in crypto module - no setup required.

Conditional Routing with Multiple Outputs

[mqtt-in] -> [worker-function: Route by Priority (3 outputs)] -> [high-priority-queue]
                                                              -> [normal-queue]
                                                              -> [low-priority-queue]

Configuration: Outputs = 3

Function tab:

javascript
// Route messages based on computed priority
const data = msg.payload;

// Complex priority calculation (example)
let priority = 0;
if (data.urgent) priority += 50;
if (data.customer_tier === 'premium') priority += 30;
if (data.value > 1000) priority += 20;

msg.priority = priority;

// Route to appropriate output
if (priority >= 70) {
    return [msg, null, null];  // High priority (output 1)
} else if (priority >= 30) {
    return [null, msg, null];  // Normal priority (output 2)
} else {
    return [null, null, msg];  // Low priority (output 3)
}

Routes messages to different outputs based on computed criteria.

Common Issues & Troubleshooting

Task Queue Full Error

Issue: Messages are rejected with "Task queue full" error.

Cause: All workers are busy and the queue has reached its maximum size (default: 100).

Solution:

  • Increase the Queue Size in the Setup tab
  • Increase the number of Workers to process messages faster
  • Add flow control upstream (e.g., delay node, rate limit)
  • Optimize your code to execute faster

Execution Timeout Error

Issue: Tasks fail with "Execution timeout" error.

Cause: User code took longer than the configured timeout (default: 30 seconds).

Solution:

  • Increase the Timeout value in the Setup tab
  • Optimize your code for better performance
  • Break large operations into smaller chunks
  • Check for infinite loops in your code

Module Not Found Error

Issue: Module fails to load with "Cannot find module" error.

Cause: The npm module is not installed or installation failed.

Solution:

  • Check Node-RED console for installation errors
  • Manually install the module: cd ~/.node-red && npm install <module>
  • Verify the module name is spelled correctly
  • For scoped packages, use the full name: @scope/package

Serialization Errors

Issue: Message fails to serialize with circular reference or function errors.

Cause: The msg object contains non-serializable data (functions, circular refs, symbols).

Expected: Only serializable data should be included in msg.

Solution:

  • Remove functions from msg before sending
  • Break circular references in objects
  • Convert class instances to plain objects with JSON.parse(JSON.stringify(obj))

Worker Crash/Restart

Issue: Workers crash and restart frequently.

Cause: Unhandled exceptions or promise rejections in user code.

Solution:

  • Wrap code in try/catch blocks
  • Handle promise rejections with .catch() or try/catch with async/await
  • Check for null/undefined access errors
  • Use console.error() to debug issues

High Memory Usage

Issue: Node-RED process uses excessive memory.

Cause: Too many workers configured or memory leaks in user code.

Solution:

  • Reduce the number of workers (each uses ~10-20MB)
  • Check for memory leaks in your code (unreleased references, growing arrays)
  • Monitor with msg.performance to identify slow operations
  • Consider processing large datasets in smaller batches

Status Shows Yellow/Red

Issue: Node status shows yellow or red indicator.

Expected: Green = normal, Yellow = queue > 50, Red = queue > 90% or error.

Solution:

  • Yellow: Queue is getting full - consider adding workers or upstream flow control
  • Red: Queue almost full or recent error - check logs and consider scaling up
  • Ring shape: All workers busy with backlog - increase workers or optimize code