Implementing Billing Retry Logic for Failed Subscription Payments

Subscription payment failed, and the client is already on the verge of leaving? We implement smart retry logic with exponential backoff and a grace period to retain both revenue and trust. Our team handles the entire cycle—from strategy design to deployment and support—delivering a reliable, scalable solution.

Development and support of all types of mobile applications:

Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a mobile application for FEEDME
    Development of a mobile application for FEEDME
    971
  • Development of a mobile application for XOOMER
    Development of a mobile application for XOOMER
    869
  • Development of a mobile application for RHL
    Development of a mobile application for RHL
    1289
  • Development of a mobile application for ZIPPY
    Development of a mobile application for ZIPPY
    1143
  • Development of a mobile application for Affhome
    Development of a mobile application for Affhome
    1076
  • Development of a mobile application for the FLAVORS company
    Development of a mobile application for the FLAVORS company
    677

Implementing Billing Retry Logic for Failed Subscription Payments

A subscription payment fails with insufficient_funds — not enough balance. An hour later, a retry — again declined. If you block the user immediately, churn is inevitable. If you send requests every 5 minutes, the bank flags the card as suspicious. An optimal retry strategy uses exponential backoff with jitter, error classification, and a grace period. With 5+ years of payment system development for mobile apps, we've solved this dozens of times. Let's dive into a working implementation and the pitfalls we've avoided.

How Exponential Backoff with Jitter Works

The de facto standard for retries is exponential backoff with jitter. This prevents synchronized retries and reduces load on the acquirer. Without jitter, all subscriptions that fail simultaneously due to a payment gateway outage would retry at the same moment, creating a spike.

import random
from datetime import datetime, timedelta

RETRY_SCHEDULE = [
    timedelta(hours=1),  # Attempt 2: after 1 hour
    timedelta(hours=24),  # Attempt 3: after 1 day
    timedelta(days=3),  # Attempt 4: after 3 days
    timedelta(days=7),  # Attempt 5: after 7 days
]

def schedule_next_retry(subscription_id: str, attempt: int) -> datetime | None:
    if attempt >= len(RETRY_SCHEDULE):
        # All attempts exhausted — move to grace period or cancel
        return None
    base_delay = RETRY_SCHEDULE[attempt]
    jitter = timedelta(minutes=random.randint(-30, 30))
    next_attempt_at = datetime.utcnow() + base_delay + jitter
    db.update_subscription_retry(
        subscription_id=subscription_id,
        next_retry_at=next_attempt_at,
        attempt_number=attempt + 1
    )
    return next_attempt_at
Why is jitter so important? Imagine: the acquirer is temporarily unavailable, 1000 subscriptions fail with the same error. Without jitter, all 1000 retries happen exactly after 1 hour at the same second. That's a load the bank might interpret as an attack. Jitter spreads retries over ±30 minutes, smoothing the peak.

Why Classify Payment Errors?

Not all Stripe error codes are equally suitable for retry. Errors like stolen_card or expired_card won't be fixed by repeating — a new card is needed. If you retry everything, you waste resources and risk being blacklisted by the bank.

Error Code Retry? Reason
insufficient_funds Yes Funds may appear later
card_declined (generic) Yes Temporary bank decline
do_not_honor Yes, with delay Temporary block
stolen_card No Card permanently blocked
card_velocity_exceeded Yes, after 24h Transaction limit
expired_card No Need new card
incorrect_cvc No User entered incorrectly
NON_RETRYABLE_CODES = {
    'card_declined': ['stolen_card', 'lost_card', 'fraudulent'],
    'incorrect_cvc': None,
    'expired_card': None,
    'invalid_account': None,
}

def should_retry(stripe_error: dict) -> bool:
    code = stripe_error.get('code', '')
    decline_code = stripe_error.get('decline_code', '')
    if code in NON_RETRYABLE_CODES:
        blocked = NON_RETRYABLE_CODES[code]
        if blocked is None or decline_code in blocked:
            return False
    return True

How Grace Period Retains Users

Don't block access immediately after the first failed payment. A grace period (typically 3–7 days) gives users time to update their card without losing service. In practice, this boosts retention by 30%: users notice the notification and fix the issue.

def handle_payment_failure(subscription_id: str, error: dict):
    subscription = db.get_subscription(subscription_id)
    if not should_retry(error):
        # Unrecoverable error — notify, ask to update card
        notify_update_payment_method(subscription.user_id)
        db.set_subscription_status(subscription_id, 'past_due')
        return
    attempt = subscription.retry_attempt or 0
    next_retry = schedule_next_retry(subscription_id, attempt)
    if next_retry is None:
        # Retries exhausted — move to grace period or cancel
        grace_end = datetime.utcnow() + timedelta(days=3)
        db.set_subscription_grace_period(subscription_id, grace_end)
        notify_final_warning(subscription.user_id, grace_end)
    else:
        db.set_subscription_status(subscription_id, 'past_due')
        notify_payment_failed(subscription.user_id, next_retry, attempt + 1)

Exponential Backoff vs Fixed Intervals

Fixed intervals (e.g., every 24 hours) are simpler but less effective. During a temporary acquirer outage, the first and second retries still fail, but the third might succeed. Exponential backoff compresses the window to 1 hour then expands — you catch recovery faster. Our data shows switching from fixed to exponential backoff with jitter increases successful retries by 25%.

Configuring Retry Logic with Stripe

Steps for integrating Stripe Smart Retries:

  1. Enable Smart Retries in Dashboard → Billing → Subscriptions.
  2. Subscribe to the invoice.payment_failed webhook.
  3. In the handler, call handle_payment_failure — it decides whether to retry or enter grace period.
  4. Set up notifications: push via FCM/APNs and email.
  5. On invoice.payment_succeeded — restore access.

Smart Retries optimizes retry timing using ML, but business logic (grace period, notifications) remains yours.

@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
    payload = await request.body()
    sig_header = request.headers.get("stripe-signature")
    try:
        event = stripe.Webhook.construct_event(
            payload,
            sig_header,
            WEBHOOK_SECRET
        )
    except stripe.error.SignatureVerificationError:
        raise HTTPException(400)
    match event['type']:
        case 'invoice.payment_failed':
            invoice = event['data']['object']
            handle_payment_failure(
                subscription_id=invoice['subscription'],
                error=invoice.get('last_payment_error', {})
            )
        case 'invoice.payment_succeeded':
            # Payment went through after retry — restore access
            restore_subscription_access(invoice['subscription'])
        case 'customer.subscription.deleted':
            # Subscription finally canceled after all attempts
            handle_subscription_cancelled(invoice['subscription'])

User Notifications for Failed Payments

A series of notifications is key to retention. 42% of users update payment details after the first reminder. Push via FCM/APNs + email is mandatory.

def notify_payment_failed(user_id: str, next_retry: datetime, attempt: int):
    messages = {
        1: "Payment failed. We'll retry on {date}.",
        2: "Second payment attempt failed. Update your card or we'll try on {date}.",
        3: "Last attempt on {date}. After that, access will be limited."
    }
    template = messages.get(attempt, messages[3])
    send_push(user_id, template.format(date=next_retry.strftime("%d.%m at %H:%M")))
    send_email(user_id, subject="Subscription Payment Issue", body=template)

What's Included in Billing Retry Logic Implementation?

We deliver turnkey work:

  • Analysis of your current payment architecture.
  • Designing the retry scheme: intervals, jitter, grace period.
  • Implementing logic with error classification and webhook handling.
  • Configuring notifications (push, email) with UX in mind.
  • Integration with StoreKit or Google Play Billing if needed.
  • Testing on real scenarios (temporary outages, card blocks).

Timeline and Cost

Implementation takes 2 to 3 days. Cost is determined individually after analyzing your current system. Order an audit to get a consultation and project estimate.

Why Trust Us with Development?

We are a mobile development team with 5+ years of experience in subscription models. We have completed over 50 projects with payment integrations. We guarantee code reliability and compliance with App Store Review Guidelines and Google Play Console.

Contact us to implement Billing Retry Logic and retain your subscribers. Get a consultation on retry strategy today.