Telemetry that waits for the link to come back

Turn it on with one call and the SDK reports how the mesh behaved for your users: delivery latency, transport dwell, routing switches, relay activity, MLS handshake health. It batches on the device, holds the batches through an outage, and uploads them itself. Until you call it, nothing is collected.

Diagram: hosted telemetry, where a device running the protocol folds routing decisions, transport switches, and deliveries into batches, persists them while offline, and uploads them to the Offline Protocol ingest, which the developer portal shows as a dashboard.

unit-07 folds what the mesh did into batches, keeps them through the outage, and uploads them when the internet returns. The portal shows them as they land.

1 minbetween metrics rollups
6days a batch waits for a link
0identifier fields on the wire
13event types, fixed by fixture

Why observability is hard past the edge of coverage

Most tooling assumes a live pipe. Agents on hosts stream metrics, logs, and traces to a backend in near real time. A drone mid-mission or a phone in a blackout produces its most useful events exactly when nothing can stream.

The SDK keeps the record on the device. Events are folded into batches the moment they happen and written to sealed storage. A device that spends three days off-link comes back with its three days intact.

Upload happens when a link exists. The uploader wakes when the internet transport comes up, sends up to eight batches, and goes back to sleep. Your app makes no lifecycle call and runs no agent.

The thirteen event types

The inventory is fixed. Each type carries exactly the fields below and nothing else, and a fixture in the SDK repository pins the bytes. Every other event the engine produces is dropped by name before its payload is read.

protocolMessage delivered, failed, deferred, relayedLatency, hop count, and transport on delivery. Reason and retry count on failure or deferral. Hops and remaining TTL on a frame relayed for someone else.Per message
protocolRelay promoted, demotedThis device started or stopped carrying traffic, with connection count and battery level, or the battery floor it stopped on.On change
routingRouting decisionDORS selected, switched, escalated, or re-scored a transport, with the winning score and a reason code.On decision
transportTransport state changedA transport moved between statuses, with the previous and current one.On change
deviceDevice capability changedBattery, charging, or relay role changed, with the fields that moved.On change
mlsDecryption failed, session missingAn inbound frame would not decrypt, or a send found no session. A failure kind and a context token, nothing more.Rate limited
metricsMetrics rollupOne row per minute folded from the 5-second frames: neighbours, pending acks, retry queue, time per transport, relay time, and send counts.Every minute
sessionSession summaryDuration, routing switches and escalations by reason, the median handshake latency, and how many sends used encryption.Per session

Inside a minute rollup

The rollup is the row most dashboards are built on. The SDK still samples a metrics frame every 5 seconds, but the frames never leave the device. Once a minute they are folded into one row that says what the device was connected through, who was nearby, what was waiting to go out, and whether it was carrying traffic for others.

Illustration: one minute rollup from unit-07 as a small dashboard. Transport at the end of the minute WiFi Direct, five neighbours on average, relaying for 41 seconds, two pending acknowledgments, time split across BLE, WiFi Direct, and no transport, and 18 sends attempted with 17 succeeded.

In code

TypeScript
// Nothing is collected until this call. Key and app id come from the developer portal.
await protocol.start();
await protocol.enableTelemetry({
  apiKey: 'mp_…',
  appId: 'app_…',
  appVersion: '1.4.2',
});

// Later: what has been buffered, sent, accepted, and dropped on this device.
const stats = await protocol.telemetryStats();

The native module owns the lifecycle. Going to the background closes the session and flushes it, and the next foreground opens a new one. The same call exists in Python and Rust.

How the pipe behaves

Off until you call itOpt-in

Before enableTelemetry succeeds the SDK collects, buffers, persists, and uploads nothing. An app that never calls it ships no telemetry.

Queued through outages64 batches, 4 MiB

Batches are sealed with the per-install key and survive restarts. A batch waits up to six days for a link, then is dropped and counted.

No identifiers on the wireFixed inventory

No event has a field for a message id, peer, group, or name. The one id the pipe reads, for handshake latency, is hashed in memory and never uploaded.

Metered and checkabletelemetryStats()

The service bills accepted events. The device keeps its own count of sent, accepted, and dropped, so the invoice can be reconciled against it.

Where it applies

Autonomous fleets
Post-mission analysis from batches captured while off-link and uploaded on return.
Industrial sites
Site operators get a record of what the mesh did through the outage.
Public sector
Humanitarian programs keep an operational record with no personal identifiers in it.
What it observes
The DORS transport, delivery, relay, and MLS session layers. Leaf nodes have no pipe of their own yet.

Hosted telemetry FAQ

Does telemetry change the privacy model?

No. Nothing is collected, buffered, or uploaded until your app calls enableTelemetry with a key from the developer portal. The event inventory is fixed and has no field for a message id, peer address, group id, username, file name, or message content, and a fixture in the SDK pins the exact bytes that leave the device.

What gets uploaded?

Fourteen event types: message delivered, failed, deferred, and relayed; relay promoted, demoted, and demoted on the battery floor; routing decisions; transport state changes; device capability changes; two MLS failure signals; a metrics rollup once a minute; and a session summary at each session boundary. Each carries only the numeric or token fields listed in the docs.

Where does it go?

To one Offline Protocol ingest endpoint fixed at build time, over TLS. Since SDK 0.30.0 the uploader trusts only the roots of the four certificate authorities the ingest's certificate can come from, compiled into the SDK, and never the device's trust store. The uploader follows no redirects, so that endpoint is the only place a batch can be sent. The developer portal turns the batches into dashboards and cross-app benchmarks.

Should we update the SDK if we use telemetry?

Yes, if your app calls enableTelemetry on SDK 0.26.0 to 0.29.0. Those versions trusted the whole bundled Mozilla root set, which still included an eMudhra root withdrawn after the October 2026 key compromise, so someone holding one of those keys on the network path could have read the telemetry API key and the batches. SDK 0.30.0 trusts only the roots the ingest uses. Apps that never enable telemetry are not affected.

Can we still read events in our own code?

Yes. onEvent still hands every protocol event to your app as it happens, with no telemetry enabled and no key. getTransportMetrics and getBleDiagnostics answer the same way. What the free API does not carry is the minute rollup or the session summary; those exist only in the hosted pipe.

What happens when the device is offline?

Events are cut into batches and persisted on the device, sealed with the same per-install key as the outbox. The queue holds up to 64 batches or 4 MiB and survives restarts. The uploader wakes when the internet transport comes back, and a batch older than six days is dropped unsent because the ingest deduplicates for seven.

What is in the minute rollup?

One row per minute, folded on the device from the metrics frames: neighbour count average, max, and median; pending acknowledgments; retry queue depth; how many milliseconds were spent on BLE, WiFi Direct, the internet, or nothing; how long the device relayed; the transport at the end of the minute; and sends attempted, succeeded, and failed.

How are identifiers handled?

No event has a field for one. The pipe reads a single identifier, the peer id on MLS records, only to pair the start and end of a handshake and compute a latency, and with scrubIds on by default it is hashed in memory first. Setting includeDeviceId stamps an opaque per-install id on each batch so the portal counts devices instead of sessions, and the privacy docs spell out the store disclosures that choice adds.

When does a batch leave the device?

Every 30 seconds when there is something to send, when the buffer passes 256 KiB, when the app goes to the background, when the internet comes back, and on flushTelemetry, endTelemetrySession, or disableTelemetry. Below 15 percent battery and not charging, the uploader defers everything except the background and explicit flushes.

How is it billed?

The service meters accepted events: 250,000 a month on Free, 2 million on Pro, and 10 million on Scale, then $0.05 per 1,000. telemetryStats() reports buffered, sent, accepted, and dropped counts on the device, so an invoice can be checked against what the SDK itself counted. setTelemetryEnabled(false) stops collection without shutting the uploader down, and disableTelemetry stops it with a final flush.

How do device events reach our own backend or Kafka?

Hosted telemetry sends the SDK's runtime events to Offline Protocol's ingest, and onEvent hands every protocol event to your own code as it happens. For your own records, such as readings and workflow events, the docs describe a retained-event pattern: the device commits each event durably, a collector accepts it over the mesh, and a destination adapter you build submits it to your backend and records the backend's acceptance. For Kafka, a Confluent source connector is in private evaluation and exports retained collector events through Kafka Connect; contact sales for access. Read the backend delivery guide.

See what your fleet does off-link. One call to turn it on.