Guides · Updated October 31, 2026

Stripe subscription past_due but the user still has access: what to do

A customer's renewal failed, Stripe shows the subscription as past_due, and they're still using your paid plan. Sometimes that's intended (a grace period while Stripe retries the card), and sometimes it's a bug that will keep them on the paid plan long after Stripe has stopped trying. This guide explains what past_due means, why apps keep granting access, how to choose a policy, a code pattern that follows it, and what happens when the retries run out.

What past_due means

Stripe's definition: payment on the latest finalized invoice either failed or wasn't attempted (Stripe: How subscriptions work). In practice it's usually a renewal whose card was declined. While a subscription is past_due:

Stripe's guidance for this status is to notify the customer and ask them to update their payment details. For canceled and unpaid it says to revoke access (Stripe: Using webhooks with subscriptions). Whether past_due keeps access is left to you.

Why your app still grants access

Decide your policy: grace period or restrict

Both are reasonable. Write down which one you're using, because the code and your Stripe settings have to agree.

A handler that follows your policy

Two rules: gate access on an allow-list of statuses, and treat each webhook as a prompt to re-read the subscription from Stripe rather than trusting the event's contents. A sketch in the style of Stripe's Node library (the db calls stand in for your own data layer):

// 1) Gate on an allow-list. Anything not listed, including a status you
//    didn't plan for, means no access.
const ALLOW = new Set(["active", "trialing"]);
const GRACE_DAYS = 0; // your policy: 0 = restrict on the first failed renewal

function hasAccess(user, now = Date.now()) {
  if (ALLOW.has(user.stripeStatus)) return true;
  if (user.stripeStatus === "past_due" && GRACE_DAYS > 0 && user.pastDueSince) {
    return now - user.pastDueSince < GRACE_DAYS * 86_400_000;
  }
  return false; // past_due after grace, unpaid, canceled, incomplete, paused, ...
}

// 2) On each webhook (after verifying the signature), re-read the
//    subscription from Stripe instead of trusting the event payload.
async function onStripeEvent(event) {
  if (await db.processedEvents.has(event.id)) return; // duplicates and resends
  const obj = event.data.object;
  const subId = obj.object === "subscription"
    ? obj.id
    // invoice events: newer API versions use parent.subscription_details.subscription,
    // older ones use invoice.subscription
    : obj.parent?.subscription_details?.subscription ?? obj.subscription;
  if (subId) {
    const sub = await stripe.subscriptions.retrieve(typeof subId === "string" ? subId : subId.id);
    const user = await db.users.findByStripeSubscription(sub.id);
    await db.users.update(user.id, {
      stripeStatus: sub.status,
      pastDueSince: sub.status === "past_due" ? (user.pastDueSince ?? Date.now()) : null,
    });
  }
  await db.processedEvents.add(event.id);
}
// Subscribe the endpoint to customer.subscription.updated,
// customer.subscription.deleted, invoice.payment_failed and invoice.paid.

Because hasAccess checks the grace period when a request comes in, access ends on time even if no further event arrives. pastDueSince records when your app first saw the status, so if events were delayed it starts late; if that matters, use the date of the first failed invoice instead. Verify the webhook signature before processing, return a 2xx quickly, and record processed event IDs, since Stripe can deliver the same event more than once (Stripe: Best practices). After invoice.paid, Stripe's docs say to retrieve the subscription and confirm it's active before extending access, which the re-read above does.

What happens after the retries end

When the last retry fails, your failed payment settings decide the subscription's next status (Stripe: Automate payment retries):

Changing these settings only affects future retries. To bring a past_due or unpaid subscription back to active with the default status setting, pay the most recent non-voided invoice or mark it uncollectible; with "all invoices", settle every outstanding one (Stripe: Subscription statuses). If a customer updated their card and nothing happened, check where they updated it: retries use the subscription's default payment method before the customer's.

Find the past_due customers who have access now

  1. In the Dashboard's subscriptions list, filter by Past due (and Unpaid), or call List subscriptions with status=past_due (API: List subscriptions). Export the customer email or ID.
  2. Export the users who currently have paid access in your app.
  3. Match them. Anyone in both lists is getting access while their payment is failing. Apply your policy: inside the grace period, send a reminder; past it, restrict. Before restricting, check they don't also have another active subscription, a one-time purchase or a comp.

What PaidCheck's free report checks

With a read-only restricted key and your user list (a CSV, a Supabase function or an HTTPS endpoint), the free report does step 3 for you and looks at the webhook causes above:

Without your user list it runs only the Stripe-side webhook checks. It can't see your code, sessions or caches, and it never writes to Stripe or your app. Paid plans run these checks daily and send an alert email when a check finds a new issue.

FAQ

Does past_due mean the Stripe subscription is canceled?

No. past_due means payment on the latest finalized invoice failed or wasn't attempted. The subscription is still in place and keeps creating invoices, and Stripe might retry the payment. What happens after the retries end depends on your failed payment settings.

Should I cut access when a subscription goes past_due?

That's your policy. Stripe's docs say to notify the customer and ask them to update their payment details when a subscription becomes past_due, and to revoke access when it becomes canceled or unpaid. Keeping access for a short grace period or restricting right away are both reasonable, as long as your code does what you decided.

How long does Stripe retry a failed subscription payment?

It depends on your settings. Stripe's docs say Smart Retries can retry a set number of times within 1 week, 2 weeks, 3 weeks, 1 month or 2 months, with a recommended default of 8 tries within 2 weeks. A custom schedule allows up to three retries. Stripe doesn't retry in some cases, for example when no payment method is available or the issuer returned a hard decline code.

What happens to a past_due subscription after the last retry?

Your failed payment settings decide: Stripe can cancel the subscription, mark it unpaid, leave it past_due, or, for eligible subscriptions, pause it. If you choose to leave it past_due, it stays past_due and invoices keep being generated, so an app that grants access to past_due keeps that customer on the paid plan.

The customer updated their card but the subscription is still past_due. Why?

Stripe's docs say retries use the subscription's default payment method first, then the subscription's default source, then the customer's default payment method. If the subscription has its own default payment method and only the customer's default was updated, Stripe keeps retrying the old one. Update the field where the payment failed, or have the customer pay the open invoice. With the default status setting, paying the most recent non-voided invoice moves the subscription back to active.

Can PaidCheck change access in my app?

No. PaidCheck reads Stripe with a read-only restricted key and compares it with who has access in your app (from a CSV, a Supabase function or an HTTPS endpoint you provide). It lists users whose subscription is past_due, unpaid or canceled but who still have access, so you can decide what to do. It never writes to Stripe or your app.