Quick diagnoses for the issues that come up most often. Each answer is a checklist ordered by likelihood.
Why can't my client connect to the server?
Transport and address must match. The client needs the same transport type and the exact address the server listens on: the same port for TCP, the same pipe name for named pipes, the paired scheme for WebSocket (ws:// against http://, wss:// against https://).
The server must be listening first. server.StartWaitingForConnection() has to run before the client's ConnectAsync. A client racing a slow-starting server is the classic first-run failure; a short retry loop around ConnectAsync absorbs it.
Security settings must mirror. A token on the server requires the same token on the client; encryption enabled on one side requires it on the other, in the same mode (standard or BouncyCastle). Any mismatch fails the handshake before a single call happens.
Check the network path. For TCP and WebSocket, confirm the port is open and not firewalled. For local IPC, confirm the names match and the client has permission to access the pipe or memory-mapped file.
Turn on logging. options.WithLogger(...) on both sides shows which handshake step refused the connection: token validation, encryption negotiation, or transport. One log line replaces an hour of guessing.
Calls worked, now they time out
Is the server still there? A crashed or stopped server, or a disposed WitServer, leaves clients hanging until timeout. Check the process first.
Did the connection drop? Network interruptions kill TCP and WebSocket connections silently; the next call times out. If interruptions are part of life in your environment, enable auto-reconnection → so the client restores the channel itself.
Did the service method throw or hang? Server-side exceptions normally reach the client wrapped in WitExceptionFault, but a method deadlocked on a lock or waiting on an external resource that never answers looks like a timeout from the client's side. Check server logs; wrap suspect logic in try/catch with logging.
Is the operation legitimately long? WitRPC awaits your Task to completion. For genuinely long work, raise the timeout with options.WithTimeout(...), or restructure: return quickly and report progress through events.
Events don't arrive at the client
Subscribe on a live proxy. Connect, call GetService<T>(), then subscribe. Subscriptions made before the connection exists never register.
Is the server actually raising the event? The most common cause. Verify the implementation invokes the event delegate, and on the instance clients are connected to.
The right proxy in composite setups. An event declared on INotificationService must be subscribed on the INotificationService proxy. With several interfaces on one connection it is easy to hold the wrong one.
Every client subscribes for itself. Subscriptions are per connection. Events broadcast to all subscribed clients; a client that never subscribed receives nothing.
Background threads and UI. Handlers run on thread-pool threads. A UI that "does not update" on events usually updates fine, on the wrong thread: marshal to the UI thread (dispatcher in WPF/WinUI, InvokeAsync(StateHasChanged) in Blazor).
Security mismatch errors
An "unauthorized" rejection or an instant disconnect during connection means the two sides disagree on security. Token errors surface as authorization failures; encryption mismatches surface as handshake or encryption exceptions. The fix is always the same: mirror the configuration. Keep tokens and security flags in one shared configuration source so the two sides cannot drift, and use WithLogger(...) on the server to see the specific refusal reason. Details and the full options are in Security & Authentication →.
Testing without a network
Local transports make the full RPC path testable in one process, with no network, no firewall rules, and no flakiness:
var pipeName = $"test_{Guid.NewGuid():N}";
var server = WitServerBuilder.Build(options =>
{
options.WithNamedPipe(pipeName);
options.WithService(new MyService());
options.WithJson();
});
server.StartWaitingForConnection();
var client = WitClientBuilder.Build(options =>
{
options.WithNamedPipe(pipeName);
options.WithJson();
});
await client.ConnectAsync(TimeSpan.FromSeconds(5), CancellationToken.None);
var proxy = client.GetService<IMyService>();
var result = await proxy.DoSomethingAsync();
await client.DisconnectAsync();
server.StopWaitingForConnection();A unique name per test keeps parallel runs from colliding, and disposing both sides in teardown releases the pipe handles. This exercises serialization, transport, and events end to end. When only business logic is under test, skip RPC entirely and instantiate the service class directly; the in-process transport is for testing the integration itself.
How do I improve throughput?
Switch to a binary serializer. WithMessagePack() or WithMemoryPack() on both sides is the single biggest easy win over JSON: smaller payloads, faster encoding.
Match the transport to the topology. Same machine: named pipes or memory-mapped files beat TCP by skipping the network stack, with MMF fastest for large one-to-one transfers. Reserve TCP and WebSocket for actual cross-machine traffic.
Break up huge payloads. Very large single messages monopolize the connection and memory. Design the contract to move big data in chunks across several calls, with progress reported through events.
Keep service methods async and unblocked. Heavy computation and slow I/O belong in awaited tasks, not blocking the request path; the server then handles other calls while the work runs.
Then tune the knobs. After the above, adjust WithTimeout(...) to fit legitimately long calls. Measured comparisons of transports and serializers are in Comparing RPC Frameworks in .NET Applications →.
Not covered here? Open an issue or discussion on GitHub: questions that reach the tracker tend to become the next entries on this page.