Diagnostic guide
stripe current_period_end not updating
This one is silent by nature: the stored date was correct the day it was written, and nothing about the row itself looks wrong later. It only becomes visible when the drift finally crosses whatever threshold decides access, by which point it has usually been wrong for a while.
most likely causes, in order
01The period end is written once, at checkout, and never again+
how it looks
New subscribers look correct on day one. The same customer, months later, has a period end that is months in the past, while still paying normally.
how to check
Search for wherever your period-end column is written. If checkout.session.completed is the only place, every renewal after the first leaves it exactly where it started.
the fix
Also write it in the customer.subscription.updated and invoice.paid handlers, on every event, not only the first.
02The value is read from the wrong place: a 2025 API change moved it+
how it looks
The field is missing or null on an integration built or updated recently, even though the webhook clearly fires.
how to check
On Stripe API versions from 2025-03-31 ("basil") onward, current_period_end and current_period_start were removed from the Subscription object entirely and now live on each SubscriptionItem instead. Code written against an older API version, or copied from an older tutorial, is reading subscription.current_period_end, which no longer exists on that version.
the fix
Read the period end from the subscription's first item: items.data[0].current_period_end. This also matters going forward if a subscription ever has more than one item: each item can carry its own period.
03The timestamp is stored with the wrong units+
how it looks
The stored date is wildly wrong: often in 1970, or thousands of years in the future.
how to check
Stripe sends this as a Unix timestamp in seconds. A common bug stores it directly as milliseconds (JavaScript's native unit), or as a raw number in a text column instead of a real date.
the fix
Multiply by 1000 before constructing a JS Date, or use your database's to_timestamp() equivalent, and store the result in an actual date or timestamptz column.
04Only one of invoice.paid and subscription.updated is handled+
how it looks
The date is right most of the time and occasionally stale by exactly one period.
how to check
Renewals can arrive as either event depending on the subscription: proration, trials, and mid-cycle changes affect which one fires and in what order. Handling only one of the two misses some renewals.
the fix
Write the period end from both events. Writing the same correct value twice on one renewal is harmless; missing the write once is not.
check it yourself
The two directions a stale period end breaks in. Run this in your SQL editor, adjusting the table and column names to match your schema.
-- app thinks access continues after Stripe's period actually ended -- (a canceled customer this keeps in) select email, stripe_customer_id, subscription_end from public.subscribers where subscribed = true and subscription_end < now(); -- app's period end sits before the row was even created -- (a sign it was never updated after the first write) select email, stripe_customer_id, subscription_end, created_at from public.subscribers where subscribed = true and subscription_end < created_at;
what this does not tell you
Both queries catch a date that has clearly stopped moving. Neither catches drift that is still small (a week or two off) because that looks like an ordinary billing date to a query with nothing else to compare it against.
Read-only, both sides. Three findings free, no account.
related
Stripe plan upgrade not updating in app
Someone paid more, or less, and your app still shows their old plan. Where the write is supposed to happen on an upgrade, and the two ways it usually goes missing.
Canceled subscription still has access
Stripe shows canceled. Your app still grants access. The four ways a cancellation fails to reach your database, and how to close it for good.