{"record":{"id":"966d1e5b23600987","repo":"microg/GmsCore","slug":"max-updates-reached-966d1e","errorCode":null,"errorMessage":"max updates reached","messagePattern":"max updates reached","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt","lineNumber":458,"sourceCode":"\n            fun start(): LocationRequestHolder {\n                if (!context.checkAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException(\"Lack of permission\")\n                return this\n            }\n\n            fun cancel() {\n                try {\n                    callback?.cancel()\n                } catch (e: Exception) {\n                    Log.w(TAG, e)\n                }\n            }\n\n            fun check() {\n                if (!context.checkAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException(\"Lack of permission\")\n                if (effectiveGranularity > permissionGranularity) throw RuntimeException(\"Lack of permission\")\n                if (timePendingMillis < 0) throw RuntimeException(\"duration limit reached (active for ${(SystemClock.elapsedRealtime() - start).formatDuration()}, duration ${request.durationMillis.formatDuration()})\")\n                if (updatesPending <= 0) throw RuntimeException(\"max updates reached\")\n                if (callback?.asBinder()?.isBinderAlive == false) throw RuntimeException(\"Binder died\")\n            }\n\n            fun processNewLocation(location: Location): Boolean {\n                check()\n                if (lastLocation != null && location.elapsedMillis - lastLocation!!.elapsedMillis < request.minUpdateIntervalMillis) return false\n                if (lastLocation != null && location.distanceTo(lastLocation!!) < request.minUpdateDistanceMeters) return false\n                if (lastLocation == location) return false\n                val returnedLocation = if (effectiveGranularity > permissionGranularity) {\n                    throw RuntimeException(\"Lack of permission\")\n                } else {\n                    if (!context.noteAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) {\n                        throw RuntimeException(\"app op denied\")\n                    } else if (clientIdentity.isSelfProcess()) {\n                        Location(location)\n                    } else {\n                        Location(location).apply { provider = \"fused\" }\n                    }","sourceCodeStart":440,"sourceCodeEnd":476,"githubUrl":"https://github.com/microg/GmsCore/blob/157c9d86ac46c195a86c2f15ab55c84036223f95/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt#L440-L476","documentation":"microG's LocationRequestManager tracks how many location updates a client request is still entitled to (updatesPending, derived from the request's update budget). Each incoming location is validated by check() before delivery. When the request's update count is exhausted (updatesPending <= 0), delivering further updates would violate the client's LocationRequest contract, so a RuntimeException 'max updates reached' is thrown.","triggerScenarios":"A client registered a LocationRequest with a bounded number of updates (e.g. setNumUpdates(n) or a duration-limited request) and all n updates have already been delivered; a new location arrives and processNewLocation() calls check() which sees updatesPending <= 0.","commonSituations":"Apps that forgot the update count is cumulative across the request lifetime; reusing a stale LocationRequest after its budget ran out; requesting numUpdates=1 expecting continuous updates; duration-limited foreground-only requests kept alive after expiry.","solutions":["Verify the LocationRequest's numUpdates is set high enough (or left unlimited) for the app's use case","Call setNumUpdates(Integer.MAX_VALUE) if continuous updates are intended instead of a fixed count","Re-register a fresh LocationRequest via FusedLocationProviderClient.requestLocationUpdates once the old one's budget is spent","Check for duplicate request registrations that drain the shared update budget faster than expected","Remove the listener with removeLocationUpdates when done so exhausted requests are not re-invoked"],"exampleFix":"// before\nLocationRequest.Builder(builder)\n    .setNumUpdates(1)\n    .build() // throws on the 2nd incoming location\n// after\nLocationRequest.Builder(builder)\n    .setNumUpdates(10) // or Integer.MAX_VALUE for continuous\n    .build()","handlingStrategy":"try-catch","validationCode":"// compute remaining updates before registering\nval updates = request.numUpdates\nif (updates <= 0) throw IllegalArgumentException(\"LocationRequest already exhausted; create a new request\")","typeGuard":"fun LocationRequest.hasUpdateBudget(): Boolean = numUpdates > 0 || durationPending","tryCatchPattern":"try {\n    client.requestLocationUpdates(request, listener, looper)\n} catch (e: RuntimeException) {\n    if (e.message == \"max updates reached\") {\n        client.requestLocationUpdates(request.toUnboundedBuilder().build(), listener, looper)\n    } else throw e\n}","preventionTips":["Set numUpdates explicitly and high enough (Integer.MAX_VALUE for continuous updates)","Treat numUpdates as cumulative over the request lifetime, not per session","Recreate the LocationRequest when resuming instead of reusing an expired one","Remove listeners when finished to avoid draining shared budgets"],"tags":["location","android","microg","request-lifecycle"],"backgroundTag":"invalid-argument-value","analyzedSha":"157c9d86ac46c195a86c2f15ab55c84036223f95","analyzedAt":"2026-09-06T17:27:33.892Z","contentChangedAt":"2026-09-06T17:27:33.892Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}