Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion aspnetcore/blazor/state-management/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: Learn how to persist user data (state) in server-side Blazor apps.
monikerRange: '>= aspnetcore-3.1'
ms.author: wpickett
ms.custom: mvc
ms.date: 11/11/2025
ms.date: 07/14/2026
uid: blazor/state-management/server
---
# ASP.NET Core Blazor server-side state management
Expand Down Expand Up @@ -185,6 +185,93 @@ window.addEventListener('visibilitychange', () => {

:::moniker range=">= aspnetcore-11.0"

## Automatic circuit pause on tab inactivity
Comment thread
ilonatommy marked this conversation as resolved.

<!-- UPDATE 11.0 - API browser cross-links -->

The framework can optionally pause a circuit when the browser tab becomes hidden, freeing server memory and SignalR connections held by inactive users. Auto-pause is provided by the `Microsoft.AspNetCore.Components.Server.AutoPause` package. After adding a package reference, enable the feature by calling `AddAutoPause` when the app's root component is mapped:

```csharp
app.MapRazorComponents<App>()
.WithBrowserOptions(options => options.AddAutoPause(p => p.HiddenDelay = TimeSpan.FromSeconds(30)));
```

`AddAutoPause` configures an `AutoPauseBrowserOptions` instance with the following properties:

* `Enabled`: Whether auto-pause is enabled. Defaults to `true`, so calling `AddAutoPause` is what enables the feature.
* `HiddenDelay`: The delay after the tab becomes hidden before the circuit pauses. Defaults to two minutes (`TimeSpan.FromMinutes(2)`) and must be greater than `TimeSpan.Zero`.

After the tab is hidden for `HiddenDelay`, the circuit pauses. If the user returns before the delay elapses, the pause doesn't occur.

> [!NOTE]
> Auto-pause triggers on the [Page Visibility API](https://developer.mozilla.org/docs/Web/API/Page_Visibility_API) `visibilitychange` event, whose meaning differs by platform:
>
> * On desktop, the tab becomes hidden when the user switches tabs or minimizes the window. The pause timer runs reliably, and the circuit pauses gracefully after the delay.
> * On mobile, the page also becomes hidden when the *whole app* is backgrounded (switching apps, returning to the home screen, or locking the screen), not just when switching browser tabs.
>
> On mobile, the operating system suspends the page's JavaScript shortly after the app is backgrounded (within seconds on Android, up to about 30 seconds on iOS). If `HiddenDelay` is longer than that window, the pause timer never fires, and the circuit is dropped by the OS-initiated disconnect instead of pausing gracefully. The session is still preserved through the normal reconnection and [circuit state persistence](#circuit-state-persistence) path, but the client-side veto and deferral logic doesn't run. For this reason, graceful auto-pause isn't guaranteed and isn't a supported scenario on mobile when the app is backgrounded.

The framework defers the pause while circuit-owned work is in progress (downloads, uploads, JS interop calls, Web Locks, Picture-in-Picture). It vetoes the pause entirely while focused text `<input>` elements with Blazor `@bind` bindings are edited or audio/video is playing.

For elements without Blazor bindings (for example, `<canvas>`, WebRTC connections, or custom elements), the app is responsible for handling state. Register a circuit handler with an `onCircuitPausing` callback in the [Blazor startup configuration](xref:blazor/fundamentals/startup):

```razor
<script>
Blazor.start({
circuit: {
circuitHandlers: [
{
onCircuitPausing: async (signal /* AbortSignal */) => {
// Example: save canvas state before the pause proceeds.
const canvas = document.getElementById('drawing-canvas');
if (canvas) {
localStorage.setItem('canvasData', canvas.toDataURL());
}

// Example: close an active WebRTC connection gracefully.
if (window.activePeerConnection && !signal.aborted) {
window.activePeerConnection.close();
await new Promise(resolve => {
signal.addEventListener('abort', resolve);
setTimeout(resolve, 100);
});
}
}
}
]
}
});
</script>
```

> [!NOTE]
> The `<input type="file">` element can't have its value restored after pause/resume due to browser security restrictions. Using `[PersistentState]` on a property bound to a file `<input>` element causes an `InvalidStateError` that crashes the circuit. Instead, capture the file name in a separate property. Because the browser doesn't expose the file name through the native `change` event, read it with a small JS interop helper:
>
> ```razor
> @inject IJSRuntime JS
>
> <input id="persist-upload" type="file" @onchange="HandleFileSelected" />
> <span>@SelectedFileName</span>
>
> @code {
> [PersistentState(AllowUpdates = true)]
> public string? SelectedFileName { get; set; }
>
> private async Task HandleFileSelected(ChangeEventArgs e)
> {
> SelectedFileName = await JS.InvokeAsync<string>("getFileName", "persist-upload");
> }
> }
> ```
>
> ```javascript
> window.getFileName = (id) => document.getElementById(id)?.files?.[0]?.name ?? '';
> ```

:::moniker-end

:::moniker range=">= aspnetcore-11.0"

## Server-triggered circuit pause

A server-side Blazor app that adopts the Interactive Server render mode can implement server-triggered circuit pause, which allows the app to gracefully pause client circuits, preserving client state for seamless reconnection.
Expand Down
Loading