Node-RED MQTT JSON: Preserve 64-Bit Counters Before Automatic Parsing

Two distinct equipment counters arrive in a Node-RED flow, yet the first Function node sees the same value for both. Check the MQTT input’s payload conversion before changing the comparison. If the original decimal digits were rounded during parsing, a later conversion to BigInt cannot reconstruct them.

This article inspects Node-RED 5.0.8’s core MQTT implementation and ECMAScript 2025 numeric rules. The concrete contract below is an unsigned 64-bit counter, not an approximate measurement or a general-purpose JSON transformation.

Find the first lossy boundary

In the pinned MQTT receive handler, explicit JSON mode uses JSON.parse(). Auto-detect can also parse JSON, depending on payload and MQTT 5 metadata. Explicit Buffer and UTF-8 modes preserve bytes or text at that boundary. The legacy auto mode and auto-detect are different code paths; inspect the deployed node configuration rather than treating their names as interchangeable.

ECMAScript’s safe-integer limit is 2^53 - 1, or 9007199254740991. Beyond that limit, some integers remain exactly representable, but distinct integers can share one Number. Exact counter identity therefore needs a stronger representation contract than “JSON parsed successfully.”

const a = JSON.parse('{"counter":9007199254740992}').counter;
const b = JSON.parse('{"counter":9007199254740993}').counter;
console.log(a === b);                  // true
console.log(Number.isSafeInteger(b));  // false
console.log(BigInt(b).toString());     // "9007199254740992"

This standalone fixture produced the indicated results under Node.js v24.19.0. It exercises language behavior only; it is not a broker or Node-RED integration test. The final line preserves the already-rounded Number exactly. It does not recover the source token ending in 993.

Unquoted wide integer JSON tokens collapse into one Number, while decimal strings survive parsing and explicit string serialization.
Original representation-boundary diagram; standalone language behavior, no measured MQTT traffic.

Original representation-boundary diagram. The numeric example is a standalone language fixture, not measured MQTT traffic.

Preserve the digits before parsing them as Number

If the producer is under your control, define the field as a decimal string:

{"counter":"9007199254740993"}

Ordinary JSON parsing then preserves that member as a string. Update the schema and every affected consumer together: changing a number into a string can break arithmetic, validation, sorting or database mappings. Do not silently introduce the change on a shared production topic.

If the producer cannot change, configure MQTT In to deliver a Buffer or UTF-8 string and put a deliberately selected lossless parser at the next boundary. Review how that parser exposes integer tokens and how it serializes them again. A later ordinary JSON node would reintroduce the same problem. Avoid regex-rewriting arbitrary JSON; nested structures, escaped strings and number syntax require a real parser.

For a relay that needs no payload inspection, preserving the original Buffer can be the simpler design. For a flow that must calculate deltas, the exact numeric conversion needs to happen before any lossy Number conversion.

Validate one explicit counter schema

The following proposed Function-node body expects MQTT In to have parsed a producer-side string schema. Its outbound payload intentionally contains only counter. It accepts canonical nonnegative decimal spelling, rejects leading zeroes except 0, and enforces the unsigned 64-bit maximum. Extend the schema deliberately if more fields must survive.

const obj = msg.payload;
const s = obj && !Array.isArray(obj) ? obj.counter : undefined;
if (typeof s !== "string" || !/^(0|[1-9][0-9]{0,19})$/.test(s)) {
    node.error("counter must be a canonical uint64 decimal string", msg);
    return null;
}
const counter = BigInt(s);
if (counter > 18446744073709551615n) {
    node.error("counter exceeds uint64 range", msg);
    return null;
}
msg.payload = JSON.stringify({ counter: counter.toString() });
return msg;

The maximum is 2^64 - 1. The temporary BigInt supports an exact range check, while the outgoing JSON carries a decimal string. Default JSON serialization throws for an unconverted BigInt value. Converting the counter back to Number merely to make serialization succeed would undo the protection.

This body was exercised in a standalone Node.js harness with a mocked error reporter, not inside Node-RED. Valid zero, the adjacent large values and the uint64 maximum round-tripped as strings. A numeric member, a leading-zero string, a negative string and a value above the maximum were rejected. Actual flow wiring and error handling still require integration verification.

Verify the whole path with adjacent values

Prepare distinct source payloads for 9007199254740991, 9007199254740992 and 9007199254740993, plus 18446744073709551615. Preserve their original bytes. At the first downstream boundary, record the payload type and the counter’s type without formatting it through another numeric conversion.

Repeat the test in the selected input mode and inspect the exact outgoing payload with an independent subscriber. The acceptance condition is distinct, correct decimal strings at the receiving application, not merely different log lines before MQTT In. Check storage and dashboard consumers too: a final database mapper can still coerce the string into an unsuitable numeric type.

If exact source bytes are already unavailable and the only surviving value is an unsafe Number, label that sample as precision-uncertain or reacquire it according to the application’s policy. No downstream function can infer which of the colliding source integers was originally sent. No MQTT broker, Node-RED runtime or production counter was exercised here.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *