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
Stripe dispute or chargeback, still has access
A dispute pulls the money back immediately and gives you roughly 7 to 21 days to respond. What has to be wired up so it also affects access, separate from answering the dispute itself.
Stripe current_period_end not updating
The date your app uses to decide access drifts further from Stripe with every renewal, until it locks out a paying customer or keeps a canceled one in. Includes the 2025 API change that moves this field off the subscription object.