microg/GmsCore · warning · RuntimeException

duration limit reached (active for ${(SystemClock.elapsedRea

Error message

duration limit reached (active for ${(SystemClock.elapsedRealtime() - start).formatDuration()}, duration ${request.durationMillis.formatDuration()})

What it means

LocationRequestHolder.check() throws RuntimeException('duration limit reached (active for X, duration Y)') when the request's expirationDuration has elapsed: timePendingMillis went negative once the holder's active time exceeded request.durationMillis. It enforces the client-requested duration limit (setExpirationDuration) before delivering further locations; the exception is caught upstream to cancel the holder.

Source

Thrown at play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt:457

            }

            fun start(): LocationRequestHolder {
                if (!context.checkAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission")
                return this
            }

            fun cancel() {
                try {
                    callback?.cancel()
                } catch (e: Exception) {
                    Log.w(TAG, e)
                }
            }

            fun check() {
                if (!context.checkAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission")
                if (effectiveGranularity > permissionGranularity) throw RuntimeException("Lack of permission")
                if (timePendingMillis < 0) throw RuntimeException("duration limit reached (active for ${(SystemClock.elapsedRealtime() - start).formatDuration()}, duration ${request.durationMillis.formatDuration()})")
                if (updatesPending <= 0) throw RuntimeException("max updates reached")
                if (callback?.asBinder()?.isBinderAlive == false) throw RuntimeException("Binder died")
            }

            fun processNewLocation(location: Location): Boolean {
                check()
                if (lastLocation != null && location.elapsedMillis - lastLocation!!.elapsedMillis < request.minUpdateIntervalMillis) return false
                if (lastLocation != null && location.distanceTo(lastLocation!!) < request.minUpdateDistanceMeters) return false
                if (lastLocation == location) return false
                val returnedLocation = if (effectiveGranularity > permissionGranularity) {
                    throw RuntimeException("Lack of permission")
                } else {
                    if (!context.noteAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) {
                        throw RuntimeException("app op denied")
                    } else if (clientIdentity.isSelfProcess()) {
                        Location(location)
                    } else {
                        Location(location).apply { provider = "fused" }

View on GitHub (pinned to 157c9d86ac)

Solutions

  1. Increase the expiration via LocationRequest.Builder#setExpirationDuration with a value covering the session, or omit it for continuous updates.
  2. Re-register the location request when the expiration elapses (handle the Status failure in the callback and restart updates).
  3. If long-term tracking is needed, implement your own periodic re-request loop instead of one long-duration request.

Example fix

// before
val request = LocationRequest.Builder(priority, 1000)
    .setExpirationDuration(60_000) // expires after 1 min
    .build()
// after
val request = LocationRequest.Builder(priority, 1000)
    .setExpirationDuration(TimeUnit.HOURS.toMillis(2)) // or don't set expiration
    .build()
Defensive patterns

Strategy: retry

Validate before calling

val duration = request.durationMillis
if (duration in 1..(elapsedSinceStart)) {
    // request already expired — rebuild it before registering
    request = rebuildWithFreshExpiration()
}

Type guard

fun isStillValid(holder: LocationRequestHolder): Boolean = holder.timePendingMillis > 0

Try / catch

// expiration failures surface as a failed status/callback in the client API
callback = object : LocationCallback() {
    override fun onLocationAvailability(a: LocationAvailability) {
        if (a != null && !a.isLocationAvailable) { reRegisterRequest() }
    }
}

Prevention

When it happens

Trigger: A location request created with setExpirationDuration(millis) has been active longer than that duration and another location arrives, triggering processNewLocation -> check(); also triggered when a request is registered without clearing a previously set expiration that has already passed.

Common situations: Long-running navigation sessions exceeding the expiration set on the LocationRequest; reusing a stale LocationRequest object whose duration was set at build time; clock/elapsedRealtime edge cases after deep sleep causing immediate expiry.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of microg/GmsCore@157c9d86ac (2026-09-06). Data as JSON: /api/errors/5da7ebbd42d50cd6. Report an issue: GitHub.