Introduction
Blazor Server maintains a persistent SignalR connection (circuit) between the browser and server. When this connection drops (network interruption, server restart, load balancer timeout), the user sees a reconnection UI. If the circuit cannot be restored within the configured time, the page must be refreshed, losing all component state. This is a significant UX issue for production Blazor Server apps.
Symptoms
- "Attempting to reconnect to the server" message persists indefinitely
- "Reload" button appears after reconnection timeout
- Connection drops every 30-60 seconds behind a load balancer
- Circuit lost after server deployment
WebSocket connection closedin browser console
Example browser console output:
``
[2026-04-09T10:15:00.000Z] Information: Normalizing '/_blazor' to
'https://myapp.com/_blazor'.
[2026-04-09T10:15:30.000Z] Warning: Connection timeout.
[2026-04-09T10:15:35.000Z] Error: Connection disconnected with error
'Error: Server timeout elapsed without receiving a message from the server'.
Common Causes
- Load balancer not configured for WebSocket support
- Server restarted during deployment, killing all circuits
- SignalR keepalive interval too short or too long
- Reverse proxy (Nginx, HAProxy) not forwarding WebSocket headers
- Circuit lifetime too short for the expected disconnection duration
Step-by-Step Fix
- 1.Configure reconnection behavior:
- 2.```razor
- 3.<!-- _Host.cshtml or App.razor -->
- 4.<component type="typeof(App)"
- 5.render-mode="ServerPrerendered"
- 6.param-Reconnect="@(new ReconnectOptions
- 7.{
- 8.MaxRetries = 10,
- 9.RetryIntervalMilliseconds = 2000
- 10.})" />
- 11.
` - 12.Customize the reconnection UI:
- 13.```razor
- 14.<!-- _Host.cshtml -->
- 15.<div id="reconnect-modal" style="display: none;">
- 16.<div class="reconnect-overlay">
- 17.<div class="reconnect-content">
- 18.<h3>Connection Lost</h3>
- 19.<p>Attempting to reconnect... <span id="reconnect-count">0</span></p>
- 20.<button id="manual-reconnect" onclick="Blazor.reconnect()">Reconnect Now</button>
- 21.<button id="reload-page" onclick="window.location.reload()">Reload Page</button>
- 22.</div>
- 23.</div>
- 24.</div>
<script src="_framework/blazor.server.js"></script> <script> Blazor.defaultReconnectionHandler._reconnectionCallback = () => { document.getElementById('reconnect-modal').style.display = 'block'; }; Blazor.defaultReconnectionHandler._reconnectionSucceededCallback = () => { document.getElementById('reconnect-modal').style.display = 'none'; }; </script> ```
- 1.Configure SignalR keepalive and timeouts:
- 2.```csharp
- 3.builder.Services.AddServerSideBlazor(options =>
- 4.{
- 5.options.DetailedErrors = builder.Environment.IsDevelopment();
- 6.options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(5);
- 7.options.JSInteropDefaultCallTimeout = TimeSpan.FromMinutes(1);
- 8.options.MaxBufferedUnacknowledgedReceiveUps = 10;
- 9.})
- 10..AddHubOptions(options =>
- 11.{
- 12.options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
- 13.options.KeepAliveInterval = TimeSpan.FromSeconds(15);
- 14.options.HandshakeTimeout = TimeSpan.FromSeconds(15);
- 15.options.MaximumReceiveMessageSize = 64 * 1024; // 64KB
- 16.options.StreamBufferCapacity = 10;
- 17.});
- 18.
` - 19.Configure load balancer for WebSockets:
- 20.```yaml
- 21.# Kubernetes Ingress
- 22.apiVersion: networking.k8s.io/v1
- 23.kind: Ingress
- 24.metadata:
- 25.annotations:
- 26.nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
- 27.nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
- 28.nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
- 29.nginx.ingress.kubernetes.io/configuration-snippet: |
- 30.proxy_set_header Upgrade $http_upgrade;
- 31.proxy_set_header Connection "upgrade";
- 32.
`
# Nginx configuration
location /_blazor {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}Prevention
- Configure load balancer with WebSocket support and long timeouts
- Set
KeepAliveIntervalto 15 seconds (default) andClientTimeoutIntervalto 30-60 seconds - Use sticky sessions if running multiple Blazor Server instances
- Implement circuit state persistence for critical user data
- Add custom reconnection UI with manual retry options
- Monitor SignalR connection metrics in Application Insights