Describe the bug
If the Play billing service disconnects while a BillingClient.startConnection() attempt is already in flight, the SDK's reconnection ladder is never re-armed and never runs again. From that state:
- The in-flight connection attempt is killed with the service, so
onBillingSetupFinished(OK) never arrives for it.
- A concurrent
startConnection call during that window gets response code 5 ("Client is already in the process of connecting to billing service."). BillingWrapper.onBillingSetupFinished handles code 5 (DEVELOPER_ERROR) with a deliberate no-op ("Billing service is already trying to connect. Don't do anything."), relying on the in-flight attempt to complete. It never does.
BillingWrapper.onBillingServiceDisconnected() only logs and tracks a diagnostics event. It schedules nothing.
Result: no code path ever calls startConnection again. The exponential backoff machinery (RECONNECT_TIMER_START_MILLISECONDS = 1s with the 15 minute cap in BillingWrapper, RETRY_TIMER_START = 878ms in BillingClientUseCase) never engages because nothing schedules a retry from this state. Queued service requests (pending_request_count=2 in the SDK's own diagnostics) wait indefinitely, so Purchases.getOfferings() pends silently: no error, no timeout, no retry, against a Play Store that is healthy again seconds later. The only release we have observed is an app background/foreground cycle, whose lifecycle hook calls startConnection and drains the queue within seconds.
We hit this in production behind a paywall: getOfferings() pends indefinitely with no callback, the user sees a spinner forever. The likely production trigger is Play Store churn (for example a Play self-update) while a billing operation is in flight.
-
Environment
- Platform: Android. We consume the SDK from a Flutter app in production via purchases_flutter 10.8.0 / purchases-hybrid-common 18.29.0, but the defect is entirely in the native Android SDK and reproduces at the native layer.
- SDK version: reproduced identically on purchases-android 10.16.0 and 10.16.1 (BillingClient 8.3.0).
- OS version: Android 17 (physical Google Pixel 10a, build CP2A.260705.006) and Android 16 (stock emulator with a Google Play image, no Google account signed in; the wedge is store-account-independent).
- Android Studio version: n/a (Gradle CLI builds).
- How widespread: low frequency, high severity. Our fleet telemetry shows roughly one silent paywall stall per week in this failure class. Once wedged, the device does not recover until the app is backgrounded and foregrounded.
-
Debug logs
Verbose SDK logs are forwarded through our Flutter log handler; the payloads are the SDK's own lines. Wedge anatomy from the physical Pixel 10a on 10.16.0 (device local time):
08-08 08:44:06.980 RC debug: Starting connection for com.android.billingclient.api.BillingClientImpl@3182b3c
08-08 08:44:07.008 ActivityManager: Start proc 10200:com.android.vending ... for bound-service {com.android.vending/com.google.android.finsky.billing.iab.InAppBillingService}
08-08 08:44:07.073 W/BillingClient: Client is already in the process of connecting to billing service.
08-08 08:44:07.074 RC debug: Starting connection for com.android.billingclient.api.BillingClientImpl@3182b3c
08-08 08:44:07.132 ActivityManager: Force stopping service ServiceRecord{... com.android.vending/com.google.android.finsky.billing.iab.InAppBillingService}
08-08 08:44:07.137 W/BillingClient: Billing service died.
08-08 08:44:08.092 RC diagnostics: google_billing_setup_finished {"billing_response_code":5,"billing_debug_message":"Client is already in the process of connecting to billing service.","pending_request_count":2}
08-08 08:44:08.092 RC warn: Billing Service disconnected for com.android.billingclient.api.BillingClientImpl@3182b3c
Then nothing. Zero "Starting connection" lines from 08:44:09 through 08:57:08 (grep-proven across the full logcat), even though the force-stop loop ended at ~08:44:56 and the Play Store was healthy from ~08:45:40. The pending getOfferings sat silent for 13m03s. At 08:57:09 we backgrounded and foregrounded the app:
08-08 08:57:09.450 RC debug: Starting connection for com.android.billingclient.api.BillingClientImpl@3182b3c
08-08 08:57:09.665 RC debug: Billing Service Setup finished for com.android.billingclient.api.BillingClientImpl@3182b3c
and the queued work completed about 3 seconds later.
The same anatomy reproduced on the emulator on both 10.16.0 and 10.16.1. Example from 10.16.1:
08-08 11:51:04.654 RC debug: Starting connection for com.android.billingclient.api.BillingClientImpl@a01c04a
08-08 11:51:04.873 ActivityManager: Force stopping service ... InAppBillingService
08-08 11:51:04.905 W/BillingClient: Billing service died.
08-08 11:51:04.928 RC warn: Billing Service disconnected for com.android.billingclient.api.BillingClientImpl@a01c04a
[zero further "Starting connection" lines until a background/foreground at 11:54:15]
- Steps to reproduce, with a description of expected vs. actual behavior
Reproduces on a stock Android emulator with a Google Play system image and no Google account, so it should be easy to run anywhere:
- Configure the SDK with verbose logs and open a screen that calls
Purchases.getOfferings().
- Simulate sustained Play Store churn from the host: run
adb shell am force-stop com.android.vending in a loop every ~300ms for ~110 seconds.
- While the loop is running, trigger a
getOfferings() call so the SDK starts a billing connection.
- The wedge forms when the loop kills the billing service during an in-flight connection attempt. Signature: "Starting connection", then code 5 "already in the process of connecting" with
pending_request_count > 0 in the google_billing_setup_finished diagnostics entry, then "Billing service died", then permanent silence.
- Stop the loop. Wait several minutes with a healthy Play Store.
- Background and foreground the app.
Expected: after the service churn ends, the exponential backoff reconnect ladder re-establishes the connection within seconds to minutes, and the pending getOfferings completes or fails with an error.
Actual: no reconnection attempt is ever scheduled. getOfferings() pends silently and indefinitely (observed 13m03s on the Pixel, and 201.8s / 192.7s / 203.3s on three emulator runs where the pend length was simply how long we waited before foregrounding). The lifecycle foreground handler is the only thing that ever calls startConnection again.
Timing note: a single force-stop does not reproduce this (the SDK rebinds in ~400ms). The disconnect has to land inside the in-flight connection window, which the sustained loop makes reliable (3 out of 3 attempts across emulator runs, plus the Pixel run).
- Other information
Our reading of the current code, consistent with all observed logs:
BillingWrapper.onBillingServiceDisconnected() is a no-op apart from logging and diagnostics, so a disconnect never re-arms the retry ladder by itself.
- The
DEVELOPER_ERROR (code 5) branch of onBillingSetupFinished intentionally does nothing, trusting the in-flight attempt to deliver a terminal callback. When the service dies mid-connect, BillingClient never delivers that callback, so the trust is misplaced and both paths drop the ball.
retryBillingServiceConnectionWithExponentialBackoff() is only invoked from terminal onBillingSetupFinished error codes, which never arrive in this scenario.
We have a fix in progress and intend to submit a PR (re-arming reconnection when a disconnect lands while requests are pending / a connection attempt is in flight).
Related: we found a second, independent way getOfferings() pends forever (HTTP layer, hung socket not bounded by any timeout) while investigating the same production stall. Filed separately as #3923.
Additional context
Production impact for us: a paywall that silently never loads offerings, with no error surfaced to the app, so the app cannot even show a retry affordance based on SDK signals. The pending_request_count growth in consecutive diagnostics entries (2, then 4, then 6 in our logs as more calls queued) makes the wedge visible in the SDK's own diagnostics stream.
Describe the bug
If the Play billing service disconnects while a
BillingClient.startConnection()attempt is already in flight, the SDK's reconnection ladder is never re-armed and never runs again. From that state:onBillingSetupFinished(OK)never arrives for it.startConnectioncall during that window gets response code 5 ("Client is already in the process of connecting to billing service.").BillingWrapper.onBillingSetupFinishedhandles code 5 (DEVELOPER_ERROR) with a deliberate no-op ("Billing service is already trying to connect. Don't do anything."), relying on the in-flight attempt to complete. It never does.BillingWrapper.onBillingServiceDisconnected()only logs and tracks a diagnostics event. It schedules nothing.Result: no code path ever calls
startConnectionagain. The exponential backoff machinery (RECONNECT_TIMER_START_MILLISECONDS= 1s with the 15 minute cap inBillingWrapper,RETRY_TIMER_START= 878ms inBillingClientUseCase) never engages because nothing schedules a retry from this state. Queued service requests (pending_request_count=2in the SDK's own diagnostics) wait indefinitely, soPurchases.getOfferings()pends silently: no error, no timeout, no retry, against a Play Store that is healthy again seconds later. The only release we have observed is an app background/foreground cycle, whose lifecycle hook callsstartConnectionand drains the queue within seconds.We hit this in production behind a paywall:
getOfferings()pends indefinitely with no callback, the user sees a spinner forever. The likely production trigger is Play Store churn (for example a Play self-update) while a billing operation is in flight.Environment
Debug logs
Verbose SDK logs are forwarded through our Flutter log handler; the payloads are the SDK's own lines. Wedge anatomy from the physical Pixel 10a on 10.16.0 (device local time):
Then nothing. Zero "Starting connection" lines from 08:44:09 through 08:57:08 (grep-proven across the full logcat), even though the force-stop loop ended at ~08:44:56 and the Play Store was healthy from ~08:45:40. The pending
getOfferingssat silent for 13m03s. At 08:57:09 we backgrounded and foregrounded the app:and the queued work completed about 3 seconds later.
The same anatomy reproduced on the emulator on both 10.16.0 and 10.16.1. Example from 10.16.1:
Reproduces on a stock Android emulator with a Google Play system image and no Google account, so it should be easy to run anywhere:
Purchases.getOfferings().adb shell am force-stop com.android.vendingin a loop every ~300ms for ~110 seconds.getOfferings()call so the SDK starts a billing connection.pending_request_count> 0 in thegoogle_billing_setup_finisheddiagnostics entry, then "Billing service died", then permanent silence.Expected: after the service churn ends, the exponential backoff reconnect ladder re-establishes the connection within seconds to minutes, and the pending
getOfferingscompletes or fails with an error.Actual: no reconnection attempt is ever scheduled.
getOfferings()pends silently and indefinitely (observed 13m03s on the Pixel, and 201.8s / 192.7s / 203.3s on three emulator runs where the pend length was simply how long we waited before foregrounding). The lifecycle foreground handler is the only thing that ever callsstartConnectionagain.Timing note: a single force-stop does not reproduce this (the SDK rebinds in ~400ms). The disconnect has to land inside the in-flight connection window, which the sustained loop makes reliable (3 out of 3 attempts across emulator runs, plus the Pixel run).
Our reading of the current code, consistent with all observed logs:
BillingWrapper.onBillingServiceDisconnected()is a no-op apart from logging and diagnostics, so a disconnect never re-arms the retry ladder by itself.DEVELOPER_ERROR(code 5) branch ofonBillingSetupFinishedintentionally does nothing, trusting the in-flight attempt to deliver a terminal callback. When the service dies mid-connect, BillingClient never delivers that callback, so the trust is misplaced and both paths drop the ball.retryBillingServiceConnectionWithExponentialBackoff()is only invoked from terminalonBillingSetupFinishederror codes, which never arrive in this scenario.We have a fix in progress and intend to submit a PR (re-arming reconnection when a disconnect lands while requests are pending / a connection attempt is in flight).
Related: we found a second, independent way
getOfferings()pends forever (HTTP layer, hung socket not bounded by any timeout) while investigating the same production stall. Filed separately as #3923.Additional context
Production impact for us: a paywall that silently never loads offerings, with no error surfaced to the app, so the app cannot even show a retry affordance based on SDK signals. The
pending_request_countgrowth in consecutive diagnostics entries (2, then 4, then 6 in our logs as more calls queued) makes the wedge visible in the SDK's own diagnostics stream.