---
title: "readRecords and ReadRecordsRequest: paging with pageSize and pageToken"
canonical: "https://aifitnessapi.com/health-connect-api/readrecords-pagination"
cluster: "Health Connect API"
primary_query: "ReadRecordsRequest"
last_reviewed: "2026-10-04"
description: "How Health Connect's readRecords pages results: ReadRecordsRequest defaults, the 1000-record pageSize, and why pageToken can come back as an empty string."
publisher: "AIFitnessAPI — independent, not sponsored"
cite_as: "\"readRecords and ReadRecordsRequest: paging with pageSize and pageToken\", AIFitnessAPI, https://aifitnessapi.com/health-connect-api/readrecords-pagination"
---

# readRecords and ReadRecordsRequest: paging with pageSize and pageToken

> HealthConnectClient.readRecords takes a ReadRecordsRequest (a record type, a TimeRangeFilter, an optional set of DataOrigin values, ascendingOrder, pageSize and pageToken) and returns one page of records plus a pageToken. The default pageSize is 1000; when more records match, you pass the returned pageToken into the next request to get the next page. Google's reference says the token is null when there are no more pages, but its read guide warns that some versions return an empty string instead, so end the loop on isNullOrEmpty(). For totals of cumulative types such as steps, Google says to use aggregate() rather than adding up raw records.

- Canonical: https://aifitnessapi.com/health-connect-api/readrecords-pagination
- Last reviewed: 2026-10-04
- Publisher: AIFitnessAPI (https://aifitnessapi.com) — independent, not sponsored
- Cite as: "readRecords and ReadRecordsRequest: paging with pageSize and pageToken", AIFitnessAPI, https://aifitnessapi.com/health-connect-api/readrecords-pagination

---

`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](https://developer.android.com/reference/kotlin/androidx/health/connect/client/request/ReadRecordsRequest), its [`ReadRecordsResponse` reference](https://developer.android.com/reference/kotlin/androidx/health/connect/client/response/ReadRecordsResponse) and the [read data guide](https://developer.android.com/health-and-fitness/health-connect/read-data), all fetched October 4, 2026. Setup and the permission request are covered in the [Google Health Connect integration guide](/integrate/google-health-connect).

## 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 `Record`s." 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](/error-codes/health-connect) 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:

```kotlin
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.pageToken` can return an empty string `""` instead of `null` when pagination is complete. To reliably check whether pagination is complete, use `isNullOrEmpty()` rather than checking only for `null`."

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

```kotlin
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](/fix/health-connect-rate-limit).

## 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](/health-connect-api/aggregate-request) |
| One value per hour, day or month | [aggregateGroupByDuration or aggregateGroupByPeriod](/health-connect-api/aggregategroupbyduration-vs-period) |
| Only what changed since your last sync, including deletions | [getChanges](/health-connect-api/getchanges-incremental-sync) |

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 `null` only.** On the versions Google's note describes, the last page carries `""`. A `!= null` check 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 `readRecords` results counts all of them. Ask [`aggregate`](/health-connect-api/aggregate-request) for 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. `getChanges` documents a range of 1 to 5000 for its own `pageSize`, 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 `dataOriginFilter` that leaves out the app that wrote the data. [Health Connect returns no data](/fix/health-connect-no-data) separates the common cases, including the default 30-day read limit, and [Health Connect SecurityException](/fix/health-connect-securityexception) covers the permission failures.

## FAQ

### 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.

[Permalink](https://aifitnessapi.com/health-connect-api/readrecords-pagination#faq-1)

### 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.

[Permalink](https://aifitnessapi.com/health-connect-api/readrecords-pagination#faq-2)

### 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.

[Permalink](https://aifitnessapi.com/health-connect-api/readrecords-pagination#faq-3)

### 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.

[Permalink](https://aifitnessapi.com/health-connect-api/readrecords-pagination#faq-4)
