OPC UA Browse Skipped Half Your Tags? You Dropped a Continuation Point
Browse returns success but the tag import is short. The OPC UA Part 4 v1.05 continuation point rules, MaxBrowseContinuationPoints, and how to find the cause.
No error. Just no tags
You import tags from a gateway. The log is clean, no exception. But from chamber 3 onward there are no tags at all. The earlier equipment is fine.
It looks like a permissions problem. It isn't.
Browse is not a single-shot service. In OPC UA Part 4 v1.05 §5.9.2 (Browse), if the server cannot fit every reference for a starting node into one response, it returns a continuationPoint alongside the results. That is the server telling you to fetch the rest with BrowseNext, §5.9.3. A client that never calls it finishes the import as "successful" — quietly, and partial.
Know exactly which value is truncating you
Confuse the names here and you spend the afternoon in the wrong settings dialog. These are three different things.
requestedMaxReferencesPerNode — a Browse request parameter. The maximum number of references to return per starting node. Part 4 §5.9.2 defines the value 0 as "the Client is imposing no limitation." The server can still truncate at its own limit, so 0 is not a promise of everything.
MaxNodesPerBrowse — an optional UInt32 property of OperationLimitsType, Part 5 v1.05 §6.3.11. It has nothing to do with reference counts. It caps the size of the nodesToBrowse array in a Browse call and the continuationPoints array in a BrowseNext call. It bites a client that pushes 500 folders into one request; it is not why a single folder came back short.
MaxBrowseContinuationPoints — a mandatory UInt16 property of ServerCapabilitiesType, Part 5 v1.05 §6.3.2. The number of Browse continuation points the server supports in parallel per session. Zero means the server imposes no restriction.
The third one causes the field failures. Most people only ever look at the first.
Continuation points do not expire on a timer
If you take one thing from this article, take this.
Part 4 v1.05 §7.9 states that continuation points remain active until the client retrieves the remaining results, the client releases the continuation point, or the session is closed. There is no validity period.
The next sentence in that clause is the real cause. A server shall automatically free continuation points from prior requests in a session if they are needed to process a new request from that same session. And if a client then tries to use a continuation point that has been released, the server returns Bad_ContinuationPointInvalid.
So this client breaks:
- Browse folder A. It truncates and returns a continuation point. Queue it.
- Browse folder B. Also truncates. Queue that one too.
- Browse C, D, E … walk the whole tree breadth-first.
- Drain the queue afterwards, calling
BrowseNexton each saved point. - The server's
MaxBrowseContinuationPointswas 2. A and B were evicted long ago.Bad_ContinuationPointInvalid.
Nothing "timed out." Our own new Browse calls pushed our own continuation points out. It fails identically on a three-hour import and a three-second one. It is also why the same client often works locally and fails over a VPN — not the latency, but a different queue depth.
The fix is ordering, not configuration. Drain one folder completely before moving to the next. Call BrowseNext until no continuation point comes back, and only then recurse. Depth-first keeps exactly one continuation point open at a time.
If you abandon an import partway, call BrowseNext with releaseContinuationPoints = TRUE to clean up. It returns no data and frees the server-side state. The session close would free them anyway, but a SCADA client that holds its session for weeks leaves the next import with nowhere to allocate.
An empty result can still carry a continuation point
Part 4 §5.9.2 has an exception that is easy to read past. If processing all the nodes would take longer than the client's timeout hint, the server may return zero results with a continuation point for the affected nodes rather than let the request expire.
A client that treats references.length == 0 as "this folder is empty" records a perfectly populated equipment folder as empty. Completion is decided by the absence of a continuation point, never by the count.
Bad_NoContinuationPoints is misread just as often. It is not what you get when resuming work. §7.9 is explicit that servers shall never return it when continuing a previously halted operation. It applies to the remaining operations in a multi-node request after the server has used up its continuation points. Seeing it means you are packing too many nodes into one Browse.
Do not crawl the whole tree
A full recursive browse of a large address space is expensive on both ends. It also drags in type definitions, diagnostic nodes, and methods that no HMI will ever display, and clutters the tag list with them.
Narrow the entry point. Start at the equipment folder the vendor documents, not the Objects root. Filter to Variable nodes with nodeClassMask when importing process tags. Take method, type, and diagnostic branches separately, when you actually need them. Keep historical, recipe, and event model branches out of ordinary tag import.
This is not about hiding data. It is about a procedure you can run at FAT and again at SAT without knocking over the server, and get the same result both times.
Do not key tags on DisplayName
Large address spaces repeat names. Status, Mode, Start, Fault, Temperature recur on every asset.
Store, per imported node: namespace URI plus the namespace index used at import time, NodeId, BrowseName, DisplayName, the full browse path, data type and engineering unit, source server and endpoint.
Store the namespace URI in particular. A namespace index can shift after a server restart or a firmware update, and a tag saved with only the index then points quietly at a different node. With the URI you can re-read the NamespaceArray and recompute the index.
DisplayName is fine on a screen. It is a weak key for tag identity.
Count things before you sign off
Before accepting tag discovery, pick a few dense folders and compare. Do not trust the SCADA client's own report on its own.
- Browse the target equipment folder with the vendor tool.
- Count child nodes in several dense subfolders.
- Import the same branch into the SCADA client.
- Compare per-folder counts and spot-check NodeIds.
- Read
Server/ServerCapabilities/MaxBrowseContinuationPointsand write the value down. - If namespace indexes are dynamic, repeat the import after a server restart.
Step 5 takes thirty seconds and settles an entire future argument. If the value comes back as 1 or 2, do not accept the import until the client browses depth-first.
What to leave for operations
Operations does not need the browse trace. They need to know whether a tag refresh is safe to rerun and what a correct result looks like: the tested starting folder, expected node counts for the major equipment branches, branches excluded on purpose, the server limits you recorded, the client version that worked, and the procedure for refreshing tags after a PLC or gateway update.
The expensive failure here is the silent one. No alarm, no exception, just missing tags. If nobody counts, nobody finds out.