Skip to main content

Scaling and limits

This page collects the knobs and constraints you will hit once your scripted server moves out of toy territory and starts serving real clients with real address spaces.

What bootstrapServer exposes​

The boot config covers the everyday knobs: port, endpoint, applicationName, productUri (a URI, see below), nodesets, securityPolicies, securityModes, allowAnonymous, users, discoveryServerEndpointUrl, registerServerMethod, shutdownTimeoutMs, forceRebuild, and the onPopulate callback.

Anything else that affects scale — connection / session limits, operation limits, frugal address-space mode — you tune from inside onPopulate, where you have full access to the live addressSpace.

Frugal address space​

If you have a lot of variables (thousands), enable addressSpace.isFrugal = true inside onPopulate while you build them. This skips reverse-reference materialisation that is rarely needed for raw I/O variables and dramatically reduces memory overhead.

onPopulate: (addressSpace, exposed) => {
const namespace = addressSpace.getOwnNamespace();
const dataset = namespace.addObject({ organizedBy: "ObjectsFolder", browseName: "Dataset" });

addressSpace.isFrugal = true;
for (let i = 0; i < 5000; i++) {
namespace.addVariable({
browseName: `Var${i.toString().padStart(4, "0")}`,
nodeId: `s=Var${i.toString().padStart(4, "0")}`,
componentOf: dataset,
dataType: "Double",
value: { dataType: "Double", value: 0 },
});
}
addressSpace.isFrugal = false;
},

Re-enable normal mode (isFrugal = false) before adding nodes that need full bidirectional references (e.g., type instances, methods on objects clients will browse).

Sampling and update rate​

A high-frequency setValueFromSource loop does not cause network traffic by itself. Network traffic is generated by monitored items that clients subscribed to. The relevant knobs:

  • minimumSamplingInterval on each variable: a hint to the server about the fastest meaningful sample rate. Default is 0 (server samples on every change).
  • Subscription publishingInterval and monitored-item samplingInterval: negotiated per client. Set minimumSamplingInterval to clamp these from below.
  • For variables that change at sub-millisecond rates, batch externally: write the value once per tick (e.g. every 50 ms) rather than firing setValueFromSource from a tight loop.
namespace.addVariable({
browseName: "FastSensor",
nodeId: "s=FastSensor",
dataType: "Double",
minimumSamplingInterval: 50, // 50 ms == 20 Hz
value: { dataType: "Double", value: 0 },
});

Cleaning up timers​

If your boot path starts intervals or timeouts (sensor simulators, refresh ticks), register them with the address space inside onPopulate so the shutdown path cleans them up automatically:

onPopulate: (addressSpace, exposed) => {
const namespace = addressSpace.getOwnNamespace();
const sensor = namespace.addVariable({
organizedBy: "ObjectsFolder",
browseName: "Sensor",
nodeId: "s=Sensor",
dataType: "Double",
value: { dataType: "Double", value: 0 },
});

const timerId = setInterval(() => {
sensor.setValueFromSource({ dataType: "Double", value: Math.random() });
}, 250);
addressSpace.registerShutdownTask(() => clearInterval(timerId));
},

Without registerShutdownTask, timers survive shutdown and will fire against a dead server, throwing AddressSpace has been disposed errors on the next deploy.

One server per Node-RED instance​

A Node-RED instance runs one OPC UA server. The instance is one OPC UA application, client and server, with one ApplicationUri and one certificate (see Security policies and certificates). A second server would present the same identity from another address space, which OPC UA clients and a GDS would take for the same application.

So a second bootstrapServer call under another ownerKey is refused while the first server runs:

OPC UA Server "beta" REFUSED to start: server count cap reached (1/1). A Node-RED instance is one OPC UA
application, with one ApplicationUri and one certificate, so it runs one server (owner "alpha" already runs):
shut it down first, or reuse its owner key.

What to do instead:

  • Several address-space areas? Build them in the one server: one folder or namespace per area in onPopulate (see A structured namespace layout).
  • Several applications, really? Run one Node-RED instance per application. Each gets its own ApplicationUri and certificate, and the load is spread over several processes.

Calling bootstrapServer again with the same ownerKey (the default, "default") is fine: it adopts or rebuilds that one server, as described in Server lifecycle.

ApplicationUri and ProductUri​

  • ApplicationUri identifies the instance and must be unique. It is the instance's own URI, shared with the client connections; the applicationUri field of bootstrapServer does not change it. Set OPCUA_APPLICATION_URI before the first start to choose it.
  • ProductUri names the product, the same on every installation. It defaults to urn:sterfive.com:opcua-for-node-red. If you set productUri, it must be a URI, with a scheme (urn:example.com:my-product); a plain name is rejected.

What to monitor in production​

The richest live signal comes from the server-info Function, which calls bootstrap.getServerInfo(handle.server) and returns:

  • traffic.currentChannelCount, currentSessionCount, currentSubscriptionCount — should track active clients.
  • traffic.cumulatedSessionCount minus currentSessionCount — sessions that came and went; growing fast can indicate clients reconnecting in a loop.
  • traffic.rejectedSessionCount, sessionAbortCount, sessionTimeoutCount — non-zero values indicate clients that fail to authenticate, drop mid-session, or stall.
  • capabilities.maxSupportedSessions, recommendedMaxSupportedSubscriptions — the advertised ceilings; raise them in onPopulate if you saturate.
  • certificates.rejectedFolder — if a client can't connect over a secure mode, their certificate likely landed here.

You can also read the same values via OPC UA itself under Server.ServerDiagnosticsSummary from any client.

Process-level shutdown hooks​

When Node-RED runs as a service, Ctrl+C, systemctl stop, and container shutdown all deliver SIGINT or SIGTERM. The bootstrap helper registers its own SIGINT/SIGTERM/exit handlers on first call, so process exit is covered without any per-flow code.

Do not install your own process.on("SIGINT") handler from a Function node; it would race against both Node-RED's signal handling and the helper's hooks. For an explicit programmatic teardown from a flow, see Stopping the server explicitly.

Further reading​

For more tips and examples, see the Sterfive book node-opcua by example.