Android’s subscription conversion rate, compared to iOS, looks like a platform problem. When RevenueCat analyzed over 115,000 apps and $16 billion in revenue for the 2026 State of Subscription Apps report, the numbers were clear: on Android, the median download to paid conversion at day 35 sits at 0.9%, while iOS lands at 2.6%. That’s a nearly threefold gap. The instinct is to blame the trial, or the audience, or some fundamental difference in how Android users behave. The data says otherwise.

Looking one level deeper in the same dataset, trial to paid conversion on Android is 32.5%. On iOS, it’s 32.6%. Once a user starts a trial on either platform, they convert at statistically the same rate. There is an important caveat: Android’s trial-starter pool is likely more filtered. Because Android surfaces fewer trials overall, the users who do start one tend to be higher-intent. That selection effect partly explains why the rates equalize. Even so, the primary lever is clear: Android apps are sending far fewer users into that first stage. Closing the gap starts with getting more users to begin a trial in the first place.

In this article, you’ll explore the two-stage Android paywall funnel and where Android apps lose users, how Google Play’s offer and tag system controls which subscription option is surfaced to users, how RevenueCat’s SubscriptionOptions selection works and where silent misconfigurations occur, what the data shows about hard paywalls versus freemium models, and how to use RevenueCat Experiments to close the gap systematically.

The fundamental problem: Two stages, one broken

The download to paid journey has two distinct stages, and they behave differently.

The first stage is download to trial start: the user installs your app, reaches your paywall, and decides whether to begin a free trial. The second stage is trial start to paid: the trial ends, and the user decides whether to continue as a subscriber.

The RevenueCat data shows these two stages behave very differently on Android. A user who starts a trial converts at 32.5%, close to the 32.6% on iOS. But Android apps are sending far fewer users into that first stage. The bulk of the conversion gap lives in stage one.

One data point makes this concrete: 89.4% of all trial starts happen on the day of install. Users who download with high intent act immediately. Users who do not start a trial on install day rarely return to do so later. That makes the first paywall impression on Android the moment that determines most subscription revenue. Everything downstream from that moment, including your trial experience, your onboarding, your product, performs about as well on Android as on iOS. The question is whether users reach that moment at all.

Stage one: What determines whether users see a trial

Two things control whether a user is presented with a free trial on Android: what you show (the paywall type) and when you show it (the timing). Both are fully within your control.

The paywall type gap

The RevenueCat data breaks down paywall models by D35 download to paid conversion, RevenueCat’s measurement window for capturing conversion across trial lengths up to one month. Hard paywalls, where users must interact with a subscription offer before accessing core features, achieve a median D35 conversion of 10.7%. The top 10% of hard paywall apps reach 38.7%. Freemium models, where users get some access without paying, convert at a median of 2.1%.

That’s a fivefold difference in conversion with nearly identical annual retention. Hard paywalls retain 27% of subscribers at 12 months. Freemium retains 28%. For most app categories, the hard paywall numbers are substantially better. If your product delivers clear, immediate value in a single session, a hard paywall is almost certainly the right model. Categories where freemium remains appropriate are those with network effects or long value-discovery cycles, such as social apps and community tools, where acquiring a broad user base matters before monetization.

Paywall modelMedian D35 conversionTop 10% D35 conversion12 month retention
Hard paywall10.7%38.7%27%
Freemium2.1%28%

There is one case where freemium shows a late-conversion advantage: at week six, freemium apps convert 22.9% of that cohort compared to 15.3% for hard paywalls. If your product has a long discovery cycle where value builds gradually over weeks, freemium captures users that a hard paywall would lose.

The paywall timing gap

The 89.4% Day 0 trial start rate has a direct implication for timing: show your paywall in the first session. Every session after the first is a sharply diminishing return.

This doesn’t mean presenting the paywall before any onboarding. Apps that show a paywall before a user understands the product’s value typically see worse opt-in rates. The pattern that works is: deliver one compelling value moment first (a single completed task, a key feature reveal, a concrete output), then present the paywall. On Day 0.

The hidden failure: When offer misconfiguration silently suppresses trials

Even if your paywall type and timing are right, there’s a second source of trial failures on Android that is harder to spot: the subscription offer itself may not be visible. This is a Google Play configuration issue, and it can happen without any error.

How Google Play structures subscription offers

Every subscription on Google Play consists of a base plan and, optionally, one or more offers. An offer defines a promotional pricing phase (a free trial, an introductory price, or both) that precedes the base plan price. Offers are represented in the Billing Library as ProductDetails.SubscriptionOfferDetails.

Each SubscriptionOfferDetails object has a list of pricing phases, a set of offer tags, and an offer token used to initiate the purchase. The pricing phases tell you the price and duration of each stage. The offer tags are strings you define in the Play Console at either the base plan level or the offer level. The Billing Library returns the union of both sets in getOfferTags(), so a tag set on the base plan automatically appears on all offers under it.

When RevenueCat fetches your products, each SubscriptionOfferDetails is converted to a GoogleSubscriptionOption. Looking at the conversion in subscriptionOptionConversions.kt:

1internal fun ProductDetails.SubscriptionOfferDetails.toSubscriptionOption(
2    productId: String,
3    productDetails: ProductDetails,
4): GoogleSubscriptionOption {
5    val pricingPhases = pricingPhases.pricingPhaseList.map { it.toRevenueCatPricingPhase() }
6    return GoogleSubscriptionOption(
7        productId,
8        basePlanId,
9        offerId,
10        pricingPhases,
11        offerTags,
12        productDetails,
13        offerToken,
14        presentedOfferingContext = null,
15        installmentPlanDetails?.installmentsInfo,
16    )
17}

The offerId is null for base plans and set for offer-based options. The offerToken is what gets passed to the billing flow when the user taps “Start free trial.” The offerTags carry the labels you assigned in the Play Console.

How RevenueCat selects the default offer

Once your product has a list of GoogleSubscriptionOption objects, RevenueCat groups them in a SubscriptionOptions collection and exposes a defaultOffer property. This is the option your paywall shows unless you explicitly select a different one.

The selection algorithm in SubscriptionOptions.kt works as follows:

1public val defaultOffer: SubscriptionOption?
2    get() {
3        val basePlan = this.firstOrNull { it.isBasePlan } ?: return null
4
5        val validOffers = this
6            .filter { !it.isBasePlan }
7            .filter { !it.tags.contains(RC_IGNORE_OFFER_TAG) }
8            .filter { !it.tags.contains(SharedConstants.RC_CUSTOMER_CENTER_TAG) }
9
10        return findLongestFreeTrial(validOffers) ?: findLowestNonFreeOffer(validOffers) ?: basePlan
11    }

The algorithm filters out any offer tagged rc-ignore-offer or rc-customer-center, then selects the offer with the longest free trial. If there is no free trial, it selects the offer with the lowest introductory price. If no offers pass those checks, it falls back to the base plan with no promotional phase at all.

That fallback is the silent failure. If your trial offer is tagged rc-ignore-offer, if it is not attached to the right base plan, or if it simply has no offer tags at all and your app relies on tag-based filtering, defaultOffer returns the base plan. Your paywall renders. Everything looks fine. No error appears. But the trial is gone.

Detecting what your paywall is actually showing

Before optimizing anything else, verify that defaultOffer on your product resolves to an offer with a free trial. The SubscriptionOption interface exposes a freePhase property for exactly this check:

1val freePhase: PricingPhase?
2    get() = pricingPhases.dropLast(1).firstOrNull {
3        it.price.amountMicros == 0L
4    }

A non-null freePhase means the option includes a free trial phase. introPhase similarly checks for introductory paid phases. If your defaultOffer has a null freePhase and a null introPhase, no promotional phase will be shown. You can also check defaultOffer?.isBasePlan directly: a true value means the SDK found no eligible offer and fell back to the base plan. Either way, inspect your offer configuration in the Play Console.

You can check this in code after fetching offerings:

1Purchases.sharedInstance.getOfferingsWith(
2    onError = { error -> /* handle */ },
3    onSuccess = { offerings ->
4        val currentOffering = offerings.current ?: return@getOfferingsWith
5        val monthlyPackage = currentOffering.monthly ?: return@getOfferingsWith
6        val subscriptionOptions = monthlyPackage.product.subscriptionOptions
7
8        val defaultOffer = subscriptionOptions?.defaultOffer
9        val hasFreeTrialOption = defaultOffer?.freePhase != null
10        val hasIntroductoryOffer = defaultOffer?.introPhase != null
11
12        Log.d("Paywall", "Default offer: ${defaultOffer?.id}")
13        Log.d("Paywall", "Has free trial: $hasFreeTrialOption")
14        Log.d("Paywall", "Has intro offer: $hasIntroductoryOffer")
15    }
16)

Run this during development and confirm the output matches your Play Console offer configuration. If hasFreeTrialOption is false and you expected a trial, the offer is not being selected. Check offer tags, verify the offer is in the correct base plan, and confirm the offer is active in the Play Console.

One structural difference to be aware of

Before moving to tracking and experimentation, it is worth naming one platform-level difference that the Android-iOS comparison does not fully capture.

On iOS, Apple sends a system-level push notification before a trial ends, reminding the user it will convert to paid. Google Play does not send an equivalent system notification. This means iOS gets a built-in re-engagement nudge at the critical trial to paid moment, and Android does not. On Android, that reminder is entirely your responsibility: an in-app banner, a push notification from your own backend, or a re-engagement flow triggered when the user returns near the end of their trial.

This structural difference partially explains why the trial to paid rates look similar despite the very different trial-starter pool sizes: iOS has a platform assist at the conversion moment. On Android, the same result requires explicit implementation. If your Android trial to paid rate is below your iOS rate, the absence of a trial-end reminder in your app is a likely contributor.

Tracking paywall performance with PresentedOfferingContext

Once your offer configuration is correct, the next step is understanding which paywall placement drives the most trial starts. RevenueCat’s PresentedOfferingContext lets you attach a placement identifier to every purchase, so your analytics can segment by where in the app the paywall appeared.

PresentedOfferingContext carries three fields:

1public class PresentedOfferingContext(
2    public val offeringIdentifier: String,
3    public val placementIdentifier: String?,
4    public val targetingContext: TargetingContext?,
5)

The offeringIdentifier is the offering from your RevenueCat dashboard. The placementIdentifier is a string you define to label the surface: "onboarding_paywall""settings_upgrade""feature_gate", and so on. The targetingContext carries rule data when you are using RevenueCat’s targeting feature.

When you initiate a purchase, this context travels with the transaction and appears in your RevenueCat dashboard and webhook events. You can then compare trial start rates and D35 conversion across placements and determine which surface is worth optimizing first.

Trial length: The overlooked variable

Beyond offer visibility and paywall type, trial duration has a measurable impact on trial to paid conversion. The RevenueCat data shows a clear pattern:

Trial lengthTrial-to-paid conversion
4 days or fewer25.5%
17 to 32 days42.5%

Apps offering longer trials show roughly 17 percentage points higher trial to paid conversion in the dataset. This is a correlation: apps that offer longer trials tend to be more deliberate productivity and creative tools where longer trials reflect a conscious product strategy, not just an arbitrary setting. Extending your trial duration does not guarantee a 17-point improvement. Yet the pattern suggests that for apps where value compounds over time, a 4-day trial may end before a user has had a meaningful product experience, while a 14 or 30-day trial gives the product enough time to demonstrate its value.

Yet 55% of all trials in the dataset are now 4 days or shorter, up from 42% the previous year. Only 5% of apps offer 17 or more days.

If your trial to paid rate is below the 32.5% Android median, trial length is worth testing. It is one of the higher-leverage variables to run through RevenueCat Experiments without needing to ship new code.

Measuring and iterating with RevenueCat Experiments

All of the variables discussed so far (paywall type, timing, trial length, offer selection) interact in ways that are hard to reason about without measurement. What works depends on your specific product, your audience, and your category.

RevenueCat Experiments lets you run A/B tests against these variables without shipping code changes or building backend infrastructure. You create a variant offering in the RevenueCat dashboard with a different configuration: a different trial length, a different default offer, or a different package lineup. RevenueCat randomly assigns users to control or variant, tracks their behavior through the full trial and conversion cycle, and surfaces D35 conversion, LTV, and trial start rate broken down by variant.

Once your experiment is running, you can monitor trial state per user via EntitlementInfo.periodType in CustomerInfo:

1Purchases.sharedInstance.getCustomerInfoWith(
2    onError = { error -> /* handle */ },
3    onSuccess = { customerInfo ->
4        val premiumEntitlement = customerInfo.entitlements["premium"]
5
6        when (premiumEntitlement?.periodType) {
7            PeriodType.TRIAL -> {
8                // User is in an active trial
9                val trialEnds = premiumEntitlement.expirationDate
10                showTrialExpirationReminder(trialEnds)
11            }
12            PeriodType.INTRO -> {
13                // User is in an introductory paid phase
14            }
15            PeriodType.NORMAL -> {
16                // User is a full subscriber
17            }
18            null -> {
19                // No active entitlement
20                showPaywall()
21            }
22        }
23    }
24)

The periodType tells you the current phase of the user’s subscription. This is useful for building trial-aware UI: showing a banner when a trial is about to expire, adjusting messaging based on subscription phase, or triggering a re-engagement paywall for users whose trial ended without converting.

Putting it together

The Android conversion gap is primarily a funnel-entrance problem with identifiable causes. The threefold difference in D35 download to paid between Android and iOS does not reflect a platform ceiling. It reflects the aggregate effect of offer misconfiguration, freemium models that suppress trial uptake, and paywalls shown too late or not at all. A structural platform difference also contributes: Android does not send system-level trial expiration reminders the way iOS does, so re-engaging users near trial end requires explicit implementation on your side.

The path to closing the gap follows a specific sequence. First, confirm that defaultOffer on your active offering resolves to an option with a non-null freePhase. If it doesn’t, fix the Play Console offer configuration before changing anything else. Second, if you are running freemium, run an experiment against a hard paywall variant and measure both trial start rate and 12-month retention. Third, if you are already running a hard paywall, test a longer trial duration. Fourth, add placementIdentifier to your PresentedOfferingContext so you can attribute trial starts to specific surfaces.

Each of these changes is measurable. RevenueCat Experiments gives you the infrastructure to test without guessing, and the D35 and trial to paid metrics give you the signal to act on.

In this article, you’ve explored how the Android paywall conversion problem lives primarily in stage one of a two-stage funnel, how Google Play’s offer and tag system determines which subscription option is surfaced, how RevenueCat’s SubscriptionOptions.defaultOffer algorithm selects a trial and where silent misconfigurations occur, what the data shows about paywall model choice and trial length, the structural platform difference around trial reminders, and how to diagnose and iterate using RevenueCat’s tooling.

Understanding where the gap lives changes what you build. Most of the work is in stage one: getting users to see and start a trial. The Android user who starts a trial converts at nearly the same rate as the iOS user. The work is making sure they get the chance to start, and giving them a reason to convert before that trial ends.