Skip to content
AF
Troubleshooting

Health Connect ERROR_RATE_LIMIT_EXCEEDED: Quota Errors and How to Back Off

Last verified October 3, 2026 · 5 min read

Sources checked (6) · October 3, 2026
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.

Covered here:Health Connect

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.

What Google documents#

Android's platform HealthConnectException reference (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 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, 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 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:

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

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.

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

Where to go next#

If the failing call threw SecurityException rather than IllegalStateException, see Health Connect SecurityException. For a cloud API's HTTP 429 rather than an on-device quota, the Fitbit 429 guide covers the server-side version of this problem.

Frequently asked questions

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

All 124 questions in Troubleshooting →

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 troubleshooting · by AIFitnessAPI