Can node-red node/flow run in parallel?

I am new to Node-RED.
I found that all javascript codes written in the node are processed in sequence in a flow (even using async function like setTimeout etc). This is also the same case among flows. Is Node-RED single threaded? Is there any setting to make Node-RED multiple threaded?
If Node-RED can only run by single thread, I would think about putting a loadbalancer in front of multiple node-red instances. Is there a reference architecture for that? Any recommendations?

1 Like

Hi @fangzhu

Node.js is single-threaded so only executes one piece of code at any time. However it uses its event loop to handle multiple workloads at the "same" time; whenever one event fires off some async work the next event can be handled.

Node-RED uses that same event loop to run the flows. If you have a flow that is entirely synchronous in its workload, then it will run in a single event. If that flow does any async work, then that allows the runtime to process multiple messages at the "same" time.

I disagree with your comment about setTimeout. Here is an example flow that use setTimeout in a Function node to send on the message it receives after 5 seconds. There's a second flow that connects an Inject node straight to a Debug node. If you trigger the first flow (and so trigger the setTimeout in the Function node), you can trigger the second flow repeatedly during that 5 seconds and you'll see the messages arrive - with the delayed message arriving after the 5 seconds is up.

[{"id":"330ca469.07ab8c","type":"inject","z":"6eeeab85.f1edd4","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":120,"y":560,"wires":[["925d4bf6.730988"]]},{"id":"925d4bf6.730988","type":"function","z":"6eeeab85.f1edd4","name":"","func":"\nsetTimeout(function() {\n    node.send(msg);\n    \n},5000)\nreturn","outputs":1,"noerr":0,"x":270,"y":560,"wires":[["61a7bd82.8a5d64"]]},{"id":"61a7bd82.8a5d64","type":"debug","z":"6eeeab85.f1edd4","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":430,"y":560,"wires":[]},{"id":"13756b6a.3cf425","type":"inject","z":"6eeeab85.f1edd4","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":120,"y":600,"wires":[["dd2be72a.17c888"]]},{"id":"dd2be72a.17c888","type":"debug","z":"6eeeab85.f1edd4","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":430,"y":600,"wires":[]}]

Regardless, scaling horizontally is certainly one pattern for scaling out flows. What sort of load balancing you do will depend on what events your flow is handling. With HTTP workloads, something like nginx can be used. We don't have any specific documentation on that because the principle is the same as putting a load balancer in front of any http-driven application - there are plenty of examples of that online.

Hello knolleary,
Thanks for your quick response.
I am a beginner, I might have done things in a wrong way.
I tried to simulate the scenario using 2 very simple flows:


When I triggered start1 then immediately triggered start2, I was expecting the nodes to be processed in a sequence of start1 -> sleep1-5s-> start2 ->sleep3-5s->sleep2-3s->sleep4-3s->end1->end2
But in reality from the log the sequence is like this:
start1 -> sleep1-5s-->sleep2-3s->end1->start2 ->sleep3-5s->sleep4-3s->end2
The sleep3-5 was waiting until the end1 was done.

Here is the exported code:
[{"id":"d75426b5.418bf8","type":"function","z":"b07350cb.8694d","name":"sleep1-5s","func":"function sleep(millis)\n{\n var date = new Date();\n var curDate = null;\n do { curDate = new Date(); }\n while(curDate-date < millis);\n}\n\n\nnode.warn(\"before sleep1:\"+new Date());\nsleep(5000);\nnode.warn(\"after sleep1:\"+new Date());\nreturn msg;","outputs":1,"noerr":0,"x":340,"y":100,"wires":[["1d827dc7.3a2f02"]]},{"id":"12b8036a.4cb88d","type":"inject","z":"b07350cb.8694d","name":"start1","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":100,"wires":[["d75426b5.418bf8"]]},{"id":"697e33e4.9b2c5c","type":"debug","z":"b07350cb.8694d","name":"end1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":730,"y":100,"wires":[]},{"id":"1d827dc7.3a2f02","type":"function","z":"b07350cb.8694d","name":"sleep2-3s","func":"function sleep(millis)\n{\n var date = new Date();\n var curDate = null;\n do { curDate = new Date(); }\n while(curDate-date < millis);\n}\n\n\n\nnode.warn(\"before sleep2:\"+new Date());\nsleep(3000);\nnode.warn(\"after sleep2:\"+new Date());\nreturn msg;","outputs":1,"noerr":0,"x":540,"y":100,"wires":[["697e33e4.9b2c5c"]]},{"id":"49331f52.31904","type":"function","z":"b07350cb.8694d","name":"sleep3-5s","func":"function sleep(millis)\n{\n var date = new Date();\n var curDate = null;\n do { curDate = new Date(); }\n while(curDate-date < millis);\n}\n\n\nnode.warn(\"before sleep3:\"+new Date());\nsleep(5000);\nnode.warn(\"after sleep3:\"+new Date());\nreturn msg;","outputs":1,"noerr":0,"x":340,"y":180,"wires":[["d11ffc91.cdccb"]]},{"id":"693741bf.69c3","type":"inject","z":"b07350cb.8694d","name":"start2","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":180,"wires":[["49331f52.31904"]]},{"id":"f0e787a1.d6ab18","type":"debug","z":"b07350cb.8694d","name":"end2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":730,"y":180,"wires":[]},{"id":"d11ffc91.cdccb","type":"function","z":"b07350cb.8694d","name":"sleep4-3s","func":"function sleep(millis)\n{\n var date = new Date();\n var curDate = null;\n do { curDate = new Date(); }\n while(curDate-date < millis);\n}\n\n\n\nnode.warn(\"before sleep4:\"+new Date());\nsleep(3000);\nnode.warn(\"after sleep4:\"+new Date());\nreturn msg;","outputs":1,"noerr":0,"x":540,"y":180,"wires":[["f0e787a1.d6ab18"]]}]

Unfortunately your flow isn't currently importable.Please read the following post How to share code or flow json and then edit the above message.

The sleep function you have created stops node.js from doing anything else:

function sleep(millis)
{
 var date = new Date();
 var curDate = null;
 do { curDate = new Date(); }
 while(curDate-date < millis);
}

This is because it is entirely synchronous code - it never gives control back to the node.js event loop. This is why you would use setTimeout if you want to introduce a delay. That is built to allow the node.js event loop to perform other work whilst waiting for the timeout to occur.

Also please read the post @ukmoose linked to - I had to manually repair your flow before I could import it to look.

This comes up a lot, I certainly fell into the first time I tried to post a sample. I'm wondering why this forum doesn't have a "code" tag to use when pasting, sort of like the "html link" button.

Initially I naively assumed the "Block-quote" or "pre-formatted text: buttons would do it "right" but obviously I was wrong.

The ``` the triple backticks seem to be the kind of arcane knowledge that just drives me nuts,

Rather than backticks, the other option is to use the code block format which achieves the same thing. It's right there in the toolbar and yet still gets ignored.

Can_node-red_node_flow_run_in_parallel__-General-_Node-RED_Forum

The disadvantage of that button is that it indents your content by 4 spaces. As soon as you hit enter, the indent resets. So if you want to write a block of code, I personally find it quicker to use the backticks - then I don't have to think about getting the indentation right.

1 Like

I tried that, it didn't seem to work I'll try again:
[
{
"id": "6d1d25e2.4e534c",
"type": "mqtt in",
"z": "6d590249.a8763c",
"name": "Garage Door Alert",
"topic": "Alarm/GarageDoorAlert",
"qos": "1",
"broker": "fd073680.473468",
"x": 130,
"y": 1560,
"wires": [
[
"b2d4636f.2287c"
]
]
},
{
"id": "618ad771.37baa8",
"type": "exec",
"z": "6d590249.a8763c",
"command": "/usr/bin/espeak-ng "Garage Door Is Open!"",
"addpay": false,
"append": "",
"useSpawn": "false",
"timer": "",
"oldrc": false,
"name": "Espeak Garage Door Open",
"x": 920,
"y": 1560,
"wires": [
[],
[],
[]
]
},
{
"id": "3aa3b985.361356",
"type": "delay",
"z": "6d590249.a8763c",
"name": "",
"pauseType": "rate",
"timeout": "5",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "2",
"rateUnits": "minute",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": true,
"x": 680,
"y": 1560,
"wires": [
[
"618ad771.37baa8"
]
]
},
{
"id": "b2d4636f.2287c",
"type": "switch",
"z": "6d590249.a8763c",
"name": "Send Garage Door Alert",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "cont",
"v": "True",
"vt": "str"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 410,
"y": 1560,
"wires": [
[
"3aa3b985.361356"
],
[]
]
},
{
"id": "fd073680.473468",
"type": "mqtt-broker",
"z": "",
"name": "",
"broker": "192.168.2.140",
"port": "1883",
"clientid": "",
"usetls": false,
"compatmode": true,
"keepalive": "60",
"cleansession": true,
"birthTopic": "",
"birthQos": "0",
"birthPayload": "",
"closeTopic": "",
"closePayload": "",
"willTopic": "",
"willQos": "0",
"willPayload": ""
}
]

That seems to put [] around the code. I think the trick is you have to paste into the "Highlight" that appears. Initially I think I dried to paste after clicking outside the highlight.

Might I suggest changing the the "tool tip" to be "code insertion" or something even clearer?

I've no hope of live typing code into a webpage, its cut and paste or nothing for me :slight_smile:

I'm sure you could suggest it to discourse who produce the forum software.

I suggest you to use the delay module in Node Red. Remember you can reset the message inside (in means after the delay will not be output) by sending an object containing only reset:1
I use only this on my flows, it saves me from code troubles

Thanks for the reply.
I was just trying to evaluate Node-Red's behavior. sleep was not a thing that I wanted to do. Instead I tried to simulate the scenario that a "function node" needs to process a lot of logic using javascript and I found that will stop other flows (even using setTimeout, it can complete the node and move to the next node, but as long as the actual javascript runs, other flows need to wait). -> Is this the expected behavior?

Thanks for the guide.
I just modified and hope this time I made it right.

I am aware that if I add a delay node, it will help me to let other node process first. But it seems it still won't allow actual javascript code to be processed in parallel.

Not sure what you mean by this. A Delay node will not stop other nodes running while it is delaying. Also setTimeout will provide the same feature inside a function node.

I mean, after delay, the next node still cannot run if other node is running. Isn't it?

There can only be one piece of code actively executing at any time. That is how node.js works.

Using busy-loops like you had in your test flow is a bad design pattern for node.js. If you have a node that needs to do heavy workloads, you need to consider how to allow that node to yield to the node.js event loop so that other workloads are given an chance to make progress.

This whole topic is not unique to Node-RED - it comes from the node.js runtime.

Can you make a simple flow showing the problem that you think you have when using the delay node?

If the Delay node is set for 5 seconds, and at the moment those 5 seconds passes another node is currently active in the node.js event loop, the Delay node will have to wait until that node finishes before it gets its turn to continue processing. So the 5 second delay could actually be longer - depending on how much time that other node ties up the event loop.

Thanks for the response.
That is the behavior I observed. So there is no way to make the other node to work immediately after delay?
What I am trying to do is to create a aggregation kind of flow, as introduced in following blog ( 6. Aggregator pattern):



But I cannot find a way to make "payload=451" and "payload=326" work in parallel as they always needs to wait for each other.

So what is the problem with the pattern as they describe? It shows how to split a flow into two separate branches and then recombine the result. It is true to say if both of those functions are entirely synchronous in what they do, one will run before the other. But imagine if each branch did an HTTP Request or database request - in that case both branches would be able to make progress whilst the other did async work.