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