← Articles
OPC UA/9 min read/ views

OPC UA Connects Fine but Every Tag Reads Bad: the NodeId Moved

After a PLC download the session opens and every item returns Bad_NodeIdUnknown. How namespace indexes and client caches shift, and what to check first.

OPC UASCADATagsTroubleshootingProject Notes

Sunday morning, the controls engineer pushed a PLC download that reorganised a couple of data blocks. Monday the mixer line shows six dashed-out faceplates. The SCADA server says the OPC UA connection is good. Certificates are trusted, the session is up, the browse tree still has Line1/Mixer right where it always was. But the historian has flatlined since 04:00 and the operators are running the line off the local panel.

This is the most common OPC UA failure I get called about, and it is almost never the network. The session is fine because sessions don't care about tags. The failure is one layer down, at CreateMonitoredItems, and the status code sitting on each dead item usually says Bad_NodeIdUnknown — the server is telling you plainly that the address you asked for no longer exists.

Read the per-item status before you touch anything

The reason this eats an afternoon is that people debug it as a connectivity problem. Split the layers and the answer arrives in about ten minutes. In order:

  1. Endpoint reachable, certificate trusted → if this fails you get a channel error, not bad tags.
  2. Session created → you already know this works if the client shows connected.
  3. Read Server_NamespaceArray (NodeId i=2255) → this is the namespace table, and it's the single most useful thing to capture.
  4. Browse the equipment folder → does Line1/Mixer/Speed still exist, and under what NodeId now?
  5. CreateMonitoredItemsread the StatusCode on each item, not just the subscription.

Step 5 is where the diagnosis lives. Bad_NodeIdUnknown means the syntax parsed but nothing in the address space matches — a moved or regenerated identifier. Bad_NodeIdInvalid means the NodeId itself is malformed, which in practice means somebody hand-typed it or a script concatenated it wrong. Two different problems, two different fixes, and most clients bury both behind a grey "bad quality" square on the HMI.

The namespace index is not an identifier

A NodeId in OPC UA Part 3 is two things: a namespace index and an identifier, where the identifier is one of Numeric, String, GUID, or Opaque. When you write ns=4;s=Line1.Mixer.Speed into a tag config, you have written down a position in a table plus a name.

Index 0 is always the OPC UA namespace (http://opcfoundation.org/UA/). Index 1 is conventionally the server's own application URI. Everything from index 2 upward is whatever order that particular server happened to build its namespaces in on that particular startup. Nothing in the spec promises index 4 means the same thing tomorrow. Add a companion-spec namespace to a gateway, or let a firmware update change the load order of two device models, and every ns= in your tag database is now pointing one slot to the left.

The stable half is the URI. So the rule I hold engineers to on handover: the interface document records URIs, never indexes. Indexes get recorded as "observed during FAT, expected to change".

Some servers also expose NamespaceMetadataType objects with NamespaceVersion and NamespacePublicationDate. If yours does, grab those in the pre-cutover export — a changed publication date is a much faster answer than diffing two thousand browse names.

Three ways the identifier moves, and they look identical from the HMI

Symbol-derived string NodeIds. Most PLC-embedded servers build the identifier from the program's own symbol path. A Siemens S7-1500 exposes something like ns=3;s="MixerData"."Speed"; a KEPServerEX-style gateway exposes ns=2;s=Channel1.Device1.Speed. Rename the data block, rename the channel, move a variable into a nested struct — the NodeId changes, because it was never independent of the program. This is the download-on-Sunday case.

Regenerated numeric NodeIds. Servers that assign numeric identifiers when they build the address space will happily assign different ones after a model rebuild or a project re-import. ns=2;i=6104 is not wrong; it just now belongs to a different variable. This is the dangerous one, because a numeric NodeId that has been reassigned rather than deleted will read good and return the wrong value. No bad quality, no alarm, just a flow reading that doesn't match the field.

Import instead of update. Importing a project into a fresh instance rather than updating the existing one regenerates the whole address space. Same tags, same folders, new identity for everything.

The field symptoms overlap enough to be useless as a discriminator: stale values, bad quality, one skid dark while the identical one next to it works because only one namespace got rebuilt. Look at the NodeIds, not the symptoms.

Pin the interface if the server lets you

Where the server supports it, decouple what you expose from how the program is written. S7-1500 firmware supports defining a server interface so the browse names and NodeIds you publish stay put while the internal DB structure gets refactored underneath. Most OPC UA gateways have an equivalent aliasing or exposed-tag layer. Use it for anything SCADA-facing, then treat that interface as change-controlled — the same way you would a Modbus register map.

What makes a NodeId survivable, in rough priority order:

PropertyWhy it matters
Independent of compile/downloadOtherwise every program change is a SCADA change
No temporary or test project namesns=2;s=FAT_Rig.Mixer.Speed will follow you to site
Unique across repeated skidsAmbiguity here means writes land on the wrong machine
Unaffected by display-label editsA translator changing a description shouldn't break the historian
Diffable as textYou need diff old.csv new.csv on cutover night

A readable string NodeId isn't automatically better than a numeric one — string identifiers are longer on the wire and some servers are slower at resolving them. It's better because you can review it, and that only matters during changes. If your server only produces generated identifiers, then the thing to verify is whether its backup/restore actually preserves them. Test that before you need it.

Registered NodeIds are session-scoped — don't cache them

This one bites custom clients and scripts specifically. RegisterNodes exists so a client can hand the server a list of NodeIds it will read repeatedly and get back handles the server can resolve faster. Those handles are valid only within the session that created them, and Part 4 is explicit about it. A client that persists them to disk, or reuses them after a reconnect, gets Bad_NodeIdUnknown on tags that were fine two minutes ago — and looks exactly like a server-side address change.

If you're troubleshooting an intermittent version of this bug that clears on service restart, check for registered-node caching before you go blaming the PLC guy.

Browse paths as recovery, not as insurance

TranslateBrowsePathsToNodeIds lets a client re-resolve a path like /Line1/Mixer/Speed into whatever NodeId currently backs it. Clients that do this on failure recover from a lot of these events on their own, and it's a genuinely good feature to buy for.

It is not a substitute for interface control, because browse names move for the same reasons NodeIds do. On repeated equipment the path can be ambiguous, and some vendors localise folder names, so the path your client stored in English may not resolve on a Korean-locale server.

The questions worth answering during FAT, while there's still time to change clients:

  • Does it rebind by namespace URI, or does it store the index?
  • Does it rebrowse automatically on Bad_NodeIdUnknown, or wait for a human?
  • Does clearing the address cache require a service restart? (Usually yes. Plan the outage.)
  • Does the subscription keep its sampling interval and deadband after rebinding?
  • Does it log old → new NodeId pairs? Without that log you cannot audit what it silently remapped.

While you're there, check whether the server raises GeneralModelChangeEventType when its address space changes. Very few do, and fewer clients subscribe to it, which is why this whole class of failure is discovered by operators rather than by software.

Cutover: the export you'll wish you had

Before touching a server that feeds SCADA, capture five things. It takes twenty minutes and it converts a night of guessing into a diff:

  • The namespace array, with URIs, plus NamespaceVersion if exposed.
  • The client's full tag list with NodeIds, as text.
  • A browse export of the critical equipment folders.
  • A marked-up list of which tags are writes, which are alarm sources, and which the historian collects.
  • Server product and firmware/build version.

After the change, don't sweep the whole HMI. Per affected namespace, test one analog, one digital status, one alarm source, one command write, and one historian-critical value. That's five points that exercise five different code paths in your client.

And test the last instance of any repeated equipment, not Line1. Off-by-one errors in templates, arrays, and generated instances almost always show up at the end of the range.

Where the partial fix leaves you exposed

The fix that gets applied under pressure is remapping the display tags, because that's what the operator is complaining about. Then the line runs, everyone goes home, and:

  • Alarm definitions still reference the old NodeIds, so those alarms simply never fire again.
  • The historian collector has its own tag database and was never in scope for the remap.
  • The standby SCADA node still holds the old cache and will fail over into a dead screen.
  • A reporting script has ns=2 hard-coded in a string literal.
  • Writes are landing on the wrong instance of a repeated skid — the most expensive one on this list, and the only one with no symptom at all.

Alarms and historian are the ones I'd go back and check tonight rather than tomorrow. A display that's obviously broken gets fixed; an alarm that quietly stopped existing doesn't, and you find out about it during the event it was supposed to catch.