readRecords and ReadRecordsRequest: paging with pageSize and pageToken
Last verified October 4, 2026 · 5 min read
Covered here:Health Connect
readRecords is the raw-records read in the Health Connect Jetpack client. It hands back records one page at a time, and the loop that walks those pages is where most of the bugs live. This page covers the request, the response and the paging loop, from Google's ReadRecordsRequest reference, its ReadRecordsResponse reference and the read data guide, all fetched October 4, 2026. Setup and the permission request are covered in the Google Health Connect integration guide.
The method and the request#
The declaration on HealthConnectClient is suspend fun <T : Record> readRecords(request: ReadRecordsRequest<T>): ReadRecordsResponse<T>, and Google's one-line summary is "Retrieves a collection of Records." The reference lists three exceptions: RemoteException "For any IPC transportation failures", SecurityException "For requests with unpermitted access" and IOException "For any disk I/O issues." The Health Connect error codes page collects every exception type the HealthConnectClient reference documents and counts the methods each appears on.
The request's constructor, with the defaults the reference shows:
ReadRecordsRequest(
recordType: KClass<T>,
timeRangeFilter: TimeRangeFilter,
dataOriginFilter: Set<DataOrigin> = emptySet(),
ascendingOrder: Boolean = true,
pageSize: Int = 1000,
pageToken: String? = null
)
| Parameter | What Google documents |
|---|---|
recordType | The record class to read, such as StepsRecord::class. |
timeRangeFilter | A TimeRangeFilter. Its between form is "inclusive-exclusive": records at the start time match, records at the end time do not. |
dataOriginFilter | A set of DataOrigin(packageName) values. Empty by default. The read guide uses setOf(DataOrigin("com.my.package.name")) to read an app's own earlier writes. |
ascendingOrder | Defaults to true. The reference gives no further description. |
pageSize | Defaults to 1000. |
pageToken | null for the first page, then the token from the previous response. |
The class description explains the paging contract: "Returned collection will contain a ReadRecordsResponse.pageToken if number of records exceeds pageSize. Use this if you expect an unbound number of records within specified time ranges."
Availability. The reference labels HealthConnectClient, ReadRecordsRequest and ReadRecordsResponse "Added in 1.1.0", in the androidx.health.connect:connect-client artifact. The readRecords method carries no label of its own.
The response, and the token that is not always null#
ReadRecordsResponse has two properties: records, "a collection of records", and pageToken: String?, which the reference describes as "an optional page token to use for ReadRecordsRequest.pageToken in the next request if more records can be fetched; contains value null if no more pages."
Google's read guide qualifies that in a note:
"Depending on the specific system or Health Connect APK version (such as on Android 12 and 13 or across specific IPC serialization boundaries),
ReadRecordsResponse.pageTokencan return an empty string""instead ofnullwhen pagination is complete. To reliably check whether pagination is complete, useisNullOrEmpty()rather than checking only fornull."
The guide's own paging sample, a few lines further down, still ends its loop with while (pageToken != null). Follow the note, not the sample.
A paging loop#
suspend fun readAllHeartRate(
client: HealthConnectClient,
start: Instant,
end: Instant
): List<HeartRateRecord> {
val all = mutableListOf<HeartRateRecord>()
var pageToken: String? = null
do {
val page = client.readRecords(
ReadRecordsRequest(
recordType = HeartRateRecord::class,
timeRangeFilter = TimeRangeFilter.between(start, end),
pageToken = pageToken
)
)
all += page.records
pageToken = page.pageToken
} while (!pageToken.isNullOrEmpty())
return all
}
Collecting everything into one list keeps the sketch short; for a long range, process each page as it arrives. Google recommends using pageToken "to retrieve all available data from the requested time period", and in the same breath warns to "be careful to avoid rate-limiting concerns." Its sample wraps the loop in a catch (quotaError: IllegalStateException) with a comment saying to back off. Retrying from the failed page is covered in Health Connect rate limit exceeded.
Reading one record by id#
If you stored a record's Metadata.id, you do not need a range query. readRecord(recordType, recordId) "Reads one Record point with its recordType and recordId", and the reified extension readRecord<T>(recordId) does the same with the type as a type parameter. The reference notes that an invalid identifier "will result in IPC failure", a RemoteException.
When readRecords is the wrong call#
| You need | Use instead |
|---|---|
| A total, average, minimum or maximum over a window | aggregate and AggregateRequest |
| One value per hour, day or month | aggregateGroupByDuration or aggregateGroupByPeriod |
| Only what changed since your last sync, including deletions | getChanges |
Google is direct about the first row: "For cumulative types like StepsRecord, use aggregate() instead of readRecords() to avoid double counting from multiple sources and improve accuracy."
Pitfalls#
- Ending the loop on
nullonly. On the versions Google's note describes, the last page carries"". A!= nullcheck keeps looping and sends an empty string back as the page token. Google does not document what Health Connect returns for that request;isNullOrEmpty()avoids finding out. - Adding up raw step records. Several apps can write steps for the same minutes, and a loop over
readRecordsresults counts all of them. Askaggregatefor the total. - Changing the request between pages. Google does not document what a page token means if the time range, filters, order or page size change between calls. Its sample changes only
pageToken; do the same. - Assuming a maximum
pageSize. The reference shows the default of 1000 and no upper bound for reads.getChangesdocuments a range of 1 to 5000 for its ownpageSize, which says nothing about this one. - Reading the sort order into
ascendingOrder. The reference documents only its default. It does not say which timestamp the ordering uses, so if order matters, check it against your data. - An empty page is not an error. No records can mean nothing was written in the window or a
dataOriginFilterthat leaves out the app that wrote the data. Health Connect returns no data separates the common cases, including the default 30-day read limit, and Health Connect SecurityException covers the permission failures.
Frequently asked questions
- What is the default pageSize of a Health Connect ReadRecordsRequest?
- 1000. Google's ReadRecordsRequest reference shows pageSize defaulting to 1000, and the read guide says that if the number of records exceeds the request's pageSize, you need to iterate over all pages using pageToken to retrieve everything. The reference does not document a maximum pageSize for reads, so treat larger values as untested rather than supported.
- Why does ReadRecordsResponse.pageToken come back as an empty string instead of null?
- Because some versions return it that way. Google's read guide notes that depending on the system or Health Connect APK version, such as on Android 12 and 13 or across specific IPC serialization boundaries, pageToken can be an empty string instead of null when pagination is complete. Google's advice is to check with isNullOrEmpty() rather than only for null, even though the reference itself still describes null as the end-of-pages value.
- Should I sum StepsRecord values from readRecords to get a daily step total?
- No. Google's read guide says that for cumulative types like StepsRecord you should use aggregate() instead of readRecords() to avoid double counting from multiple sources and improve accuracy. Several apps and devices can write steps for the same period, and summing the raw records counts each of them, while the aggregate API applies Health Connect's own handling of duplicate data.
- Does TimeRangeFilter.between include a record that starts exactly at the end time?
- No. Google's TimeRangeFilter reference describes between as inclusive-exclusive: a record at the start time is included and one at the end time is not. The before form treats its end time as exclusive and the after form treats its start time as inclusive. Adjacent windows built this way, such as midnight to midnight, therefore do not share a boundary instant.
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 4, 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 health connect api · by AIFitnessAPI