Which OPC UA Name Should Your SCADA Store: NodeId, BrowseName, or DisplayName?
NodeIds regenerate, browse paths move, DisplayName is a label. Picking the reference your HMI and historian store so firmware doesn't blank a screen.
A packaged filler machine got a firmware update over a weekend. Monday morning, about forty tags on the line overview were showing quality Bad with Bad_NodeIdUnknown, and the trend pens for the same points went flat. The PLC was fine. The OPC UA server was up and browsable. What changed was that the vendor's server assigns numeric NodeIds in build order, and the firmware update added two variables near the top of the model, shifting everything below it by two.
The HMI had stored ns=4;i=6021 and friends. Those numbers no longer meant what they used to.
That failure is not exotic. It comes from picking the wrong identifier to store at import time, which is a decision most people make by accident, inside a tag browser wizard, in about four seconds.
The four names, and what IEC 62541-3 actually promises
Part 3 of the OPC UA spec (IEC 62541-3, Address Space Model) gives every Node four attributes people confuse:
| Attribute | Type | What the spec guarantees |
|---|---|---|
| NodeId | NodeId (numeric / string / GUID / opaque) | Unique within a server. Nothing about surviving a restart or rebuild. |
| BrowseName | QualifiedName (nsIndex + name) | Not localized. Unique among siblings under one parent, by convention — not enforced. |
| DisplayName | LocalizedText | Nothing. It's a label. It has a locale. It is expected to change. |
| Description | LocalizedText | Nothing. Documentation text. |
Read that NodeId row again, because it is where most of this goes wrong. The spec says unique, not stable. Persistence across a server restart or a project rebuild is a server implementation property, and vendors treat it very differently. A Siemens-style server that derives string NodeIds from PLC symbols behaves nothing like a gateway that numbers nodes from a SQLite row id.
DisplayName is never the key
I'll be blunt about this one because it costs the most to undo. DisplayName is a LocalizedText. If a client requests locale ko-KR and the server has a Korean translation, you get different bytes back for the same node. Building identity on it means your identity depends on which locale the importing engineer's client happened to request.
So: never use DisplayName for historian point identity, alarm source identity, MES equipment mapping, or HMI import matching. Use it on screens and reports, which is what it exists for.
The test I apply during design review: rename "Pump 1 Run" to "P-101 Running" in the server and reload. If any technical mapping breaks or any historian series starts a new point, the design is wrong and you fix it now, not after two years of trend history splits in half.
Prove NodeId stability instead of hoping for it
If you're going to store NodeIds — and often you should, because they're the fastest reference and the only one Read/Write/Subscribe actually takes — spend an afternoon proving they hold. Four checks, in order of how often they catch something:
- Rebuild and diff. Add one dummy variable to the PLC or gateway project, rebuild, re-export the address space, diff against the previous export. This is the check that caught the filler machine. If unrelated NodeIds moved, numeric NodeIds are off the table.
- Restart the server. Some in-memory servers renumber on every start.
- Deploy to a second runtime. Import the same project on a spare gateway. If NodeIds differ between the two machines, you cannot promote a tested project from dev to production without re-importing tags.
- Ask the vendor directly whether NodeIds are a contract or an implementation detail. Get it in writing. "They've never changed for us" is not an answer.
Roughly: string NodeIds derived from symbol names survive rebuilds; numeric NodeIds assigned by build order usually don't; GUIDs are a coin flip that depends entirely on whether the server persists its node table.
Store the namespace URI, not the index
This is the cheap mistake, and it's the one I still see in mature projects.
ns=3;i=1207 is meaningless on its own. The 3 is an index into the server's NamespaceArray — the string array on the Server object, NodeId i=2255. That array is built by the server at startup, and its order can shift when a vendor adds a namespace, when you enable an optional companion-spec model, or simply because dev and production servers were configured in a different sequence.
Store the URI (http://vendor.com/PackagedLine/) alongside the tag. At connect, read i=2255, find your URI, and use whatever index it landed on. Any client worth using does this for you — but check, because plenty of cheap drivers store the raw index and silently read the wrong namespace's node.
If you take one thing from this article, take this one. It costs a column in a spreadsheet and prevents an entire class of "the values are wrong but not obviously wrong" incidents.
Browse paths: durable against rebuilds, fragile against reorganization
The alternative to storing a NodeId is storing a path — Area1/Line2/MotorA/Speed — and resolving it with TranslateBrowsePathsToNodeIds (Part 4, Services) at connect time. The tradeoff is clean: paths survive NodeId regeneration, NodeIds survive hierarchy changes. Pick based on which your server does more often.
Paths break when:
- Equipment gets moved to a different folder during a project cleanup.
- Two nodes share a BrowseName under different parents and the import strips the parent context.
- A vendor migrates from flat tags to an object-with-variables model — every path gains a level.
- The client resolves paths only at startup and swallows the per-path
Bad_NoMatch.
That last one is the real danger. TranslateBrowsePathsToNodeIds returns a result per path, so a partial failure is normal and easy to ignore. A client that logs "connected" and leaves nine tags unresolved is worse than one that fails loudly. Before handover, force a failure: rename one node in the server and confirm the HMI actually tells you.
Put an alias layer between the two
Screens, alarms, and scripts should reference a site tag name — Line01_P101_RunFb — and exactly one place in the project should hold the OPC UA mapping. This is not architecture astronautics; it's the difference between a firmware update costing you one import sheet or costing you a sweep of every screen, alarm, and script that hardcoded a NodeId.
It pays for itself the first time any of these happen, and at least one of them always does:
- The vendor changes the model in an update.
- A historian point must keep its identity while the source mapping moves.
- A bilingual HMI needs different operator labels on one technical signal.
- Dev and production servers land on different namespace indexes.
Keep the alias boring. Don't encode the OPC UA address into the alias name — if you name a tag ns4_i6021_Speed, you've just moved the fragility somewhere harder to grep.
The import sheet
More columns than the wizard gives you, and the last one matters more than it looks:
Internal SCADA tag name · equipment/area · data type and EU · endpoint URL · namespace URI · NodeId type and value (if used) · browse path (if used) · leaf BrowseName · DisplayName · read/write direction · alarm or historian use · last verified date.
The verified date is what tells the next engineer whether this sheet is documentation or archaeology.
Two failures worth recognizing on sight
Values are correct in the browser, blank in the HMI. The browser is resolving the current address space interactively; the runtime is using a NodeId stored at import time. Inspect what the driver tag actually holds, not what the browser shows you now. These are different pieces of data and the browser will happily lie to you by being right.
Historian points duplicate after a label cleanup. Someone tidied DisplayNames, the historian keyed identity on the label, and now you have Pump 1 Run ending Tuesday and P-101 Running starting Wednesday. Merging those series afterward is manual work nobody budgets for. Label changes must be metadata updates against a stable point key.
Before you sign the handover
Take one sample tag per equipment type and check that a server restart leaves the mapping intact, that a DisplayName edit changes nothing technical, that the stored reference matches the sheet, and that the client's namespace URI resolves to the model you expect.
Then do the one test everyone skips: rename a node in the server and confirm the HMI reports it. A mapping you've never seen fail is a mapping you don't know the failure mode of.