Connections drop, servers restart, transient errors happen. WitRPC ships two complementary mechanisms for surviving them, plus health check integration for observing connection state in production.

Feature Handles Scope
Auto-reconnection Lost connections: network outages, server restarts Connection
Retry policy Failed individual calls: timeouts, transient errors Single RPC call

The two work independently and combine naturally: reconnection restores the channel, retries repeat the call.

Automatic reconnection

With auto-reconnection enabled, the client monitors its connection and re-establishes it after a drop:

csharp
var client = WitClientBuilder.Build(options =>
{
    options.WithTcp("server.example.com", 5000);
    options.WithJson();
    options.WithEncryption();
    options.WithAutoReconnect();   // defaults
});

Behavior is configurable:

csharp
options.WithAutoReconnect(reconnect =>
{
    reconnect.MaxAttempts = 10;                        // 0 = unlimited
    reconnect.InitialDelay = TimeSpan.FromSeconds(1);
    reconnect.MaxDelay = TimeSpan.FromMinutes(2);
    reconnect.BackoffMultiplier = 2.0;
    reconnect.ReconnectOnDisconnect = true;            // also reconnect when the server closes the connection
});

Delays grow exponentially between attempts (InitialDelay × BackoffMultiplier^n, capped at MaxDelay), so with the defaults the sequence runs 1s, 2s, 4s, 8s and onward up to two minutes. Backoff prevents a fleet of clients from hammering a server that just came back up.

Reconnection callbacks

Three callbacks expose the process, which is where applications refresh their state after an interruption:

csharp
options.WithAutoReconnect(reconnect =>
{
    reconnect.OnReconnecting = (attempt, delay) =>
        Console.WriteLine($"Reconnection attempt {attempt}, waiting {delay}...");

    reconnect.OnReconnected = () =>
    {
        Console.WriteLine("Reconnected.");
        // Refresh state the client may have missed while offline
    };

    reconnect.OnReconnectionFailed = lastException =>
    {
        Console.WriteLine($"Reconnection failed: {lastException?.Message}");
        // Alert the user, switch to offline mode
    };
});

Connection state and manual control

The client exposes its state through ConnectionState:

csharp
switch (client.ConnectionState)
{
    case ReconnectionState.Connected:     /* normal operation */          break;
    case ReconnectionState.Reconnecting:  /* attempts in progress */      break;
    case ReconnectionState.Disconnected:  /* idle, not reconnecting */    break;
    case ReconnectionState.Failed:        /* attempts exhausted */        break;
}

await client.StopReconnectionAsync() cancels ongoing attempts, which belongs in shutdown paths. WithoutAutoReconnect() disables the feature explicitly.

Retry policies

A retry policy repeats an individual call that failed with a retryable error. Since 3.0 the policy is deliberately conservative in two ways.

Only client-local failures are retried by default. The retryable statuses out of the box are Timeout (the call did not come back in time) and TransportError (the connection failed mid-call). Both are outcomes where a second attempt can genuinely succeed. A service fault (InternalServerError, the implementation threw) is never retried by default: re-running failed business logic must be an explicit decision, made through RetryableStatuses or RetryOn<TException>().

Only methods declared idempotent are retried. With no declarations the policy is inert, so a command never silently runs twice:

csharp
options.WithRetryPolicy(retry =>
{
    retry.MaxRetries = 3;
    retry.InitialDelay = TimeSpan.FromMilliseconds(500);
    retry.MaxDelay = TimeSpan.FromSeconds(5);
    retry.BackoffType = BackoffType.Exponential;

    // Declare what is safe to re-execute:
    retry.MarkIdempotent(nameof(IMyService.GetStatus), nameof(IMyService.ListItems));
    // or, deliberately: retry.RetryAllMethods = true;
});

BackoffType selects how delays grow between attempts: Fixed keeps them constant, Linear grows them arithmetically, Exponential (the default) doubles them under the configured multiplier. An OnRetry callback (Action<Exception?, int, TimeSpan>) fires before each attempt, useful for logging.

Why a retried call never executes twice

Every invocation carries an InvocationId that stays stable across retries. When the first attempt did reach the server and only its response was lost, the server recognizes the retry as a duplicate and answers it from a bounded per-connection cache instead of re-executing the method. Declaring a method idempotent therefore says "a retry of this call is safe", and the de-duplication makes it safe even in the lost-response case that classic retry schemes get wrong.

The declaration is still yours to make: which operations are reads, which are state-setting updates, and which must never repeat (payments, counters, appends) is a property of your service semantics. The framework's default of retrying nothing until told keeps the dangerous case opt-in.

Health checks

The OutWit.Communication.Client.HealthChecks package plugs WitRPC clients into ASP.NET Core health checks. It works with clients registered through the dependency injection integration, referencing them by name:

csharp
builder.Services.AddHealthChecks()
    .AddWitRpcClient("MainClient");   // name of the DI-registered client

The check reports the named client's connection health through the standard health endpoint, alongside databases and other dependencies, so existing monitoring picks it up without special handling. Optional parameters set the check name, failure status, and tags. Registering named clients is covered in Dependency Injection →.

Putting it together

A production client typically enables all three:

csharp
var client = WitClientBuilder.Build(options =>
{
    options.WithTcp("server.example.com", 5000);
    options.WithMemoryPack();
    options.WithEncryption();
    options.WithAccessToken(token);

    options.WithAutoReconnect(r =>
    {
        r.MaxAttempts = 0;                            // keep trying
        r.OnReconnected = () => RefreshApplicationState();
    });

    options.WithRetryPolicy(r =>
    {
        r.MaxRetries = 3;
        r.BackoffType = BackoffType.Exponential;
        r.MarkIdempotent(nameof(IMyService.GetOrders), nameof(IMyService.GetStatus));
    });
});

Reconnection keeps the channel alive across outages, retries absorb transient call failures, and the health check makes both visible to monitoring. What remains application work: refreshing missed state in OnReconnected, and declaring which methods are idempotent, since without those declarations the retry policy stays inert by design.