PassiveMonitoringClient and MeasureClient on Wear OS
Last verified October 3, 2026 · 6 min read
Which Health Services client fits which product is introduced on Wear OS Health Services. This page is the code for the two non-exercise clients, from Google's guides "Passive data updates" and "Take spot health measurements with MeasureClient" and the Jetpack reference, read on 2026-10-03. The workout path is ExerciseClient in Kotlin.
Choosing between the three clients#
Google's own descriptions draw the lines:
| Client | Google's description | Use it for |
|---|---|---|
PassiveMonitoringClient | "suited for long-lived experiences where data updates are relatively infrequent" | All-day steps, daily goals, background heart rate trends, sleep state |
MeasureClient | "suited for short-lived experiences, such as while the user looks at your app UI" | A heart rate readout on one screen |
ExerciseClient | manage a workout, set goals, rapid updates "as long as the exercise belongs to your app" | A workout the user started |
Google's MeasureClient guide is blunt about the boundary: "MeasureClient is not suitable for workout tracking." The reference says the same about background use.
PassiveMonitoringClient: configure once, choose a channel#
val passiveClient = HealthServices.getClient(context).passiveMonitoringClient
val capabilities = passiveClient.getCapabilitiesAsync().await()
val canHeartRate = DataType.HEART_RATE_BPM in capabilities.supportedDataTypesPassiveMonitoring
val canStepGoal = DataType.STEPS_DAILY in capabilities.supportedDataTypesPassiveGoals
val config = PassiveListenerConfig.builder()
.setDataTypes(setOf(DataType.HEART_RATE_BPM, DataType.STEPS_DAILY))
.build()
The reference says Health Services "will typically reject requests made for DataTypes which are not supported," so the capability check is not optional.
There are two delivery channels, and the reference says they operate independently, with "each channel" receiving "its own stream of data."
A service, for when your app is not running.
class PassiveDataService : PassiveListenerService() {
override fun onNewDataPointsReceived(dataPoints: DataPointContainer) {
// write to Room; do not do network work here
}
}
passiveClient.setPassiveListenerServiceAsync(PassiveDataService::class.java, config).await()
Declare it in the manifest with android:permission="com.google.android.wearable.healthservices.permission.PASSIVE_DATA_BINDING" and android:exported="true". The reference explains the permission: it is there "in order to ensure that Health Services is the source of the binding." Service data "will be batched", though "Any requested goal, user activity, or health event updates will not be batched."
A callback, for while your app is alive.
passiveClient.setPassiveListenerCallback(config, object : PassiveListenerCallback {
override fun onNewDataPointsReceived(dataPoints: DataPointContainer) { }
override fun onRegistrationFailed(throwable: Throwable) { }
})
Callback updates "are sent on generation (they will not be batched)", but if the callback cannot be notified, "the request will automatically be unregistered."
Both registrations are "unique per subscribing app. Subsequent registrations will replace the previous registration." That one line causes real bugs: if a tile registers heart rate and a settings screen later registers only steps, heart rate silently stops. Build one config from the union of what every feature needs, in one place.
To stop, call clearPassiveListenerServiceAsync() or clearPassiveListenerCallbackAsync(). The reference warns that "Data will not be delivered after this call," so call flushAsync() first if you need pending batches. Flushes are "subject to throttling by Health Services."
Order by timestamp, not by arrival#
Because passive data is batched, Google says you may receive several types, or several points of one type, in one batch, and should "Use the timestamps included within these objects rather than the time they were received by your app." Point times are relative to boot. Convert with Instant.ofEpochMilli(System.currentTimeMillis() - SystemClock.elapsedRealtime()) and pass the result to getStartInstant() or getEndInstant(). Day boundaries then become your problem; see time zones and day boundaries.
Registrations do not survive a reboot#
Google states it directly: "Passive data registrations don't persist across reboots." The documented fix is a BroadcastReceiver for ACTION_BOOT_COMPLETED that does not register directly but enqueues a WorkManager job, because "Health Services might take 10 seconds or more to acknowledge a passive data registration request, and this might exceed the allowable execution time of a BroadcastReceiver." The manifest needs android.permission.RECEIVE_BOOT_COMPLETED and the receiver's intent filter. Check permissions inside the worker before re-registering; a user may have revoked them while the watch was off.
Daily types, passive goals and activity state#
The _DAILY data types are the passive client's natural unit. Google's reference defines each with the same boundary: for example STEPS_DAILY is "The total step count over a day, where the previous day ends and a new day begins at 12:00 AM local time." The same midnight-local-time boundary appears in the definitions of CALORIES_DAILY, DISTANCE_DAILY, FLOORS_DAILY and ELEVATION_GAIN_DAILY.
A passive goal wraps a DataTypeCondition:
val dailySteps = PassiveGoal(
DataTypeCondition(
dataType = DataType.STEPS_DAILY,
threshold = 10_000,
comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
)
)
val goalConfig = PassiveListenerConfig.builder()
.setDailyGoals(setOf(dailySteps))
.build()
// override onGoalCompleted(goal: PassiveGoal) in the service or callback
For high-level user state, request ACTIVITY_RECOGNITION, call setShouldUserActivityInfoBeRequested(true) on the builder, and override onUserActivityInfoReceived. Google's sample reads UserActivityState.USER_ACTIVITY_ASLEEP and comments that stateChangeTime "may be in the past!"
Background heart rate needs a background permission#
Google's permissions page sets the rule by target API level. If your app targets API level 36 or higher and "uses PassiveMonitoringClient to access body sensor information in the background, request the READ_HEALTH_DATA_IN_BACKGROUND permission." For targets between 33 and 35 inclusive, "request both the BODY_SENSORS and BODY_SENSORS_BACKGROUND permissions instead." Steps, calories and distance types sit under ACTIVITY_RECOGNITION.
MeasureClient: register, read, unregister#
val measureClient = HealthServices.getClient(context).measureClient
val supported = DataType.HEART_RATE_BPM in
measureClient.getCapabilitiesAsync().await().supportedDataTypesMeasure
val callback = object : MeasureCallback {
override fun onAvailabilityChanged(dataType: DeltaDataType<*, *>, availability: Availability) {
// e.g. not on the wrist
}
override fun onDataReceived(data: DataPointContainer) {
// render the latest HEART_RATE_BPM point
}
}
// when the screen appears
measureClient.registerMeasureCallback(DataType.HEART_RATE_BPM, callback)
// when it disappears
measureClient.unregisterMeasureCallbackAsync(DataType.HEART_RATE_BPM, callback)
Facts from the reference that shape this code:
- It takes a
DeltaDataType, one per callback, so aggregate types likeHEART_RATE_BPM_STATSare not what it is for. - Registering "is expected to increase the sample rate on the associated sensor(s)," and the callback runs "until the app is killed or
unregisterMeasureCallbackAsyncis called." Tie registration to the screen's lifecycle. Google's guide says it is "important to minimize the amount of time that your callback is registered." - The callback runs on the main thread unless you pass an
Executor. - Even live capture "can still be sent out in batches depending on the application processor state."
- "Existing subscriptions made with the
PassiveMonitoringClientare also expected to get the data generated by this client." A spot reading also lands in your own passive stream, which matters if you count readings.
Where the data goes next#
Passive data is the natural feed for a tile; Wear OS tiles covers rendering from a local cache. Battery trade-offs between the three clients are on watch app battery. If you also write the readings to the phone-side store, the record types are in Health Connect records.
Frequently asked questions
- Do Health Services passive data registrations survive a watch reboot?
- No. Google's Passive data updates guide states that passive data registrations do not persist across reboots. Its documented fix is a BroadcastReceiver for ACTION_BOOT_COMPLETED that enqueues a WorkManager worker to re-register, rather than registering inside the receiver, because Health Services might take 10 seconds or more to acknowledge a registration at startup, which can exceed a BroadcastReceiver's allowed execution time. Declare RECEIVE_BOOT_COMPLETED in the manifest, and check permissions inside the worker before registering again.
- Why did my PassiveMonitoringClient stop delivering heart rate after I registered for steps?
- Because a new registration replaces the old one. Google's reference for setPassiveListenerServiceAsync and setPassiveListenerCallback says each registration is unique per subscribing app and that subsequent registrations replace the previous one, and that data stops for a type when you register again without it. If two features register separately, the last one wins. Build a single PassiveListenerConfig from the union of all the data types and goals your app needs, and register it from one place.
- Does a MeasureClient heart rate reading also reach my passive listener?
- Google's MeasureClient reference says existing subscriptions made with PassiveMonitoringClient are also expected to get the data generated by this client. So a spot reading taken on screen can appear again in your PassiveListenerService or callback. If you count readings or store both streams, deduplicate by data point timestamp, which Google says you should use for ordering anyway, rather than assuming the two channels never overlap.
- What permission does background heart rate need on Wear OS 6?
- Google's Health Services permissions page says that if your app targets API level 36 or higher and uses PassiveMonitoringClient to access body sensor information in the background, it should request READ_HEALTH_DATA_IN_BACKGROUND, alongside READ_HEART_RATE for heart rate itself. Apps targeting API levels 33 through 35 inclusive request BODY_SENSORS and BODY_SENSORS_BACKGROUND instead. The migration steps cap the legacy permissions with android:maxSdkVersion set to 35, so one manifest can serve both ranges.
Keep reading
Elsewhere on the site
Pages that share this one’s concepts and sources, from other sections.
Next steps
Was this page useful?
Independent comparison, last reviewed October 3, 2026. Pricing, rate limits, and feature availability change often — confirm current details in each provider’s official documentation before you commit. Product and company names are trademarks of their respective owners; AIFitnessAPI is not affiliated with, endorsed by, or sponsored by any product listed here.
← All watch apps · by AIFitnessAPI