---
title: "Health Connect ERROR_RATE_LIMIT_EXCEEDED: Quota Errors and How to Back Off"
canonical: "https://aifitnessapi.com/fix/health-connect-rate-limit"
cluster: "Troubleshooting"
primary_query: "health connect rate limit exceeded"
last_reviewed: "2026-10-03"
description: "Android defines ERROR_RATE_LIMIT_EXCEEDED as the caller exhausting its rate limit. How it reaches Jetpack code, what Google documents, how to back off."
publisher: "AIFitnessAPI — independent, not sponsored"
cite_as: "\"Health Connect ERROR_RATE_LIMIT_EXCEEDED: Quota Errors and How to Back Off\", AIFitnessAPI, https://aifitnessapi.com/fix/health-connect-rate-limit"
---

# Health Connect ERROR_RATE_LIMIT_EXCEEDED: Quota Errors and How to Back Off

> ERROR_RATE_LIMIT_EXCEEDED is a Health Connect platform error code (value 7), which Android describes as "The caller exhausted the allotted rate limit." Through the Jetpack library on Android 14 and later it reaches your code as an IllegalStateException with the platform HealthConnectException as its cause, and Google's own read sample catches IllegalStateException as a quota error and backs off. Google documents periodic and daily limits on reads and writes, with stricter limits in the background, but publishes no numbers. Retry the failed page or batch after a delay rather than restarting, and use aggregation and the changelog to make fewer calls.

- Canonical: https://aifitnessapi.com/fix/health-connect-rate-limit
- Last reviewed: 2026-10-03
- Publisher: AIFitnessAPI (https://aifitnessapi.com) — independent, not sponsored
- Cite as: "Health Connect ERROR_RATE_LIMIT_EXCEEDED: Quota Errors and How to Back Off", AIFitnessAPI, https://aifitnessapi.com/fix/health-connect-rate-limit

---

A large Health Connect read or a bulk insert starts failing partway through with `java.lang.IllegalStateException`, and when you dig into the cause you find `android.health.connect.HealthConnectException` with error code 7. That code is `ERROR_RATE_LIMIT_EXCEEDED`. Google documents that the limits exist and how to stay under them. It does not publish the numbers. Code 7 and the other `HealthConnectException` constants are listed, with Google's description of each, on [the Health Connect error codes page](/error-codes/health-connect).

## What Google documents

Android's [platform `HealthConnectException` reference](https://developer.android.com/reference/android/health/connect/HealthConnectException) (fetched October 3, 2026) defines `ERROR_RATE_LIMIT_EXCEEDED`, constant value 7, added in API level 34, as "The caller exhausted the allotted rate limit."

Google's [rate-limiting guide](https://developer.android.com/health-and-fitness/health-connect/rate-limiting) describes how the limits are structured:

- **Reads and changelog:** "A periodic limit on the number of API calls your app can make to the API" and "A daily limit on the number of API calls your app can make."
- **Insert, update and delete:** a periodic limit, a daily limit, "A memory limit for bulk insertions" and "A memory limit for single record insertions."
- **Background:** "background rate limiting is stricter than foreground rate limiting."

The guide says limits "are variable based on the type of operation your app is performing, and whether that operation occurs in the foreground or background." It gives no figures. If a blog post quotes a number of calls per minute, that number does not come from this guide.

## What your code actually catches

Through the Jetpack library you will not see `HealthConnectException` as the thrown type. The library's source ([`ExceptionConverter.kt`](https://github.com/androidx/androidx/blob/androidx-main/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/ExceptionConverter.kt), read October 3, 2026) maps four platform codes to standard exceptions: I/O to `IOException`, remote to `RemoteException`, security to `SecurityException`, invalid argument to `IllegalArgumentException`. Every other code, rate limiting included, becomes `IllegalStateException(this)` with the platform exception as its cause. That file only covers the platform path, which needs API 34 or higher. We did not verify how the Android 13 and lower app path reports a rate limit, and the mapping is internal code that can change.

Google's own read sample agrees on the type. Its paging example in the [read-data guide](https://developer.android.com/health-and-fitness/health-connect/read-data) ends in `catch (quotaError: IllegalStateException)` with the comment `// Backoff`.

`IllegalStateException` is not only a quota signal. Google's exception table lists two other meanings: "The Health Connect service isn't available" and "The request isn't a valid construction." A request that is badly built fails the same way on every retry, so check the cause before you back off:

```kotlin
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
fun platformErrorCode(e: IllegalStateException): Int? =
    (e.cause as? HealthConnectException)?.errorCode

fun isWorthRetrying(e: IllegalStateException): Boolean {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return true // cause not inspectable here
    return when (platformErrorCode(e)) {
        HealthConnectException.ERROR_RATE_LIMIT_EXCEEDED,
        HealthConnectException.ERROR_DATA_SYNC_IN_PROGRESS -> true
        else -> false
    }
}
```

`ERROR_DATA_SYNC_IN_PROGRESS` (code 8) also belongs in the retry bucket. Android describes it as "Data sync is in progress. Data read and writes are blocked. Caller should try this api call again later."

## Back off and resume from the same page

Google's read guide notes that `ReadRecordsRequest` has "a default pageSize value of 1000" and warns, when paging, to "be careful to avoid rate-limiting concerns." When a page fails, retry that page with the same `pageToken`. Do not start the whole read again.

```kotlin
suspend fun readAllHeartRate(
    client: HealthConnectClient,
    start: Instant,
    end: Instant,
    sink: suspend (List<HeartRateRecord>) -> Unit
) {
    var pageToken: String? = null
    var waitMs = 1_000L // illustrative, not a Google figure
    do {
        try {
            val page = client.readRecords(
                ReadRecordsRequest(
                    recordType = HeartRateRecord::class,
                    timeRangeFilter = TimeRangeFilter.between(start, end),
                    pageToken = pageToken
                )
            )
            sink(page.records)
            pageToken = page.pageToken
            waitMs = 1_000L
        } catch (e: IllegalStateException) {
            if (!isWorthRetrying(e) || waitMs > 64_000L) throw e
            delay(waitMs) // same pageToken: resume, don't restart
            waitMs *= 2
        }
    } while (!pageToken.isNullOrEmpty())
}
```

The delays are ours. Google publishes no retry interval, so treat them as a starting point to tune against your own logs. For a sync job that must give up and continue later, persist `pageToken` along with the records you have stored, and pick up from it next run. That pattern is covered in [historical backfill](/architecture/historical-backfill).

## Writes: retry from the failure, not from zero

Google's rate-limiting guide is direct about write failures: "we recommend retrying from where the exception occurred. Don't delete all the data in question and retry the entire write request. This approach eats into your insert quota, reduces performance, and has a negative impact on battery life."

Batch size sets how much you redo. Android's reference says `insertRecords` runs "in a transaction - if one fails, none is inserted", so a failed batch is all-or-nothing and the retry unit is the batch. Our recommendation is to write in moderate batches and record which batches have committed, so a retry resends one batch instead of the whole export. The memory limits on bulk and single insertions are a further reason to avoid one huge `insertRecords` call.

## Spend fewer calls in the first place

Google names three habits that lower the risk:

1. **Aggregate instead of summing raw reads.** The read guide says the aggregation API "lessens the chances of rate limiting" and handles duplicate records.
2. **Use the changelog for sync.** The rate-limiting guide: "you should utilize changelog handling to synchronize your database with data from Health Connect, rather than over-relying on raw read requests." Changes tokens expire, which is covered in [changesTokenExpired](/fix/health-connect-changes-token-expired).
3. **Do less in the background.** Background limits are stricter, so leave heavy backfills for foreground sessions where you can. The scheduling trade-offs are in [background sync](/architecture/background-sync).

## Where to go next

If the failing call threw `SecurityException` rather than `IllegalStateException`, see [Health Connect SecurityException](/fix/health-connect-securityexception). For a cloud API's HTTP 429 rather than an on-device quota, the [Fitbit 429 guide](/fix/fitbit-api-429-rate-limit) covers the server-side version of this problem.

## Sources

- [developer.android.com/reference/android/health/connect/HealthConnectException](https://developer.android.com/reference/android/health/connect/HealthConnectException) — ERROR_RATE_LIMIT_EXCEEDED (7), ERROR_DATA_SYNC_IN_PROGRESS (8), ERROR_SECURITY and related codes (checked 2026-10-03)
- [developer.android.com/health-and-fitness/health-connect/rate-limiting](https://developer.android.com/health-and-fitness/health-connect/rate-limiting) — periodic, daily and memory limits; background stricter; retry from where the exception occurred; changelog over raw reads; no figures published (checked 2026-10-03)
- [developer.android.com/health-and-fitness/health-connect/read-data](https://developer.android.com/health-and-fitness/health-connect/read-data) — quotaError: IllegalStateException sample; default pageSize 1000; aggregation lessens rate limiting; IllegalStateException table row (checked 2026-10-03)
- [developer.android.com/health-and-fitness/health-connect/write-data](https://developer.android.com/health-and-fitness/health-connect/write-data) — same exception table on the write side (checked 2026-10-03)
- [developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient](https://developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient) — insertRecords runs in a transaction: if one fails, none is inserted (checked 2026-10-03)
- [github.com/androidx/androidx/blob/androidx-main/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/ExceptionConverter.kt](https://github.com/androidx/androidx/blob/androidx-main/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/ExceptionConverter.kt) — Jetpack maps unmapped platform codes, including rate limiting, to IllegalStateException(this) (checked 2026-10-03)

## FAQ

### What are Health Connect's actual rate limit numbers?

Google does not publish them. Its rate-limiting guide, checked October 3, 2026, describes periodic and daily limits on reads and changelog calls, periodic, daily and memory limits on inserts, updates and deletes, and stricter limits in the background, but gives no figures. Build a loop that backs off when it is limited instead of tuning to a number found elsewhere.

[Permalink](https://aifitnessapi.com/fix/health-connect-rate-limit#faq-1)

### Which exception does the Jetpack client throw when Health Connect rate-limits my app?

IllegalStateException. On Android 14 and later the Jetpack library's source converts every platform error code it does not map to IOException, RemoteException, SecurityException or IllegalArgumentException into IllegalStateException, with the platform HealthConnectException as the cause. Google's read sample catches IllegalStateException as a quota error. Check the cause's error code before retrying, because an invalid request throws the same type.

[Permalink](https://aifitnessapi.com/fix/health-connect-rate-limit#faq-2)

### Why does my Health Connect background sync get rate-limited before foreground reads do?

Google's rate-limiting guide says background rate limiting is stricter than foreground rate limiting, because background work costs battery and raises privacy questions. Move heavy backfills to foreground sessions where you can, keep background jobs small, and use changes tokens instead of repeated raw reads.

[Permalink](https://aifitnessapi.com/fix/health-connect-rate-limit#faq-3)

### Should I retry a failed Health Connect batch insert from the beginning?

No. Google's guide recommends retrying from where the exception occurred, and warns that deleting the data and retrying the entire write eats into your insert quota. Since insertRecords is a transaction where one failure means nothing in that call is inserted, record which batches committed and resend only the failed one.

[Permalink](https://aifitnessapi.com/fix/health-connect-rate-limit#faq-4)
