Bolt.Net.Client
1.0.0-dev.200
See the version list below for details.
dotnet add package Bolt.Net.Client --version 1.0.0-dev.200
NuGet\Install-Package Bolt.Net.Client -Version 1.0.0-dev.200
<PackageReference Include="Bolt.Net.Client" Version="1.0.0-dev.200" />
<PackageVersion Include="Bolt.Net.Client" Version="1.0.0-dev.200" />
<PackageReference Include="Bolt.Net.Client" />
paket add Bolt.Net.Client --version 1.0.0-dev.200
#r "nuget: Bolt.Net.Client, 1.0.0-dev.200"
#:package Bolt.Net.Client@1.0.0-dev.200
#addin nuget:?package=Bolt.Net.Client&version=1.0.0-dev.200&prerelease
#tool nuget:?package=Bolt.Net.Client&version=1.0.0-dev.200&prerelease
Bolt
A high-performance binary RPC and streaming protocol for .NET. Faster than gRPC, leaner than SignalR, with bidirectional streaming and zero GC pressure.
Why Bolt?
Bolt was built to answer a simple question: what if we stripped away every layer of overhead between two .NET services and just sent raw bytes?
The result is a protocol that:
- Uses a compact 33-byte request header instead of HTTP/2 frames, HPACK headers, and Protobuf encoding
- Routes messages through a hub by reading only the header — the payload is never decoded during routing
- Achieves zero Gen0 garbage collections under any load level
- Scales via connection pooling — multiple WebSocket connections per client, distributed round-robin
Performance
All benchmarks run on .NET 10, Windows 11. "Hub" means the message is routed through a central server (Client → Hub → Service → Hub → Client). "Direct" means client connects straight to the service. All transports use the same hub architecture for fair comparison.
Sequential Latency (single request)
| Transport | Latency | Ops/sec | Memory/req |
|---|---|---|---|
| Bolt Direct | 71 us | 14,014 | 1.24 KB |
| Bolt Hub | 121 us | 8,233 | 1.55 KB |
| gRPC Direct | 136 us | 7,343 | 8.64 KB |
| SignalR Hub | 159 us | 6,296 | 5.94 KB |
| gRPC Hub | 279 us | 3,587 | 19.71 KB |
Concurrent Load (64 parallel requests)
| Transport | Latency | Ops/sec | Memory |
|---|---|---|---|
| Bolt Hub | 1,329 us | 752 | 88 KB |
| gRPC Direct | 1,396 us | 716 | 584 KB |
| Bolt Direct | 1,513 us | 661 | 68 KB |
| gRPC Hub | 1,679 us | 595 | 1,304 KB |
| SignalR Hub | 5,129 us | 195 | 378 KB |
Peak Throughput (100 concurrent batch)
| Transport | Per-op Latency | Peak Ops/sec | Memory/op |
|---|---|---|---|
| Bolt Direct | 12.7 us | 78,709 | 978 B |
| Bolt Hub | 16.7 us | 60,014 | 1,313 B |
| gRPC Hub | 17.4 us | 57,515 | 20,780 B |
| gRPC Direct | 17.3 us | 57,659 | 9,268 B |
| SignalR Hub | 68.5 us | 14,589 | 5,956 B |
Scalability (many concurrent clients)
Each Bolt client uses 2 WebSocket connections. Each gRPC client uses 1 HTTP/2 channel.
| Clients | Bolt Latency | Bolt Memory | gRPC Latency | gRPC Memory |
|---|---|---|---|---|
| 10 | 425 us | 12.6 KB | 855 us | 198 KB |
| 50 | 1,148 us | 64 KB | 2,523 us | 1,002 KB |
| 100 | 2,131 us | 126 KB | 4,869 us | 2,001 KB |
At 100 clients, Bolt is 56% faster and uses 94% less memory than gRPC.
Head-to-Head: Bolt vs gRPC
| Metric | Bolt | gRPC | Winner |
|---|---|---|---|
| Sequential latency (hub) | 121 us | 279 us | Bolt by 57% |
| Concurrent latency (hub) | 1,329 us | 1,679 us | Bolt by 21% |
| Peak throughput (hub) | 60,014 ops/s | 57,515 ops/s | Bolt by 4% |
| Peak throughput (direct) | 78,709 ops/s | 57,659 ops/s | Bolt by 37% |
| Memory per request | 1.3 KB | 20 KB | Bolt by 94% |
| Memory at 100 clients | 126 KB | 2,001 KB | Bolt by 94% |
| GC pressure | Zero | Zero | Tie |
| Streaming | IAsyncEnumerable | IAsyncEnumerable | Tie |
| Browser support | WebSocket (native) | gRPC-Web (proxy) | Bolt |
| Serialization | MemoryPack (binary) | Protobuf (binary) | Bolt (faster) |
| Schema required | No | Yes (.proto) | Bolt (simpler) |
| Hub routing | Built-in | Not built-in | Bolt |
How It Works
Wire Protocol
Every Bolt frame starts with a 1-byte type followed by a fixed-size header. The hub only reads the header for routing — the payload bytes are forwarded without decoding.
RPC Request: [1:type] [16:requestId] [4:recipientHash] [4:senderHash] [4:commandHash] [4:payloadLen] [payload] 33B header
RPC Response: [1:type] [16:requestId] [2:statusCode] [4:payloadLen] [payload] 23B header
Stream Open: [1:type] [16:streamId] [4:recipientHash] [4:commandHash] 25B header
Stream Data: [1:type] [16:streamId] [4:payloadLen] [payload] 21B header
Stream Close: [1:type] [16:streamId] [2:statusCode] 19B header
Routing uses FNV-1a hashes (4-byte integer comparison) instead of string matching.
Registration Identity Binding
Authenticated service connections can bind the Bolt register identity to the service identity from the authenticated principal. The reusable server exposes BoltServerOptions.RegistrationIdentityBindingMode with Off, Audit, and Enforce modes. XFramework Bolt Hub configures this through BoltConfiguration:RegistrationIdentityBindingMode, reserves the central XFrameworkServiceNames.All identities, and defaults to Audit so mismatches are visible before rollout switches to Enforce.
For service clients, the expected Bolt registration is clientName == authenticated service name and clientId == SHA256(clientName). Non-service user/browser clients keep the normal registration path unless they attempt to claim a reserved service identity by name, prefix, or reserved deterministic service client ID.
XFramework Hub intentionally fixes the required service scope to bolt.service and resolves service identity from client_id, service, azp, then sub. Set BoltConfiguration:RegistrationIdentityBindingMode to Audit during rollout and watch for Bolt registration identity mismatch allowed in audit mode warnings. Switch to Enforce once all callers register with their authenticated service identity.
Architecture
┌──────────────┐
Client A ───WS──▶│ │──WS──▶ Service B
Client A ───WS──▶│ Bolt Server │──WS──▶ Service B
│ (Hub) │
Client C ───WS──▶│ │──WS──▶ Service D
Client C ───WS──▶│ │──WS──▶ Service D
└──────────────┘
Each client can open multiple WebSocket connections to the hub. The hub round-robins requests across all connections for a given service, eliminating single-connection bottlenecks.
For direct mode (no hub), the client connects straight to the service:
Client ───WS──▶ Service (handles requests locally)
Why Bolt Beats gRPC
gRPC's overhead at each hop:
- HTTP/2 HPACK header encode/decode
- Protobuf serialize/deserialize
- HTTP/2 stream frame management
- gRPC status and trailer processing
Bolt eliminates all of this:
- Compact binary headers — no HTTP framing
- MemoryPack payload — faster than Protobuf, no schema required
- Hub forwards raw bytes — zero decode at the routing layer
- FNV-1a hash routing — 4-byte integer comparison
Why Bolt Beats SignalR
SignalR adds overhead from:
- MessagePack encoding for the SignalR protocol layer (on top of your payload)
- Hub method resolution by string name
- Connection management overhead
- No native connection pooling — single connection per client
SignalR also collapses under high concurrent load (5,129 us at 64 concurrent vs Bolt's 1,329 us).
Features
XFramework Generated Handlers
XFramework API modules normally expose Bolt-callable feature handlers through source generation rather than manual RegisterHandler calls. Put [BoltHandler] on a static VSA handler whose first parameter implements IBoltRequest<TRequest, TResponse>. If the same method also has [MapPost], [MapGet], [MapPut], [MapPatch], or [MapDelete], BoltHandlerGenerator emits both the Bolt IBoltHandler and the Minimal API adapter. Module Program.cs maps generated REST routes with app.MapGeneratedEndpoints(); Bolt handler registration is discovered at startup by the XFramework integration hosted service.
public sealed record AuthenticateIdentityRequest(string Email, string Password) :
IBoltRequest<AuthenticateIdentityRequest, Result<AuthenticateIdentityResponse>>;
public static class AuthenticateEndpoint
{
[MapPost("/api/auth/authenticate", Tags = ["Auth"])]
[BoltHandler]
public static Task<Result<AuthenticateIdentityResponse>> Handle(
AuthenticateIdentityRequest request,
IAuthService authService,
CancellationToken ct) => authService.AuthenticateAsync(request, ct);
}
Use manual BoltServer.RegisterHandler only for low-level protocol tests, direct-mode samples, or infrastructure that is not a VSA feature.
RPC (Request-Response)
// Typed — auto-serializes request and deserializes response with MemoryPack
var response = await client.SendAsync<GreetRequest, GreetResponse>("greeting-service", "greet",
new GreetRequest { Name = "World" });
// Command (no response body)
var status = await client.SendAsync("service", "delete-item",
new DeleteRequest { Id = itemId });
// Raw bytes (when you need full control)
var (statusCode, data) = await client.InvokeAsync("service", "command", rawBytes);
Bidirectional Streaming
Stream any binary data — video, audio, files, sensor data:
// Sender
var stream = await client.OpenStreamAsync("video-service", "upload");
await stream.SendAsync(frame1);
await stream.SendAsync(frame2);
await stream.CloseAsync();
// Receiver
client.RegisterStreamHandler("upload", async (stream) =>
{
await foreach (var chunk in stream.ReadAllAsync())
ProcessChunk(chunk);
});
Typed Streaming with IAsyncEnumerable
Auto-serialization with MemoryPack:
// Pipe an async producer into a stream
await client.StreamAsync("analytics", "ingest",
GetSensorReadingsAsync(), ct); // IAsyncEnumerable<SensorReading>
// Receive typed objects
client.RegisterStreamHandler<SensorReading>("ingest",
async (readings, stream) =>
{
await foreach (var reading in readings)
await StoreAsync(reading);
});
Connection Pooling
Multiple WebSocket connections per client, auto-scaling under load:
var options = new BoltClientOptions
{
MinConnections = 2, // Start with 2 connections
MaxConnections = 8, // Scale up to 8 under load
ScaleUpThreshold = 16, // Scale when pending sends > 16
RpcTimeoutSeconds = 30
};
Direct Mode
Server handles requests locally — no routing hop:
// Server
var server = app.Services.GetRequiredService<BoltServer>();
server.RegisterHandler("hello", async (payload, requestId) =>
{
var request = MemoryPackSerializer.Deserialize<HelloRequest>(payload.Span);
var response = new HelloResponse { Message = $"Hello {request.Name}" };
return (HttpStatusCode.OK, MemoryPackSerializer.Serialize(response));
});
app.UseWebSockets();
app.MapBolt("/bolt");
// Client
var client = new BoltClient(new Uri("ws://server/bolt"), "my-client", "MyClient", options, logger);
await client.ConnectAsync();
var (status, data) = await client.InvokeAsync("_", "hello", payload);
Packages
| Package | Description | Dependencies |
|---|---|---|
Bolt.Protocol |
Wire format, codec, buffers | None |
Bolt.Server |
Hub server middleware for ASP.NET Core | Bolt.Protocol |
Bolt.Client |
RPC + streaming client with DI support | Bolt.Protocol, MemoryPack |
Quick Start
Server
var builder = WebApplication.CreateBuilder();
builder.Services.AddBoltServer();
// Or with options:
// builder.Services.AddBoltServer(o => o.InvocationTimeoutMs = 60000);
var app = builder.Build();
app.UseWebSockets();
app.MapBolt("/bolt");
app.Run();
Client (via DI — recommended for Blazor, ASP.NET, hosted apps)
builder.Services.AddBoltClient(bolt => bolt
.WithServer("ws://localhost:5000/bolt")
.WithClientId("my-service")
.WithClientName("MyService")
.WithMinConnections(2)
.WithMaxConnections(8)
.WithTimeout(30)
.HandleRpc("greet", async (payload, id) =>
{
var name = MemoryPackSerializer.Deserialize<string>(payload.Span);
var reply = MemoryPackSerializer.Serialize($"Hello {name}");
return (HttpStatusCode.OK, (ReadOnlyMemory<byte>)reply);
})
.HandleStream("live-data", async (stream) =>
{
await foreach (var chunk in stream.ReadAllAsync())
ProcessUpdate(chunk);
})
);
The client auto-connects on app startup via IHostedService and disconnects on shutdown. Then inject it anywhere:
public class GreetingService(BoltClient bolt)
{
public async Task<HelloMsg> Greet(string name)
{
return await bolt.SendAsync<HelloMsg, HelloMsg>("greeting-service", "greet",
new HelloMsg { Text = name });
}
}
Client (manual — for console apps or when you need full control)
var client = new BoltClient(
new Uri("ws://localhost:5000/bolt"),
"my-service", "MyService",
new BoltClientOptions { MinConnections = 2 }, logger);
client.RegisterHandler("greet", async (payload, id) =>
{
var name = MemoryPackSerializer.Deserialize<string>(payload.Span);
var reply = MemoryPackSerializer.Serialize($"Hello {name}");
return (HttpStatusCode.OK, (ReadOnlyMemory<byte>)reply);
});
await client.ConnectWithRetryAsync();
Direct Mode (server handles requests locally, no hub routing)
// Server
builder.Services.AddBoltServer();
var app = builder.Build();
app.Services.GetRequiredService<BoltServer>().RegisterHandler("hello", async (payload, id) =>
{
var msg = MemoryPackSerializer.Deserialize<HelloMsg>(payload.Span)!;
var reply = MemoryPackSerializer.Serialize(new HelloMsg { Text = $"Hello {msg.Text}" });
return (HttpStatusCode.OK, (ReadOnlyMemory<byte>)reply);
});
app.UseWebSockets();
app.MapBolt("/bolt");
// Client connects directly — no hub needed
builder.Services.AddBoltClient(bolt => bolt
.WithServer("ws://server:5000/bolt")
.WithClientId("caller")
);
// Usage — typed, clean
var response = await bolt.SendAsync<HelloMsg, HelloMsg>("_", "hello", new HelloMsg { Text = "World" });
Blazor Server / WASM
// In Program.cs
builder.Services.AddBoltClient(bolt => bolt
.WithServer("ws://api.myapp.com/bolt")
.WithClientId($"blazor_{Guid.NewGuid():N}")
.WithClientName("BlazorApp")
.HandleRpc("notification", async (payload, id) =>
{
// Handle server-push notifications
var notification = MemoryPackSerializer.Deserialize<Notification>(payload.Span);
NotificationStore.Add(notification);
return (HttpStatusCode.OK, ReadOnlyMemory<byte>.Empty);
})
.HandleStream("live-feed", async (stream) =>
{
await foreach (var update in stream.ReadAllAsync<LiveUpdate>())
StateContainer.ApplyUpdate(update);
})
);
// In any component or service — inject and use
@inject BoltClient Bolt
@code {
private async Task SendMessage(string text)
{
await Bolt.SendAsync("chat-service", "send", new ChatMessage { Text = text });
}
private async Task<UserProfile> GetProfile(Guid userId)
{
return await Bolt.SendAsync<GetProfileRequest, UserProfile>("user-service", "get-profile",
new GetProfileRequest { UserId = userId });
}
}
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Bolt.Net.Protocol (>= 1.0.0-dev.200)
- MemoryPack (>= 1.21.4)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.9)
- Microsoft.Extensions.ObjectPool (>= 10.0.9)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Bolt.Net.Client:
| Package | Downloads |
|---|---|
|
XFramework.Integration
XFramework integration layer — Bolt drivers, JWT, SignalR, DataContext, and service abstractions. |
|
|
Bolt.Net.Media
Bolt Media — voice/video call support for Bolt protocol. E2E encryption, adaptive bitrate, FEC, NACK retransmission, simulcast, P2P direct connections. Add-on package for Bolt.Net.Client. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 1,294 | 3/24/2026 |
| 1.0.0-dev.200 | 0 | 7/8/2026 |
| 1.0.0-dev.199 | 71 | 7/6/2026 |
| 1.0.0-dev.198 | 75 | 7/6/2026 |
| 1.0.0-dev.197 | 75 | 7/6/2026 |
| 1.0.0-dev.196 | 103 | 7/3/2026 |
| 1.0.0-dev.195 | 106 | 7/3/2026 |
| 1.0.0-dev.194 | 110 | 7/3/2026 |
| 1.0.0-dev.193 | 106 | 7/3/2026 |
| 1.0.0-dev.192 | 111 | 7/3/2026 |
| 1.0.0-dev.191 | 105 | 7/3/2026 |
| 1.0.0-dev.190 | 105 | 7/3/2026 |
| 1.0.0-dev.189 | 108 | 7/3/2026 |
| 1.0.0-dev.188 | 108 | 7/3/2026 |
| 1.0.0-dev.187 | 108 | 7/2/2026 |
| 1.0.0-dev.186 | 110 | 7/2/2026 |
| 1.0.0-dev.185 | 108 | 7/1/2026 |
| 1.0.0-dev.177 | 100 | 6/28/2026 |
| 1.0.0-dev.176 | 104 | 6/28/2026 |
| 1.0.0-dev.175 | 107 | 6/28/2026 |