# Process many values efficiently

> How-to guide: Read, write, and subscribe to thousands of values with one call: how the SDK adapts large requests to the limits of the server, and what pays off when.

> This content was machine translated and may contain errors. [Read in German](https://www.traeger.de/docs/development/opcua/client/bulk-access)

**Sample applications:**

- Client.BulkChanges ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Subscriptions/Client.BulkChanges), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Subscriptions/Client.BulkChanges)): This sample demonstrates how to subscribe to a large number of nodes and process their data changes in a separate consumer thread.
- Server.BulkChanges ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Subscriptions/Server.BulkChanges), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Subscriptions/Server.BulkChanges)): This sample demonstrates how to setup an OPC UA server that changes the values of a large number of nodes at once in a fixed interval.

Each call to `ReadNode` or `WriteNode` is a separate request to the server with its own network round trip. If you read many values one by one in a loop, you quickly lose seconds. Instead, the methods `ReadNodes`, `WriteNodes`, and `SubscribeNodes` accept any number of commands in one call. The SDK splits them into requests that the server accepts, and it reassembles the responses in the order of the commands.

## Read many values in one call

Types used: `OpcClient`, `OpcReadNode`, `OpcValue`

The results are in the same order as the commands. Evaluate the status of each value separately. An invalid node does not abort the call:

```csharp
var commands = new OpcReadNode[5000];
 
for (int index = 0; index < commands.Length; index++)
    commands[index] = new OpcReadNode("ns=2;s=Sensors/Sensor" + (index + 1));
 
IEnumerable<OpcValue> values = client.ReadNodes(commands);

```

```vbnet
Dim commands = New OpcReadNode(4999) {}

For index As Integer = 0 To commands.Length - 1
    commands(index) = New OpcReadNode("ns=2;s=Sensors/Sensor" & (index + 1))
Next

Dim values As IEnumerable(Of OpcValue) = client.ReadNodes(commands)
```

## Write many values in one call

Types used: `OpcClient`, `OpcWriteNode`, `OpcStatus`, `OpcStatusCollection`

Writing also returns one result per command. Use the index to match each result to its command:

```csharp
var commands = new OpcWriteNode[5000];
 
for (int index = 0; index < commands.Length; index++)
    commands[index] = new OpcWriteNode("ns=2;s=Setpoints/Setpoint" + (index + 1), value: 0.0);
 
OpcStatusCollection results = client.WriteNodes(commands);
 
for (int index = 0; index < results.Count; index++) {
    if (results[index].IsBad)
        Console.WriteLine("{0}: {1}", commands[index].NodeId, results[index].Description);
}

```

```vbnet
Dim commands = New OpcWriteNode(4999) {}

For index As Integer = 0 To commands.Length - 1
    commands(index) = New OpcWriteNode("ns=2;s=Setpoints/Setpoint" & (index + 1), value:=0.0)
Next

Dim results As OpcStatusCollection = client.WriteNodes(commands)

For index As Integer = 0 To results.Count - 1
    If results(index).IsBad Then
        Console.WriteLine("{0}: {1}", commands(index).NodeId, results(index).Description)
    End If
Next
```

## How the SDK splits large requests

Types used: `OpcClient`, `OpcClientServices`

Servers limit how many nodes a single request can contain and report these limits under `Server/ServerCapabilities/OperationLimits`. If a call contains more than one command, the client proceeds as follows:

1. On the first such call, it reads the limit of the server for the respective service (`MaxNodesPerRead`, `MaxNodesPerWrite`, `MaxNodesPerBrowse`, and `MaxNodesPerTranslateBrowsePathsToNodeIds`). If the server reports no limit (0), the client uses 1000.
2. If you set your own limit for the service, the smaller of the two values applies.
3. The client remembers the value and then splits all further calls into multiple requests. It reassembles the responses in the order of the commands.

This applies to `ReadNodes`, `WriteNodes`, browsing, and `TranslatePaths`. A lower limit of your own makes sense if single requests should take less time, or if the server fails on response sizes despite its reported limit:

```csharp
client.Services.ReadNodes.MaxNodesPerRead = 500;
client.Services.WriteNodes.MaxNodesPerWrite = 500;
client.Services.Browse.MaxNodesPerBrowse = 100;
```

```vbnet
client.Services.ReadNodes.MaxNodesPerRead = 500
client.Services.WriteNodes.MaxNodesPerWrite = 500
client.Services.Browse.MaxNodesPerBrowse = 100
```

The corresponding property with the prefix `Used` shows which limit the client actually uses. It is `null` as long as no call with multiple commands has taken place:

```csharp
uint? nodesPerRead = client.Services.ReadNodes.UsedMaxNodesPerRead;
```

```vbnet
Dim nodesPerRead As UInteger? = client.Services.ReadNodes.UsedMaxNodesPerRead
```

Regardless of the number of nodes, the client limits the size of the data transferred. The section [Data transfer limits](https://www.traeger.de/en/docs/development/opcua/client/configuration#data-transfer-limits) describes how to raise these limits.

## Subscribe to many nodes

Types used: `OpcClient`, `OpcSubscribeDataChange`, `OpcSubscription`, `OpcMonitoredItem`

If the values change rarely compared to the read interval, a subscription is cheaper than cyclic reading, because the server reports only changes. `SubscribeNodes` creates all MonitoredItems in one subscription. In the handler, the `sender` is the MonitoredItem. Use its NodeId to match the notification:

```csharp
var commands = new OpcSubscribeDataChange[1000];
 
for (int index = 0; index < commands.Length; index++) {
    commands[index] = new OpcSubscribeDataChange(
            "ns=2;s=Sensors/Sensor" + (index + 1),
            HandleDataChanged);
}
 
OpcSubscription subscription = client.SubscribeNodes(commands);
...
private static void HandleDataChanged(
        object sender,
        OpcDataChangeReceivedEventArgs e)
{
    OpcMonitoredItem item = (OpcMonitoredItem)sender;
    Console.WriteLine("{0}: {1}", item.NodeId, e.Item.Value);
}
```

```vbnet
Dim commands = New OpcSubscribeDataChange(999) {}

For index As Integer = 0 To commands.Length - 1
    commands(index) = New OpcSubscribeDataChange( _
            "ns=2;s=Sensors/Sensor" & (index + 1), _
            AddressOf HandleDataChanged)
Next

Dim subscription As OpcSubscription = client.SubscribeNodes(commands)
...
Private Shared Sub HandleDataChanged( _
        ByVal sender As Object, _
        ByVal e As OpcDataChangeReceivedEventArgs)
    Dim item As OpcMonitoredItem = CType(sender, OpcMonitoredItem)
    Console.WriteLine("{0}: {1}", item.NodeId, e.Item.Value)
End Sub

```

## Choose the right access method

| Situation | Recommendation |
|---|---|
| Values are needed once or rarely | `ReadNodes` with all nodes in one call |
| The same nodes are read or written cyclically | Register the nodes and call `ReadNodes` or `WriteNodes` with the registered identifiers, see [Register frequently read nodes](https://www.traeger.de/en/docs/development/opcua/client/read-write-values#register-frequently-read-nodes) |
| Values change rarely, the application reacts to changes | Subscription with all nodes through `SubscribeNodes`, see [Subscriptions](https://www.traeger.de/en/docs/development/opcua/client/subscriptions) |
| Very large single values (binary data) | Read the values as a stream, see [Large binary values as a stream](https://www.traeger.de/en/docs/development/opcua/client/node-types#large-binary-values-as-a-stream) |
| Nodes are only known by their path | Resolve them once with `TranslatePaths`, see [Find nodes by path](https://www.traeger.de/en/docs/development/opcua/client/browse#find-nodes-by-path) |
