TL;DR: Learn how to build a real-time dashboard in Blazor using gRPC server-streaming over HTTP/2. This guide covers proto contracts, streaming logic, interactive charts with Syncfusion, and performance tips like gzip compression and reconnect strategies. Perfect for .NET developers building low-latency apps.
Real‑time dashboards are critical for apps like stock trading, health monitoring, and delivery tracking. Missing an update can cost money or impact lives.
In this guide, you’ll learn how to build a Blazor dashboard powered by gRPC server-streaming for low-latency updates and interactive data visualization. We’ll use Syncfusion Blazor Charts to render real-time data in a sleek, interactive format, perfect for monitoring live metrics, trends, or sensor feeds.
Why Blazor + gRPC?
Blazor’s component model, combined with gRPC streaming, delivers:
- Low latency updates directly to the browser.
- Scalable architecture for thousands of clients.
- Secure communication via HTTP/2 and TLS.
Step 1: Set up your environment
Create two projects in Visual Studio:
- Blazor server app for UI using Microsoft templates.
- gRPC service for real-time data streaming as the backend. For detailed instructions, see the official document.
After creating the projects and solution, your folder structure should look like this:

You can find the complete codebase of this application in this GitHub Repository.
Add required NuGet packages
The gRPC client project requires the following NuGet packages:
- Grpc.Net.Client: Provides the .NET gRPC client.
- Google.Protobuf: Includes protobuf message APIs for C#.
- Grpc.Tools: Adds C# tooling support for.proto files (not required at runtime, so mark with PrivateAssets=”All”).
Install-Package Grpc.Net.Client
Install-Package Google.Protobuf
Install-Package Grpc.Tools
Reference the gRPC project in Blazor
Add a reference to the gRPC project in your Blazor app to use the auto-generated C# classes from .proto files. This avoids duplicating files or manually copying code.
<ProjectReference Include="..\DashboardGrpc\DashboardGrpc.csproj" />
Step 2: Define the proto contract
Create a new file at DashboardGrpc/Protos/dashboard.proto.
syntax = "proto3";
option csharp_namespace = "Dashboard";
package dashboard;
// The DashboardService exposes one server-streaming method.
service DashboardService {
// Subscribe tells the server which source to stream
// and how often. The server sends a stream of DataPoint.
rpc Subscribe (Subscription) returns (stream DataPoint);
}
// Subscription message carries the source identifier
// and the desired update interval in milliseconds.
message Subscription {
string source_id = 1; // e.g., "Sensor-A" or "EURUSD"
int32 interval_ms = 2; // server waits this long between messages
}
// DataPoint represents one timestamped value.
message DataPoint {
int64 timestamp = 1; // Unix time in milliseconds
double value = 2; // measurement or price
}
Configure protobuf code generation
To enable gRPC communication, both the client and server projects must generate code from the same .proto file and link them to avoid duplication. Add the following to each .csproj file.
Client (DashboardApp):
<ItemGroup>
<Protobuf Include="..\DashboardGrpc\Protos\dashboard.proto" GrpcServices="Client" />
</ItemGroup>
Server (DashboardGrpc):
<ItemGroup>
<Protobuf Include="Protos\dashboard.proto" GrpcServices="Server" />
</ItemGroup>
Enable HTTP/2 and TLS on the gRPC host
gRPC streaming needs HTTP/2. Most browsers demand TLS when HTTP/2 is in use. An HTTP endpoint for development supports both HTTP/1.1 and HTTP/2.
DashboardGrpc/Program.cs
builder.WebHost.ConfigureKestrel(k =>
{
k.ListenAnyIP(5001, opt =>
{
opt.UseHttps();
opt.Protocols = HttpProtocols.Http2;
opt.UseHttps();
});
});
Run dotnet run inside DashboardGrpc to confirm the server starts without errors.
Register the gRPC client in Blazor app (Program.cs)
Make sure the address matches the HTTPS endpoint configured in the gRPC server.
// DashboardApp/Program.cs
builder.Services.AddGrpcClient<DashboardService.DashboardServiceClient>(options =>
{
// The same HTTPS address you opened in Program.cs of the gRPC host
options.Address = new Uri("https://localhost:5001");
});
When you start the solution via F5 or dotnet run:
- DashboardGrpc launches on localhost:5001 using HTTP/2.
- DashboardApp launches on its default port, usually localhost:5002, and connects to the gRPC server using the registered client.
Note: If DashboardGrpc does not launch on Localhost:5001, verify the port configuration in launchSettings.json or update the client URI in DashboardApp accordingly.
Understanding gRPC streaming
In this section, you will learn how gRPC delivers different call patterns and why server-streaming is the right choice for a live dashboard. You will define the proto contract, implement the service method, and see how cancellation and keep-alive work together to maintain a reliable stream.
gRPC remote calls fall into four patterns:
- Unary: Client sends one request; server returns one reply.
- Server‑streaming: Client sends one request; server keeps sending many messages.
- Client‑streaming: Client streams many requests; server returns one reply.
- Bidirectional: Both sides stream many messages at the same time.
Here, we focus on server‑streaming because dashboards mostly consume continuous data but rarely need to push large amounts back.
For a dashboard, the browser issues a single subscribe request and then waits as the server continuously pushes new data points. That maps exactly to server-streaming.
Implement server-streaming in gRPC
Create the service class
Add a new class named DashboardStreamService.cs to your gRPC project:
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Dashboard;
namespace DashboardGrpc.Services
{
public class DashboardStreamService : Dashboard.DashboardService.DashboardServiceBase
{
public override async Task Subscribe(
Subscription request,
IServerStreamWriter<DataPoint> responseStream,
ServerCallContext context)
{
var random = new Random();
// Loop until the client disconnects
while (!context.CancellationToken.IsCancellationRequested)
{
// Create a new data point
var point = new DataPoint
{
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
Value = random.NextDouble() * 100
};
// Send the point to the client
await responseStream.WriteAsync(point);
// Wait for the specified interval or cancellation
await Task.Delay(request.IntervalMs, context.CancellationToken);
}
}
}
}
Key concepts:
- while (!context.CancellationToken.IsCancellationRequested) Keeps the stream alive until the client disconnects (e.g., browser closes or network drops).
- context.CancellationToken Ensures the loop terminates gracefully when the client cancels the request.
- Task.Delay(…, context.CancellationToken) Waits between data points. If the client disconnects during the delay, it throws and exits the loop.
Register the service
In Program.cs of your DashboardGrpc project, map the service:
app.MapGrpcService<DashboardStreamService>();
This enables the gRPC server to handle incoming requests for your streaming service.
Keep-alive and connection health:
- gRPC over HTTP/2 uses built-in keep-alive pings to prevent idle timeouts.
- You can adjust Kestrel’s keep-alive settings in Program.cs if your network environment drops idle connections. However, the defaults are sufficient without modification for most development scenarios.
Building an interactive dashboard UI
In this section, you will connect the Blazor UI to the gRPC stream, consume each incoming DataPoint message, and update page elements in real time. You’ll learn how to:
- Subscribe to data using C# code
- Render raw values in the UI
- Replace the list with a chart component, without changing the underlying data flow
Register and inject the gRPC client
You must ensure the gRPC client is registered in Program.cs file. Then, inject it into any Razor component like this:
@using DashboardGrpc
@inject DashboardService.DashboardServiceClient Grpc
@page "/live"
@using Dashboard
@inject DashboardService.DashboardServiceClient Grpc
<h3>Real-Time Values</h3>
<button @onclick="StartStream" disabled="@isStreaming">Start</button>
<button @onclick="StopStream" disabled="@!isStreaming">Stop</button>
<p>Status: @status</p>
<ul>
@foreach (var p in points)
{
<li>@DateTimeOffset.FromUnixTimeMilliseconds(p.Timestamp): @p.Value:F2</li>
}
</ul>
@code {
private List<DataPoint> points = new();
private CancellationTokenSource? cts;
private bool isStreaming;
private string status = "Idle";
private async Task StartStream()
{
isStreaming = true;
status = "Connecting...";
cts = new CancellationTokenSource();
var request = new Subscription { SourceId = "sensor-1", IntervalMs = 500 };
using var call = Grpc.Subscribe(request, cancellationToken: cts.Token);
status = "Streaming";
try
{
var responseStream = call.ResponseStream;
while (await responseStream.MoveNext(cts.Token))
{
var dp = responseStream.Current;
points.Add(dp);
if (points.Count > 100) points.RemoveAt(0);
StateHasChanged(); // update the UI for each new point
}
}
catch (OperationCanceledException)
{
// Stream was canceled by StopStream
}
finally
{
isStreaming = false;
status = "Stopped";
}
}
private void StopStream()
{
cts?.Cancel();
}
}
Streaming logic overview
This section, we will explore the four core operations that power gRPC streaming in Blazor.
- StartStream: Opens the gRPC stream and continuously reads incoming messages.
- Data handling: Each DataPoint is added to a list called points, which is trimmed to the last 100 items to keep the UI responsive.
- UI update: The component refreshes using StateHasChanged() after each update.
- StopStream: Cancels the token, allowing the loop to exit gracefully.
Replacing the list with a chart
You can integrate any Blazor chart library. Here’s how to use Syncfusion Blazor Charts:
1. Install the package
Run this in the DashboardApp project:
dotnet add package Syncfusion.Blazor.Charts
2. Register Syncfusion services
In Program.cs of your Blazor client app (DashboardApp), register the Syncfusion Blazor service:
using Syncfusion.Blazor;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSyncfusionBlazor();
var app = builder.Build();
3. Include the script reference
In App.razor, add this at the end of the </body> tag:
In App.razor, add this at the end of the </body> tag:
<body>
....
<script src="_content/Syncfusion.Blazor.Core/scripts/syncfusion-blazor.min.js" type="text/javascript"></script>
</body>
Note: To learn more about Blazor charts, refer to the official Syncfusion documentation
4. Update the Razor component
Then, update the Razor page. You can find the updated Razor component here.
<h3>Real-Time Dashboard</h3>
<button
@onclick="StartStream"
disabled="@isStreaming"
class="btn btn-primary me-2">Start</button>
<button
@onclick="StopStream"
disabled="@(!isStreaming)"
class="btn btn-secondary">Stop</button>
<p class="mt-3">Status: <span class="badge bg-@(status == "Streaming" ? "success" : status == "Connecting..." ? "warning" : "secondary")">@status</span></p>
<div class="row mt-4">
<div class="col-md-8">
<div class="card bg-dark text-light">
<div class="card-header">
<h5>Real-Time Data Stream</h5>
</div>
<div class="card-body">
<SfChart
ref="chart"
Title="Real-Time Data Stream">
<ChartPrimaryXAxis
ValueType="Syncfusion.Blazor.Charts.ValueType.DateTime">
<ChartAxisLabelStyle Format="HH:mm:ss"/>
</ChartPrimaryXAxis>
<ChartPrimaryYAxis>
<ChartAxisLabelStyle Format="N2"/>
</ChartPrimaryYAxis>
<ChartSeriesCollection>
<ChartSeries
DataSource="@chartData"
XName="Timestamp"
YName="Value"
Type="ChartSeriesType.Line">
</ChartSeries>
</ChartSeriesCollection>
</SfChart>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-dark text-light">
<div class="card-header">
<h5>Latest Values</h5>
</div>
<div class="card-body" style="max-height: 400px; overflow-y: auto;">
<ul class="list-group list-group-flush">
@foreach (var p in points.TakeLast(10).Reverse())
{
<li class="list-group-item d-flex justify-content-between align-items-center bg-dark text-light">
<small>@p.Timestamp.ToString("HH:mm:ss")</small>
<span class="badge bg-primary rounded-pill">@p.Value.ToString("F2")</span>
</li>
}
</ul>
</div>
</div>
</div>
</div>
In StartStream, replace StateHasChanged() with await RenderChart() to redraw the chart for each new batch of points.
By separating data collection from rendering, you can easily switch between lists, tables, or charts without changing the streaming logic.
Optimizing performance
1. Enable Gzip compression
In DashboardGrpc/Program.cs, enable gzip compression:
builder.Services.AddGrpc(options =>
{
options.ResponseCompressionAlgorithm = "gzip";
options.ResponseCompressionLevel = CompressionLevel.Fastest;
});
2. Automatic reconnect with exponential backoff
To make your dashboard resilient to short network interruptions, wrap your streaming call in a retry helper:
await RunWithRetry(ct => StartStream(ct), cts.Token);
This ensures short network blips don’t break your dashboard. See the implementation in LiveData.razor.
Run multiple projects in Visual Studio
1. Open solution properties
In solution explorer, right-click the solution and select Properties.
2. Enable multiple startup projects
Navigate to common properties → startup project, then select multiple startup projects.
3. Set actions
For each project you want to run, set its Action to Start.
4. Run the solution
Press F5 or click Start to launch all selected projects simultaneously.

Scaling and profiling
To ensure your gRPC-based system performs reliably under load and scales effectively, apply the following strategies for deployment and monitoring.
- Horizontal scaling: Deploy multiple gRPC servers behind a Layer 4 load balancer. Use IP or cookie affinity if UI sessions need to be sticky.
- Monitor performance: Tracks real-time call counts, latency, and thread usage.
dotnet-counters monitor grpc
Load testing
1. Install tools
brew install k6 # macOS
choco install k6 # Windows
k6 install xk6-grpc
2. Create load-test.js
JavaScript
import grpc from "k6/net/grpc";
const client = new grpc.Client();
client.load(["./DashboardGrpc/Protos"], "dashboard.proto");
export default () => {
client.connect("localhost:5333", { plaintext: true });
const stream = client.invoke("dashboard.DashboardService/Subscribe", {
source_id: "sensor-1",
interval_ms: 100,
});
stream.on("data", (response) => console.log(`Received: ${JSON.stringify(response)}`));
client.close();
};
3. Run test
Simulates 100 virtual users for 30 seconds.
k6 run load-test.js --vus 100 --duration 30s
Summary
You have completed the end‑to‑end setup:
- Environment: Blazor UI and gRPC host.
- Streaming: Proto definitions and server‑streaming implementation.
- UI binding: Consuming IAsyncEnumerable and rendering as charts.
- Optimization: Compression, batching, reconnect logic, scaling, and profiling.
Next steps:
- Secure streams with JWT tokens or mutual TLS to ensure only authorized clients can subscribe.
- Consider SignalR for bidirectional messaging or legacy browser support without a proxy.
- Extend your proto to support multiple data sources, alerts, or user commands for richer interactivity.
Apply these patterns in your next project to deliver responsive dashboards that keep pace with real‑world data.
You can also contact us through our support forum, support portal, or feedback portal for queries. We are always happy to assist you!
Related Blogs
- 10 Steps to Replace REST Services with gRPC-Web in Blazor WebAssembly
- Fix Broken Charts Instantly: Handle Missing Data Like a Pro in Blazor!
- Visualize Financial Insights Instantly with Blazor Waterfall Charts
- How to Add Error Bars to Blazor Charts: A Complete Guide with Examples
This article was originally published at Syncfusion.com.
Top comments (0)