Google Play Billing provides a comprehensive API surface for handling in-app purchases and subscriptions on Android. Most developers are comfortable with the standard purchase flow: launch the billing flow, receive a result, acknowledge the purchase, and grant entitlements. But production billing systems must handle a wider range of scenarios that are often underrepresented in tutorials and sample code. Pending purchases, multi-quantity consumables, subscription downgrades with proration, and the ITEM_ALREADY_OWNED response are all situations your app will encounter in the real world, and mishandling any of them can result in lost revenue, confused users, or failed purchases.
In this article, we’ll explore the most common edge cases in Google Play Billing, understand why they occur, examine how to handle each one correctly with the Play Billing Library, and see how RevenueCat simplifies these scenarios so you can focus on your product instead of billing infrastructure.
The fundamental problem: the happy path is not enough
Most billing implementations start from the sample code in the Android documentation:
This handles a successful, immediate purchase. But what happens when the payment is delayed by 48 hours because the user is paying at a convenience store? What happens when the user already owns the item because a previous acknowledgment failed silently? What happens when a subscription downgrade takes effect at the next renewal instead of immediately? Each of these scenarios requires specific handling, and ignoring them leads to support tickets, refund requests, and lost subscribers.
Pending purchases: when payment is not immediate
Not all purchases complete instantly. Certain payment methods, including cash payments at convenience stores, bank transfers, and some carrier billing options, require asynchronous processing. When a user initiates a purchase with one of these methods, Google Play returns a purchase in the PENDING state rather than the PURCHASED state.
Why pending purchases happen
Pending purchases are common in markets where credit card penetration is low:
If your app is available globally, you’re bound to encounter pending purchases. Ignoring this state means users in these regions cannot purchase your products at all — or worse, they see confusing behavior where their purchase ‘disappears’.
Detecting and handling the pending state
The PurchasesUpdatedListener receives pending purchases alongside completed ones. The critical distinction is in the purchaseState field:
The key rule is: do not grant entitlements for pending purchases. The user has not paid yet. Instead, record the pending purchase and communicate the status clearly:
Completing a pending purchase
When the payment is eventually confirmed, your app receives an updated purchase via onPurchasesUpdated or through queryPurchasesAsync. The purchaseState will now be PURCHASED, and you can proceed with acknowledgment and entitlement granting.
However, there is a subtlety: the user might not have your app open when the payment completes. Your backend should handle this through Real-Time Developer Notifications (RTDN). When you receive a ONE_TIME_PRODUCT_PURCHASED or SUBSCRIPTION_PURCHASED notification for a previously pending token, your backend should update the entitlement and notify the user:
Enabling pending purchases in your BillingClient
Pending purchase support must be explicitly enabled when building the BillingClient. Without this, purchases from delayed payment methods will fail entirely:
Starting with Play Billing Library 7, calling enablePendingPurchases() is required. Without it, BillingClient initialization will fail.
The ITEM_ALREADY_OWNED response: a common source of confusion
One of the most frequently encountered edge cases is BillingResponseCode.ITEM_ALREADY_OWNED. This response occurs when a user attempts to purchase a non-consumable product or subscription they already own. While it sounds straightforward, the scenarios that trigger it are often surprising.
Why ITEM_ALREADY_OWNED happens
The most common cause is not the user deliberately trying to buy something twice. It’s a previous purchase that was not properly acknowledged. Google Play’s acknowledgment requirement means that unacknowledged purchases exist in a limbo state: the user has been charged, but the purchase has not been confirmed by your app. If the user tries to buy the same item again, Google Play returns ITEM_ALREADY_OWNED because the unacknowledged purchase still exists.
This happens more often than you might expect:
- The app crashed after receiving the purchase but before acknowledging it
- A network error prevented the acknowledgment call from completing
- The user force-closed the app during the purchase flow
- The acknowledgment API call returned an error that was not retried
Handling ITEM_ALREADY_OWNED correctly
The correct response to ITEM_ALREADY_OWNED is not to show an error message. Instead, you should query for existing purchases and process any unacknowledged ones:
This pattern turns a frustrating error into a seamless recovery. The user does not need to know that a previous purchase failed to acknowledge. From their perspective, they tap ‘Buy’ and get the item.
Preventing ITEM_ALREADY_OWNED proactively
The best approach is to prevent this scenario by processing unacknowledged purchases on app startup:
Call this method when the BillingClient connects successfully. This ensures that any purchases that slipped through the cracks are recovered before the user encounters problems.
Consumable purchases: acknowledge vs. consume
For consumable products like in-game virtual currency, extra lives, or token packs, the distinction between acknowledgment and consumption is a common source of bugs. Both are required for consumable products, but they serve different purposes and have different timing requirements.
The acknowledgment and consumption flow
Acknowledgment tells Google Play you’ve delivered the purchased content. It must happen within three days of the purchase, or the purchase is automatically refunded.
Consumption resets the purchase so the user can buy the same item again. Without consuming a product, the user cannot repurchase it, and attempting to do so returns ITEM_ALREADY_OWNED.
For consumable products, you should consume the purchase, which implicitly acknowledges it:
The multi-quantity edge case
Google Play supports multi-quantity purchases for consumable products. A user can buy multiple units of a consumable in a single transaction. The quantity is available in the Purchase object:
If you ignore the quantity field and always grant one unit, users who purchase multiple units will receive fewer items than they paid for. This leads to support tickets and refund requests.
To enable multi-quantity purchases, you must configure the product in the Google Play Console with ‘Allow multi-quantity purchases’ enabled. Additionally, your BillingFlowParams can specify a maximum quantity the user is allowed to select:
The consumption retry problem
If the consumeAsync call fails (due to a network error, for example), the user has received their content but the purchase has not been consumed. This means:
- The user cannot buy the same consumable again
- The purchase may be refunded after three days if not acknowledged (though consumption implicitly acknowledges)
You should implement a retry mechanism for failed consumptions:
Call retryPendingConsumptions() each time the BillingClient connects, alongside your unacknowledged purchase recovery logic.
Subscription downgrades and proration modes
When a user changes their subscription plan, the billing behavior depends on whether they are upgrading or downgrading and which proration mode you specify. Downgrades in particular have behavior that surprises many developers.
The default downgrade behavior
When a user downgrades their subscription (moves to a cheaper plan), the default behavior is DEFERRED: the downgrade takes effect at the next renewal date, not immediately. The user continues to have access to the higher-tier features until their current billing period ends.
Understanding replacement modes
Each replacement mode has different implications for billing, access, and user experience:
The deferred downgrade pitfall
The most common mistake with deferred downgrades is checking the subscription state immediately after the purchase flow completes and expecting to see the new plan. With DEFERRED mode, the original subscription remains active with the original product ID until the next renewal. The new subscription only appears after renewal.
This means your entitlement check must account for the transition period:
The linked purchase token on plan changes
When a subscription replacement is processed (whether upgrade or downgrade), a new purchase token is generated. The new purchase includes a linkedPurchaseToken field pointing to the old subscription. Your backend must handle this correctly to avoid creating duplicate entitlements:
Failing to invalidate the old purchase token when processing a replacement is a common bug that leads to inflated subscriber counts and incorrect revenue reporting.
Network failures and retry strategies
Billing operations are network dependent, and network failures or low latency are inevitable. The critical operations that can fail are the purchase flow itself, acknowledgment, consumption, and purchase verification.
The acknowledgment window
Google Play gives you three days to acknowledge a purchase. If you fail to acknowledge within this window, the purchase is automatically refunded. This is a safeguard for users, but it means your acknowledgment logic must be resilient to transient failures:
BillingClient disconnection
The BillingClient can disconnect at any time, and operations performed on a disconnected client will fail. You should implement reconnection logic with exponential backoff:
How RevenueCat handles these edge cases
Each of the edge cases described above requires careful implementation, retry logic, and backend infrastructure. This is where RevenueCat provides significant value by abstracting away the complexity and handling these scenarios automatically.
Pending purchases
RevenueCat tracks pending purchase states internally and updates CustomerInfo when payments are confirmed. Your app only needs to check entitlements:
RevenueCat’s backend processes RTDN notifications from Google Play, so when a pending purchase completes, the entitlement is updated RevenueCat’s server-side. The next time your app queries CustomerInfo, the entitlement is active. No custom notification handling or purchase token tracking is needed on your side. If you’re a sole developer, building the overall backend infrastructure is a ton of resources.
Acknowledgment and consumption
RevenueCat handles acknowledgment and consumption automatically. When a purchase is received by the SDK, it is verified with RevenueCat’s backend, and RevenueCat acknowledges the purchase with Google Play on your behalf. For consumable products, RevenueCat handles consumption after verification. You never need to call acknowledgePurchase or consumeAsync yourself.
This eliminates the entire class of bugs related to failed acknowledgments, missed consumption calls, and the ITEM_ALREADY_OWNED problem.
Subscription plan changes
RevenueCat provides a clean API for subscription upgrades and downgrades through purchaseWith:
RevenueCat handles the linked purchase token logic, entitlement transitions, and deferred downgrade tracking on the backend. Your app simply checks CustomerInfo for the current entitlement state.
Network resilience
RevenueCat’s SDK includes built-in retry logic for all network operations, caches CustomerInfo locally for offline access, and synchronizes with the backend when connectivity is restored. This means your app can check entitlements even when the device is offline:
The SDK distinguishes between stale and fresh data, retries failed operations with exponential backoff, and ensures that entitlements are eventually consistent with the server-side state.
Wrapping up
In this article, you’ve explored the edge cases that separate a sample billing integration from a production one.
Each of these scenarios has a well defined solution using the Play Billing Library directly, but the cumulative implementation effort is significant. You need client-side handling, backend RTDN processing, retry mechanisms, and careful state management across all of them. For teams that want to ship subscription features without building and maintaining this infrastructure, RevenueCat handles these edge cases automatically, letting you check a single CustomerInfo object instead of managing the complexity yourself.
Whether you build the billing infrastructure directly or use RevenueCat, understanding these edge cases is essential. They represent the difference between a billing system that works in testing and one that works reliably for millions of users across diverse markets and payment methods.
As always, happy coding!
— Jaewoong

