The Node-RED function node lets you run your own JavaScript on every message that flows through the editor, which makes it the most flexible tool you have for reshaping MQTT data. In this guide you will use it to convert units, filter out bad readings, route messages to several outputs, keep running state and handle errors cleanly, all against a live broker you can reach in seconds.
What the Node-RED Function Node Actually Does
Every message in Node-RED is a plain JavaScript object called msg. The function node receives that object, runs the code you wrote, and passes on whatever you return. Return the same object and the message continues. Return null and the message stops. Return an array and each element goes to a different output port. That is the whole contract, and almost everything else is a variation on it.
Inside the function you have access to a small set of helpers:
| Object | What it is for |
|---|---|
msg |
The incoming message, usually with payload and topic |
node |
Logging (node.warn, node.error), status (node.status), async sending (node.send, node.done) |
context, flow, global |
Storing values between messages at node, tab or instance scope |
env |
Reading environment variables and subflow properties with env.get() |
RED.util |
Utilities such as RED.util.cloneMessage() |
The editor also has three tabs: On Start runs once when the node is deployed, On Message runs for each message, and On Stop runs when the flow is stopped or redeployed. On Start is the right place to initialise counters, and On Stop is where you clear timers you created.
If you have not wired an MQTT flow before, start with our Node-RED MQTT tutorial, which covers the broker configuration node and the mqtt in and mqtt out nodes in detail.
Set Up a Test Feed on the Public Broker
You need a stream of messages to work with. The free public MQTT broker at broker.connectmq.com accepts anonymous connections, so you can publish test data from a terminal without any signup. Pick a unique prefix so your topics do not collide with anyone else's:
# Publish a reading every two seconds (Ctrl+C to stop)
while true; do
T=$(awk -v r=$RANDOM 'BEGIN{printf "%.1f", 18 + (r % 150) / 10}')
mosquitto_pub -h broker.connectmq.com -p 1883 -i demo-pub-7f3a \
-t "connectmq/demo/7f3a/line1/sensor1/telemetry" \
-m "{\"temp\": $T, \"unit\": \"C\", \"ts\": $(date +%s)}"
sleep 2
done
In Node-RED, add an mqtt in node, point its server at broker.connectmq.com port 1883, give the broker node a non-empty unique client ID, subscribe to connectmq/demo/7f3a/#, and set Output to "a parsed JSON object". Now every message arrives with msg.payload already parsed.
Remember that the public area is readable by everyone. Use it for experiments only, never for real plant or customer data.
Five Node-RED Function Node Examples for MQTT
1. Transform and enrich a payload
Devices rarely publish exactly what your dashboard or database wants. This function converts Celsius to Fahrenheit, adds a site label and pulls the device name out of the topic:
// On Message
const parts = msg.topic.split("/"); // connectmq/demo/7f3a/line1/sensor1/telemetry
const line = parts[3];
const device = parts[4];
msg.payload = {
device: device,
line: line,
tempC: msg.payload.temp,
tempF: Math.round((msg.payload.temp * 9 / 5 + 32) * 10) / 10,
receivedAt: new Date().toISOString()
};
return msg;
Splitting the topic is cheaper and more reliable than asking every device to repeat its identity inside the payload.
2. Filter bad readings
Sensors glitch. A function node that returns null drops a message silently, so you can enforce sanity limits before data reaches storage:
const t = msg.payload.temp;
if (typeof t !== "number" || Number.isNaN(t)) {
node.warn("Non-numeric temp on " + msg.topic);
return null;
}
if (t < -40 || t > 125) {
return null; // outside sensor range, discard
}
return msg;
node.warn writes to the debug sidebar and the Node-RED log, which helps you see how often filtering happens without adding extra debug nodes.
3. Route to multiple outputs
Set Outputs to 2 in the node settings, then return an array. Position 0 goes to the first port, position 1 to the second. Use null for a port that should not fire:
const limit = 30;
if (msg.payload.temp > limit) {
const alert = {
topic: msg.topic.replace("/telemetry", "/alert"),
payload: { level: "high", temp: msg.payload.temp }
};
return [msg, alert]; // normal data + alert
}
return [msg, null]; // normal data only
The second output can feed an mqtt out node with an empty topic field, so it publishes to whatever msg.topic says. For simple threshold routing the Switch node is often clearer; reach for a function when the condition involves calculations or several fields.
4. Keep state between messages
Node context survives between messages for as long as the flow is running. This example publishes only when the value changes by more than half a degree, a simple deadband that cuts traffic a lot on slow-moving signals:
const key = "last_" + msg.topic;
const last = context.get(key);
const current = msg.payload.temp;
if (last !== undefined && Math.abs(current - last) < 0.5) {
return null;
}
context.set(key, current);
return msg;
If you need the value on another tab or after a restart, use flow, global or a persistent store. Our guide to Node-RED context explains the scopes and how to enable file-backed storage.
5. Send asynchronously
When your code waits on a timer or a promise, returning from the function is too early. Use node.send() and call node.done() when finished so Complete and Catch nodes work correctly:
// Batch readings and send a summary every 10 messages
let buf = context.get("buf") || [];
buf.push(msg.payload.temp);
if (buf.length >= 10) {
const avg = buf.reduce((a, b) => a + b, 0) / buf.length;
context.set("buf", []);
setTimeout(() => {
node.send({ topic: "connectmq/demo/7f3a/line1/sensor1/avg", payload: { avg: Number(avg.toFixed(2)) } });
node.done();
}, 0);
} else {
context.set("buf", buf);
node.done();
}
return null;
A Complete Flow You Can Import
Copy this JSON, press Ctrl+I in the editor, paste it and deploy. Then run the publish loop from earlier. Change the 7f3a prefix to your own random string in both places.
[
{"id":"b1","type":"mqtt-broker","name":"ConnectMQ public","broker":"broker.connectmq.com","port":"1883","clientid":"nr-demo-7f3a","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true},
{"id":"in1","type":"mqtt in","z":"t1","name":"telemetry","topic":"connectmq/demo/7f3a/+/+/telemetry","qos":"0","datatype":"json","broker":"b1","x":130,"y":100,"wires":[["fn1"]]},
{"id":"fn1","type":"function","z":"t1","name":"enrich + alert","func":"const p = msg.topic.split('/');\nmsg.payload.device = p[4];\nmsg.payload.tempF = Math.round((msg.payload.temp * 9 / 5 + 32) * 10) / 10;\nif (msg.payload.temp > 30) {\n return [msg, {topic: msg.topic.replace('/telemetry','/alert'), payload: {level:'high', temp: msg.payload.temp}}];\n}\nreturn [msg, null];","outputs":2,"x":330,"y":100,"wires":[["dbg1"],["out1"]]},
{"id":"dbg1","type":"debug","z":"t1","name":"data","active":true,"complete":"payload","x":530,"y":80,"wires":[]},
{"id":"out1","type":"mqtt out","z":"t1","name":"alerts","topic":"","qos":"1","retain":"false","broker":"b1","x":530,"y":130,"wires":[]},
{"id":"t1","type":"tab","label":"Function demo"}
]
Error Handling and Debugging
A thrown exception inside a function node logs an error, but by default the message simply disappears. Two habits make failures visible.
First, use node.error(err, msg) with the message as the second argument. That form triggers any Catch node on the same tab, so you can route failures to a dedicated debug node, a log file or an alert topic.
try {
msg.payload = JSON.parse(msg.payload.raw);
return msg;
} catch (err) {
node.error("Bad raw payload: " + err.message, msg);
return null;
}
Second, use node.status() to show what the node is doing directly on the canvas, for example the last value seen or a count of dropped messages:
node.status({ fill: "green", shape: "dot", text: "last " + msg.payload.temp + " C" });
Common Mistakes Checklist
- Mutating a message you also forward elsewhere. If one output goes to two wires, Node-RED clones the message for you, but objects you stash in context are references. Use
RED.util.cloneMessage(msg)before storing. - Forgetting to return. A function with no return statement sends nothing, which looks like a broken subscription.
- Using a string as an object. If the
mqtt innode outputs a string,msg.payload.tempisundefined. Set the output to parsed JSON or add a JSON node; see the Node-RED JSON node guide. - Leaving timers running. Clear intervals in On Stop, or each redeploy adds another one.
- Doing heavy work per message. A function node runs on the same event loop as the rest of Node-RED. Long loops on high-rate topics will slow every flow. Throttle first with the Node-RED delay node.
- Putting secrets in code. Use
env.get("API_KEY")rather than pasting tokens into the function body, because flow files are often shared or committed to git.
When Not to Use a Function Node
The function node is powerful, but flows built from twenty function nodes are hard for teammates to read. Prefer the Change node for setting and moving properties, the Switch node for routing on simple conditions and the JSON node for parsing. Keep function nodes for logic that genuinely needs code, and give each one a clear name. If the same function appears on several tabs, package it as a subflow so you fix bugs in one place.
You can also load npm modules into a function node through the Setup tab when functionExternalModules is enabled in settings.js. That is handy for things like date formatting libraries, but every module you add is another dependency to update and audit. The official function node documentation lists everything available in the sandbox.
Run Your Node-RED Flows on ConnectMQ
Once your function logic works on the public broker, move it to a private broker by editing one configuration node. ConnectMQ is a managed MQTT broker with TLS on port 8883, MQTT 3.1.1 and 5.0, and a portal with an in-browser client and live topic explorer, so you can watch the alerts your function publishes. Create a free Serverless broker at app.connectmq.com without a credit card, copy the generated username, password and client ID into the Node-RED broker node, tick Use TLS, and redeploy. If you are comparing hosting options, our overview of a cloud MQTT broker explains what to look for.
Frequently Asked Questions
Can a Node-RED function node use async/await?
Yes. The function body runs inside an async wrapper, so you can await promises. Send results with node.send() and call node.done() rather than relying on the return value after awaiting.
How do I send multiple messages from one function node?
Return an array of messages for different outputs, or an array inside an array, such as return [[m1, m2, m3]], to send several messages in sequence from the first output.
Why does my function node output nothing?
Usually the function returns null or undefined, or an exception was thrown. Check the debug sidebar for errors and add node.warn(msg) at the top to confirm messages are arriving.
Is the function node slow for high-frequency MQTT data?
Simple transformations are fast, but every function runs on the single Node.js event loop. For very high message rates, keep code lightweight and filter or rate-limit early in the flow.



