Offline persistence
A train goes into a tunnel, a laptop lid closes mid-export, a collector restarts. Without persistence
the telemetry of exactly those moments - often the interesting ones - is lost. With it, an export
request that fails with a network error or a retryable status is kept on the device and sent again
later: in localStorage in the browser, and in files in Blazor Hybrid and on servers.
What is kept, and when
Persistence works on whole export requests, already serialized, rather than on records: what is stored is exactly the body that could not be delivered, so it is sent again unchanged. A request is kept when it fails with a network error, or with one of the statuses a receiver uses to say "later":
Before a request is stored it is first retried in place a few times, with backoff that honours
Retry-After. Any other status - a 400 for a malformed body, a 401 for a wrong key - is
a permanent answer, and the request is dropped rather than replayed forever. An Azure Monitor
206 partial success retries only the items the response lists as retryable.
Persistence needs two things at once:
options.Persistence.Enabled- on by default - which is the operator's switch.-
The user's consent for the
Persistencefeature. Keeping data on someone's device is a decision of its own under GDPR, so it is a feature bit of its own, and nothing is written before it is granted.
builder.Services.AddBitBelemetryServices(options =>
{
options.Persistence.Enabled = true; // the default; consent still applies
options.Persistence.BrowserMaxBytes = 1024 * 1024; // localStorage is small: 1 MB
options.Persistence.MaxBytes = 20 * 1024 * 1024; // the file store (Hybrid, server): 20 MB
options.Persistence.MaxAge = TimeSpan.FromHours(48); // discarded unsent after this
options.Persistence.RetryInterval = TimeSpan.FromMinutes(1); // how often stored requests are retried (both sides)
// options.Persistence.Directory = "..."; // defaults to LocalApplicationData
});Where it is kept
| Host | Store | Size cap | Retried |
|---|---|---|---|
| Browser (every render mode) | localStorage key bit-belemetry-queue |
BrowserMaxBytes (1 MB) |
Every RetryInterval (1 minute) while the page is open, when the script starts (the next page load), when the browser fires online, and when Persistence is granted |
| WebAssembly .NET pipeline | Handed to the script, which stores it in the same localStorage queue |
BrowserMaxBytes |
As above - the script owns the retry, and can also send with keepalive during unload |
| Blazor Hybrid, and the server pipeline | Files under Directory (default {LocalApplicationData}/bit-belemetry/{service.name}) |
MaxBytes (20 MB) |
Every RetryInterval (1 minute) |
Past the size cap the oldest requests are evicted first, and a request older than
MaxAge (48 hours) is discarded unsent - telemetry from two days ago rarely answers a
question anyone is still asking, and a device should not fill up with it. Neither store keeps request
headers - an API key has no business sitting in localStorage or in a file - so they are
added again from the exporter's current configuration when a request is resent (a rotated key applies
to the backlog too). Credentials that expire, such as an Entra
ID token for Azure Monitor, are never stored on either side: the exporter adds a fresh one at send time.
Each entry of the browser queue has this shape:
Look at the queue
This panel reads localStorage['bit-belemetry-queue'] directly. On a healthy connection it
is empty - nothing failed, so nothing was kept. To fill it, open your browser's developer tools, set the
Network panel to Offline, press Track an event, and read the
queue again. Then go back online: the script hears the online event, resends, and the
queue empties.
- Persistence consent
- unknown
- bit-belemetry-queue
- not read yet
Track an event needs the Events feature granted, and the queue only
fills with Persistence granted. Revoking Persistence clears the queue at once:
read it again after pressing Revoke Persistence while it holds something.
Revoking clears it
Withdrawing consent has to reach data already stored, not only data still to come. Revoking
Persistence - from the banner, from IBelemetryConsent.RevokeAsync or from
BitBelemetry.setConsent in the browser - removes the bit-belemetry-queue key in the
browser and deletes the stored files on the .NET side. Revoking any other feature purges that
feature's records still queued in memory, so nothing it collected leaves the device afterwards.
Hybrid apps
In MAUI, WPF and WinForms the browser script relays its records to .NET, and the .NET pipeline exports
everything - so the file store is the one that matters. A phone is suspended long before it loses its
files, so flush when the app goes to the background; what does not get through is stored and retried on
the next RetryInterval after the app resumes.
API
BelemetryPersistenceOptions
Class
Offline persistence: export requests that fail with a network error or a retryable status are kept on the device and retried later, as long as the user granted BelemetryFeatures.Persistence.
| Name | Type and default | Description |
|---|---|---|
Enabled |
bool= true |
Whether persistence is used at all. Defaults to true (still subject to consent). |
Directory |
string |
The folder the .NET side stores requests in (Hybrid and Server). Defaults to {LocalApplicationData}/bit-belemetry/{service.name}. In WebAssembly the browser storage is used. |
MaxBytes |
long= 20971520 |
The maximum size of the .NET store; the oldest requests are evicted past it. Defaults to 20 MB. |
BrowserMaxBytes |
long= 1048576 |
The maximum size of the browser store (localStorage is small). Defaults to 1 MB. |
MaxAge |
TimeSpan= 2.00:00:00 |
How long a stored request is kept before it is discarded unsent. Defaults to 48 hours. |
RetryInterval |
TimeSpan= 00:01:00 |
How often stored requests are retried. Defaults to 1 minute. |