Session Recovery & Resilience
OpcSharp includes built-in resilience features that automatically handle network interruptions, session timeouts, and token expiration. This page shows how to subscribe to recovery events and configure resilience behavior for production applications.
Overview
The resilience system has three cooperating handlers:
| Handler | Purpose | Default |
|---|---|---|
| KeepAlive | Detects connection loss via periodic server reads | Every 5 seconds |
| Token Renewal | Renews the SecureChannel token before expiry | At 75% of token lifetime |
| Session Reconnect | Restores the session after a detected failure | 1s initial, 30s max backoff |
When a failure is detected, the reconnect handler runs a two-phase recovery:
- Reactivate (fast path) — reuse the existing session on a new transport/channel
- Recreate (fallback) — create a new session and transfer subscriptions
Client State Machine
Track the client lifecycle through the StateChanged event:
Disconnected → Connecting → Connected → Reconnecting → Connected
↘ Faultedclient.StateChanged += (sender, e) =>
{
Console.WriteLine($"State: {e.OldState} → {e.NewState}");
if (e.NewState == ClientState.Reconnecting)
Console.WriteLine("Connection lost — automatic recovery in progress...");
if (e.NewState == ClientState.Faulted)
Console.WriteLine("Unrecoverable error — consider restarting the connection.");
};ClientStateChangedEventArgs properties:
| Property | Type | Description |
|---|---|---|
OldState | ClientState | Previous state |
NewState | ClientState | Current state |
Keepalive Monitoring
The keepalive handler periodically reads the server’s status node (i=2259). If the read fails, the reconnect handler is triggered automatically.
client.KeepAlive += (sender, e) =>
{
if (e.Error != null)
{
Console.WriteLine($"Keepalive failed: {e.Error.Message}");
// The reconnect handler will be triggered automatically.
// Set CancelKeepAlive = true to stop keepalive monitoring entirely:
// e.CancelKeepAlive = true;
return;
}
Console.WriteLine($"Server state: {e.ServerState} at {e.Timestamp:HH:mm:ss}");
if (e.ServerState == ServerState.Shutdown)
{
Console.WriteLine("Server shutting down — pausing operations.");
e.CancelKeepAlive = true;
}
};KeepAliveEventArgs properties:
| Property | Type | Description |
|---|---|---|
ServerState | ServerState? | Current server state (null on error) |
Error | Exception? | The exception if the keepalive read failed |
Timestamp | DateTime | UTC time of the keepalive check |
CancelKeepAlive | bool | Set to true to stop keepalive monitoring |
Reconnection Events
Before Each Attempt — Reconnecting
Fires before each reconnect attempt. Use this to log attempts, update UI, or cancel recovery.
client.Reconnecting += (sender, e) =>
{
Console.WriteLine(
$"Reconnect attempt #{e.AttemptNumber} — phase: {e.Phase}");
if (e.Error != null)
Console.WriteLine($" Previous error: {e.Error.Message}");
// Cancel reconnection after too many attempts
if (e.AttemptNumber > 10)
{
Console.WriteLine("Too many attempts — giving up.");
e.Cancel = true;
}
};After Success — Reconnected
Fires once the session is restored. Use this to re-fetch state or resume application logic.
client.Reconnected += (sender, e) =>
{
Console.WriteLine(
$"Reconnected after {e.AttemptNumber} attempt(s) via {e.Phase}");
if (e.Phase == ReconnectPhase.Recreating)
{
// A new session was created — subscriptions were transferred
// or recreated automatically. You may want to verify state.
Console.WriteLine("New session created — verifying monitored items...");
}
};SessionReconnectEventArgs properties:
| Property | Type | Description |
|---|---|---|
Phase | ReconnectPhase | Reactivating (reuse session) or Recreating (new session) |
AttemptNumber | int | Which retry attempt this is (1-based) |
Error | Exception? | Error from the previous failed attempt |
Cancel | bool | Set to true to stop reconnection (client enters Faulted state) |
Reconnect Phases
| Phase | What Happens | When Used |
|---|---|---|
Reactivating | New transport + channel, then ActivateSession with existing session ID | First attempt (fast path) |
Recreating | New transport + channel + session, then TransferSubscriptions | When the server no longer recognizes the old session |
Complete Example
Wire up all resilience events in a production application:
using OpcSharp.Client;
using OpcSharp.Types;
await using var client = new OpcSharpClientBuilder()
.WithEndpoint("opc.tcp://localhost:4840")
.WithSessionName("ResilientApp")
.WithKeepAliveInterval(TimeSpan.FromSeconds(5))
.WithReconnectPeriod(
initial: TimeSpan.FromSeconds(1),
max: TimeSpan.FromSeconds(30))
.WithTokenLifetime(TimeSpan.FromHours(1))
.WithAutoAcceptUntrustedCertificates()
.Build();
// Track state transitions
client.StateChanged += (sender, e) =>
{
Console.WriteLine($"[State] {e.OldState} → {e.NewState}");
};
// Monitor server health
client.KeepAlive += (sender, e) =>
{
if (e.Error != null)
Console.WriteLine($"[KeepAlive] Error: {e.Error.Message}");
};
// Log reconnect attempts
client.Reconnecting += (sender, e) =>
{
Console.WriteLine(
$"[Reconnecting] Attempt #{e.AttemptNumber}, phase: {e.Phase}");
};
// React to successful recovery
client.Reconnected += (sender, e) =>
{
Console.WriteLine(
$"[Reconnected] Recovered via {e.Phase} after {e.AttemptNumber} attempt(s)");
};
// Handle subscription data (survives reconnection)
client.DataChanged += (sender, e) =>
{
Console.WriteLine($"[Data] {e.NodeId}: {e.Value}");
};
await client.ConnectAsync();
// Create a subscription — it will be automatically transferred on reconnect
var sub = await client.CreateSubscriptionAsync(publishingInterval: 1000);
await client.CreateMonitoredItemsAsync(sub.SubscriptionId, new[]
{
new MonitoredItemCreateRequest
{
ItemToMonitor = new ReadValueId
{
NodeId = new NodeId(0, 2258),
AttributeId = AttributeIds.Value
},
MonitoringMode = MonitoringMode.Reporting,
RequestedParameters = new MonitoringParameters
{
SamplingInterval = 500,
QueueSize = 10,
DiscardOldest = true
}
}
});
Console.WriteLine("Running — press Enter to disconnect.");
Console.ReadLine();
await client.DisconnectAsync();Configuration Reference
Builder Methods
var client = new OpcSharpClientBuilder()
.WithKeepAliveInterval(TimeSpan.FromSeconds(10)) // How often to check server health
.WithReconnectPeriod(
initial: TimeSpan.FromSeconds(2), // First retry delay
max: TimeSpan.FromMinutes(1)) // Maximum backoff ceiling
.WithTokenLifetime(TimeSpan.FromHours(2)) // SecureChannel token lifetime
.Build();Disabling Features
var client = new OpcSharpClientBuilder()
.WithEndpoint("opc.tcp://localhost:4840")
.WithDisableKeepAlive() // No automatic health checks
.WithDisableAutoReconnect() // No automatic session recovery
.Build();Warning: Disabling keepalive also disables automatic failure detection. Without it, connection loss is only discovered when a service call fails.
Defaults
| Setting | Default | Builder Method |
|---|---|---|
| Keepalive interval | 5 seconds | WithKeepAliveInterval() |
| Reconnect initial delay | 1 second | WithReconnectPeriod() |
| Reconnect max delay | 30 seconds | WithReconnectPeriod() |
| Reconnect jitter | ±10% | Automatic |
| Token lifetime | 1 hour | WithTokenLifetime() |
| Token renewal trigger | 75% ± 5% of lifetime | Automatic |
How It Works Internally
KeepAlive fails ──┐
├──→ SessionReconnectHandler triggered
Token renewal fails┘
│
Phase 1: Reactivate
┌─ New TCP connection
├─ Hello/ACK handshake
├─ Open new SecureChannel
└─ ActivateSession (existing ID)
│
┌─────┴─────┐
Success BadSessionIdInvalid
│ │
Done Phase 2: Recreate
┌─ New TCP + SecureChannel
├─ CreateSession + ActivateSession
└─ TransferSubscriptions (or recreate)
│
DoneThe backoff between retry attempts doubles exponentially (1s → 2s → 4s → … → 30s max) with ±10% jitter to avoid thundering herd problems when many clients reconnect simultaneously.