# Long-running clients

> How-to guide: Clients for continuous operation: how the SDK detects connection breaks, restores sessions and subscriptions, and what the application handles itself.

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

**Sample applications:**

- Client.DynamicNamespaces ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Scenarios/Client.DynamicNamespaces), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Scenarios/Client.DynamicNamespaces)): This use case demonstrates how a client addresses nodes by namespace URI instead of namespace index and renews its subscriptions after each reconnect, so it copes with namespaces that change on the server.
- Server.DynamicNamespaces ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Scenarios/Server.DynamicNamespaces), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Scenarios/Server.DynamicNamespaces)): This use case demonstrates how to provide each namespace through its own node manager and how to restart the server with a different selection of node managers.

A client that stays connected to a plant permanently must survive network outages, server restarts, and maintenance windows. The OPC UA .NET SDK handles most of this: it detects the break, reconnects on its own, and restores subscriptions. This page summarizes what happens automatically and which tasks remain with the application. The section [Connection monitoring](https://www.traeger.de/en/docs/development/opcua/client/configuration#connection-monitoring) of the configuration page describes the individual settings.

## What the SDK handles on a connection break

Types used: `OpcClient`, `OpcClientState`, `OpcClientKeepAlive`

As long as `UseBreakDetection` is enabled (default), the client checks the connection with KeepAlive requests at intervals of `KeepAlive.Interval` (default 5000 ms). If a KeepAlive request fails, or a service call reports a lost connection (for example with `BadConnectionClosed` or `BadSessionIdInvalid`), the following sequence runs at once:

| Step | What the SDK does | State and event |
|---|---|---|
| 1 | The client detects the break. | `Reconnecting`, event `Reconnecting` |
| 2 | At intervals of `ReconnectInterval` (default 10000 ms), the client tries to restore the existing session, for at most `MaxReconnectDuration` (default 0, unlimited). | remains `Reconnecting` |
| 3a | The session can be restored: the client requests the pending notifications of its subscriptions again. | `Reconnected`, event `Reconnected` |
| 3b | The session has expired on the server: the client creates a new session and re-creates all subscriptions with their MonitoredItems. | `Connected`, event `Connected` |
| 4 | In both cases, the client reloads the namespace table of the server. If `UseDynamic` or type resolution is active, it also reloads the type system. | |

For the application, this means that subscriptions, MonitoredItems, and their event handlers are preserved. You do not need to subscribe again after a connection break.

## Connection events at a glance

Types used: `OpcClient`, `OpcClientState`

The client determines whether the connection to the server exists through its KeepAlive logic and through the results of its service calls. If a call such as `ReadNode` as a whole fails with a status that indicates a lost connection (for example `BadConnectionClosed` or `BadSessionIdInvalid`), the client changes its state at once and does not wait for the next KeepAlive. Other errors of single calls and a bad status of single values in the result do not change the state. The client reports every state change through the following events of the `OpcClient` class:

| Event | When the client raises it | Meaning |
|---|---|---|
| `Connecting`, `Connected` | when you call `Connect`, and after a connection loss if the client created a new session | `Connecting` reports the start of a connection attempt, `Connected` its successful completion. |
| `Reconnecting`, `Reconnected` | after a connection loss, if `UseBreakDetection` is enabled and `ReconnectInterval` is greater than 0 | After `Reconnecting`, the client tries to restore the connection at intervals of `ReconnectInterval`. `Reconnected` reports that it was able to keep using the existing session. If it had to create a new session, it raises `Connected` instead. |
| `Disconnecting`, `Disconnected` | when you call `Disconnect`, or implicitly `Dispose`; also after a connection loss if `ReconnectInterval` is 0 or `MaxReconnectDuration` has elapsed (then only `Disconnected`, without a preceding `Disconnecting`) | The connection was closed manually, or the client detected a connection loss that it does not resolve itself. |
| `BreakDetected` | after a connection loss, if `UseBreakDetection` is enabled and `ReconnectInterval` is 0 or `MaxReconnectDuration` has elapsed, following `Disconnected` | Signal to restore the connection yourself (see [Reconnect yourself](https://www.traeger.de/en/docs/development/opcua/client/long-running-clients#reconnect-yourself)). |
| `StateChanged` | on every change of the `State` property, each time before the events above | general monitoring of all state changes (see [Log state changes](https://www.traeger.de/en/docs/development/opcua/client/long-running-clients#log-state-changes)) |

If `UseBreakDetection` is disabled, the client does not restore the connection automatically. Your application must then handle such situations itself.

## Connect at startup

Types used: `OpcClient`, `OpcException`

Automatic reconnection only takes effect once a connection has existed. If the server is not reachable when the application starts, `Connect` throws an `OpcException`, and the client returns to its previous state. A long-running client therefore retries the connection itself:

```csharp
var client = new OpcClient("opc.tcp://localhost:4840/");
client.ReconnectInterval = 5000;
 
while (true) {
    try {
        client.Connect();
        break;
    }
    catch (OpcException ex) {
        Console.WriteLine("Connect failed: {0}", ex.Code);
        Thread.Sleep(TimeSpan.FromSeconds(5));
    }
}
```

```vbnet
Dim client = New OpcClient("opc.tcp://localhost:4840/")
client.ReconnectInterval = 5000

Do
    Try
        client.Connect()
        Exit Do
    Catch ex As OpcException
        Console.WriteLine("Connect failed: {0}", ex.Code)
        Thread.Sleep(TimeSpan.FromSeconds(5))
    End Try
Loop

```

## Create subscriptions once

Types used: `OpcClient`, `OpcSubscription`, `OpcDataChangeReceivedEventArgs`

Create subscriptions once, after the first successful `Connect`. The SDK restores them itself for a new session, so subscribing again in the handler of the `Connected` event would duplicate every subscription after a connection break:

```csharp
// Subscribe once after Connect: the client recreates the subscription for a new session.
OpcSubscription subscription = client.SubscribeDataChange(
        "ns=2;s=Machine/Speed",
        HandleDataChanged);
...
private static void HandleDataChanged(
        object sender,
        OpcDataChangeReceivedEventArgs e)
{
    Console.WriteLine("Speed: {0}", e.Item.Value);
}
```

```vbnet
'Subscribe once after Connect: the client recreates the subscription for a new session.
Dim subscription As OpcSubscription = client.SubscribeDataChange( _
        "ns=2;s=Machine/Speed", _
        AddressOf HandleDataChanged)
...
Private Shared Sub HandleDataChanged( _
        ByVal sender As Object, _
        ByVal e As OpcDataChangeReceivedEventArgs)
    Console.WriteLine("Speed: {0}", e.Item.Value)
End Sub

```

If notifications arrive that cannot be decoded, the SDK reports this through the `NotificationFailed` event, see [Handle notifications](https://www.traeger.de/en/docs/development/opcua/client/subscriptions#handle-notifications).

## Log state changes

Types used: `OpcClient`, `OpcClientState`, `OpcClientStateChangedEventArgs`

For operation and troubleshooting, it helps to log every state change. The `StateChanged` event occurs before the corresponding individual event and states the old and the new state. A change from `Reconnecting` to `Connected` indicates that the client created a new session:

```csharp
client.StateChanged += HandleStateChanged;
...
private static void HandleStateChanged(
        object sender,
        OpcClientStateChangedEventArgs e)
{
    Console.WriteLine("{0} -> {1}", e.OldState, e.NewState);
 
    if (e.OldState == OpcClientState.Reconnecting && e.NewState == OpcClientState.Connected)
        Console.WriteLine("New session, the subscriptions have been recreated.");
}
```

```vbnet
AddHandler client.StateChanged, AddressOf HandleStateChanged
...
Private Shared Sub HandleStateChanged( _
        ByVal sender As Object, _
        ByVal e As OpcClientStateChangedEventArgs)
    Console.WriteLine("{0} -> {1}", e.OldState, e.NewState)

    If e.OldState = OpcClientState.Reconnecting AndAlso e.NewState = OpcClientState.Connected Then
        Console.WriteLine("New session, the subscriptions have been recreated.")
    End If
End Sub
```

## What to do after a new session

Types used: `OpcClient`, `OpcNodeIdCollection`

Everything the server assigns to a single session is no longer valid after a new session. This mainly affects registered nodes (see [Register frequently read nodes](https://www.traeger.de/en/docs/development/opcua/client/read-write-values#register-frequently-read-nodes)). The identifiers returned by `RegisterNodes` are only valid in the session in which the client registered them. Therefore, register the nodes in the handler of the `Connected` event. The event occurs on the first connection and after every new session:

```csharp
client.Connected += HandleConnected;
...
private OpcNodeId[] nodeIds = { "ns=2;s=Machine/Speed", "ns=2;s=Machine/Temperature" };
private OpcNodeIdCollection registeredNodeIds;
 
private void HandleConnected(object sender, EventArgs e)
{
    // Registered node ids are only valid in the session they have been registered in.
    this.registeredNodeIds = ((OpcClient)sender).RegisterNodes(this.nodeIds);
}
```

```vbnet
AddHandler client.Connected, AddressOf HandleConnected
...
Private nodeIds As OpcNodeId() = {"ns=2;s=Machine/Speed", "ns=2;s=Machine/Temperature"}
Private registeredNodeIds As OpcNodeIdCollection

Private Sub HandleConnected(ByVal sender As Object, ByVal e As EventArgs)
    'Registered node ids are only valid in the session they have been registered in.
    Me.registeredNodeIds = CType(sender, OpcClient).RegisterNodes(Me.nodeIds)
End Sub

```

For nodes in other namespaces, also use the namespace URI instead of a hard-coded index, see [Address nodes by their namespace URI](https://www.traeger.de/en/docs/development/opcua/client/read-write-values#address-nodes-by-their-namespace-uri). After a server restart, the order of its namespaces can change.

## Reconnect yourself

Types used: `OpcClient`

If `ReconnectInterval` is 0, the client does not reconnect on its own. It changes to the `Disconnected` state and raises the `BreakDetected` event. The application then decides itself when and how to re-establish the connection, for example after a delay of its own:

```csharp
client.ReconnectInterval = 0;
client.BreakDetected += HandleBreakDetected;
...
private static void HandleBreakDetected(object sender, EventArgs e)
{
    OpcClient client = (OpcClient)sender;
 
    // Decide on your own when and how to connect again.
    Task.Run(() => {
        Thread.Sleep(TimeSpan.FromSeconds(10));
        client.Connect();
    });
}
```

```vbnet
client.ReconnectInterval = 0
AddHandler client.BreakDetected, AddressOf HandleBreakDetected
...
Private Shared Sub HandleBreakDetected(ByVal sender As Object, ByVal e As EventArgs)
    Dim client As OpcClient = CType(sender, OpcClient)

    'Decide on your own when and how to connect again.
    Task.Run( _
            Sub()
                Thread.Sleep(TimeSpan.FromSeconds(10))
                client.Connect()
            End Sub)
End Sub
```

## Access from multiple threads

Types used: `OpcClient`, `OpcSubscription`, `OpcTaskSchedule`

You can use an instance of `OpcClient` from multiple threads. The client runs its service calls one after another: each call locks the client until the server's response arrives or the timeout expires. This applies to the services Read, Write, Browse, BrowseNext, Call, and TranslateBrowsePathsToNodeIds, to access to historical data, to adding and deleting nodes and references, and to registering nodes. A slow call therefore holds up all other calls of the same client. `Connect` and `Disconnect` use the same lock.

The `BrowseNodes` method, in contrast, runs without this lock. It reads through the node cache of the stack, and this cache does not lock concurrent access. We therefore recommend that you do not call `BrowseNodes` of one client from multiple threads at the same time.

Subscriptions work in parallel to the service calls. By default, the SDK dispatches their notifications on threads of the .NET thread pool; the [SDK internals](https://www.traeger.de/en/docs/development/opcua/internals) page describes the details.

While the client re-establishes the connection, new calls fail immediately with the status `BadServerNotConnected`. Calls that are already waiting for a response at that time end with `BadConnectionClosed` or `BadRequestInterrupted`. Handle these cases like other failed calls (see [Handle errors](https://www.traeger.de/en/docs/development/opcua/client/error-handling)).

## Checklist

| Topic | Recommendation |
|---|---|
| Startup | Retry `Connect` in a loop until the server is reachable. |
| Break detection | Keep `UseBreakDetection` enabled, adjust `KeepAlive.Interval` and `ReconnectInterval` to the plant, and limit `MaxReconnectDuration` if needed. |
| Subscriptions | Create once after the first `Connect`, not in the handler of `Connected`. |
| Registered nodes | Register in the handler of `Connected`. |
| Threads | Service calls of one client run one after another; do not call `BrowseNodes` of one client from multiple threads at the same time. |
| NodeIds | Address namespaces through the URI (`nsu=`). |
| Faulty notifications | Handle `NotificationFailed`. |
| Errors of single operations | Evaluate the status, see [Handle errors](https://www.traeger.de/en/docs/development/opcua/client/error-handling). |
