Diagnostic guide

failed payment, past due, still has access

Most apps are supposed to keep access on while a payment retries; that is the point of retries. The failure mode is not that access stays on: it is that nothing is configured for what happens once Stripe gives up, so past_due becomes a permanent, unnoticed state instead of a temporary one.

most likely causes, in order

01This might not be broken yet: Stripe is still retrying+

how it looks

The subscription has been past_due for a few days, and access is still on.

how to check

Billing settings → Subscriptions and emails → Manage failed payments, to see how many retry attempts remain and over what window.

the fix

Nothing to fix yet. Keep reading for what should happen once retries are exhausted.

02Nothing is configured for what happens after the last retry+

how it looks

A subscription can sit in past_due indefinitely (weeks or months) with access never revoked, because nothing tells Stripe, or your app, to do anything once retries run out.

how to check

In the same Billing settings, find the setting for what happens when all retries fail: it can cancel the subscription, mark it unpaid, or leave it exactly as-is.

the fix

Set it to cancel the subscription or mark it unpaid. Leaving it "as-is" is what causes this.

03customer.subscription.updated is handled for other statuses but not unpaid+

how it looks

The webhook clearly does something (the plan syncs, cancellations work), but past_due never resolves either way.

how to check

Read the status branch in the handler. If it checks for active and canceled explicitly and falls through silently for unpaid, that status has no effect on access.

the fix

Add unpaid alongside canceled wherever access is revoked.

04Access is revoked on the first failed attempt, not after retries are exhausted+

how it looks

The opposite problem: customers lose access the moment a card blips, before Stripe has tried again, and complain that a working card "randomly" failed.

how to check

Look for invoice.payment_failed in the handler. If it revokes access directly, it is treating the first attempt as the end of the story.

the fix

invoice.payment_failed fires on every retry, not just the last one: use it to notify the customer, not to revoke access. Revoke on the status transition to unpaid or canceled instead.

check it yourself

Access that has outlived its period end by more than two weeks. Run this in your SQL editor, adjusting the table and column names to match your schema.

select email, stripe_customer_id, subscribed, subscription_end
from public.subscribers
where subscribed = true
  and subscription_end is not null
  and subscription_end < now() - interval '14 days';

what this does not tell you

This catches subscriptions stuck well past any reasonable retry window. It will not catch one still legitimately mid-retry, and it cannot tell you whether Stripe is even configured to resolve this one eventually: that setting lives in Stripe, not your database.

Read-only, both sides. Three findings free, no account.

related