#neha_unexpected
1 messages · Page 1 of 1 (latest)
Below are links to other discussions we've had with you in the past week in case you want to review that information. If your question is related to one of these previous discussions, please provide a comprehensive summary of the current state and what you need help with now. We help many users simultaneously, so a summary allows us to resolve your issue as soon as possible.
- neha_paymentintent-dataingestion, 3 days ago, 40 messages
👋 Welcome to your new thread!
⏲️ We'll be here soon! Typically we respond in a few minutes, but sometimes we might take a bit longer if the server is busy or if you have a particularly tricky question.
⏱️ We close idle threads, which makes them read-only. Once a thread is closed it won't be reopened, but you can always start a new thread if you have another question.
🔗 This thread will always be available, even after it's closed. You can find it again using Discord's search, or you can save this link: https://discord.com/channels/841573134531821608/1234628353848381552
📝 Have more to share? Add more details, code, screenshots, videos, etc. below.
@sharp lance Im sorry but I don't really get the question and what the problem is just yet. We don't guarantee any specific order of Events
sorry i had to rearrange the Q to fit the prompts but yes, i understand event order is not guarnteed
what i am confused by is:
i got invoice.paid
i fetched the latest subscription details
and stripe said that the subscription was "trialing, ending april 26" rather than "active, ending may 23"
i'm wondering why this subscription info was out of date? considering i fetched it anew
relatedly: at the exact same time, the subscription.updated event occurred, so there shouldn't have been any data staleness
I don't really get what is "out of date". There's no such thing in our API as saying "trialing, ending april 26". Our API returns properties for code to consume.
When you create a Subscription with a trial period then it has status: 'trialing'. There's still an Invoice for the trial and that Invoice can be paid
case "invoice.paid": {
const data = event.data.object;
const subscription = await stripe.subscriptions.retrieve(data.subscription;
so this code was run for event evt_1P8vsGBZ8IchjqOeQJhzjdkO
and the "subscription" that was returned had status "trialing" + current_period_end "april 26"
but it should have had status "active" + current_period_end "may 23"
But why do you expect invoice.paid to mean "the Subscription has to be active"? That is not a correct assumption to make/have
😅 I feel like we're talking past each other but I have no idea why
in this case - the user upgraded from a trial to an active subscription
so i would expect that fetching the latest subscription would have shown status "active"
instead it showed the old status of "trialing"
does that make sense?
evt_1P8vsGBZ8IchjqOeQJhzjdkO
evt_1P8vsGBZ8IchjqOeeUs4W4I7
can you see these events?
I can, just struggling to follow your train of thought. You hadn't mentioned this was about the trial ending
Please pause
ok
Sorry I'm helping many users at once and everyone is writing really short sequential sentences that are a bit all over the place. I'm about to run and be replaced by someone else, can I ask you to try and take 3 steps back and summarize it clearly with all the info in one unique big message together instead of the stream of thoughts?
ok
timeline of events:
- the user signed up for a 7-day free trial ending on april 26
- on april 23, the user decided to end his free trial in order to get a higher level of access. at this point, two events were sent at the exact same time
- the first event was "invoice.paid" (evt_1P8vsGBZ8IchjqOeQJhzjdkO) and the second event was "subscription.updated" (evt_1P8vsGBZ8IchjqOeeUs4W4I7). there were other concurrent events but let's focus on these two
in my handleWebhook code, i always fetch the latest subscription details from stripe, because i understand event order is not guaranteed and it's up to me to get the latest data. this is how i do it:
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
in this case, for this customer, when i fetched the subscription following the "invoice.paid" event i got stale data. the subscription was said to be status "trialing" despite actually being status "active"
milliseconds later, when i responded to the "subscription.updated" event (because i process the events in a one-by-one queue), i got fresh data. the subscription was said to be status "active"
my question is - why the stale data?
Hi there 👋 taking over, as my colleague needs to step away
Give me a few minutes to get caught up.
The 2 events you gave as examples are entirely unrelated (e.g. one is for a test mode invoice and the other is for a live mode subscription that's unrelated to that test mode invoice). Is that intentional?
oh sorry i sent the wrong event IDs then
hold on
here's the customer ID: cus_Px0x1itIo5zy4I
evt_1P8vsGBZ8IchjqOeQJhzjdkO
evt_1P8vsGBZ8IchjqOeeUs4W4I7
that's what the workbench is giving me
It sounds like a classic race-condition, since you're creating subsequent requests within ~milliseconds of webhook event receipt. I would recommend either building internal logic that checks the timestamps (accurate within a millisecond) of each request and then chooses to either update your database, or do nothing, depending on when the request was actually received.
but the events were both sent at "Apr 23, 2024, 10:47:57 PM"
why wasn't stripe giving up-to-date info when queried?
since you're creating subsequent requests
not sure what you mean by that
basically every time i get a webhook i care about, i ask stripe for the latest subscription details. in this case, after event evt_1P8vsGBZ8IchjqOeQJhzjdkO, stripe was stale
customer paid their invoice & two events occurred
=> i received webhook 1 (invoice.paid)
=> i asked stripe for latest subscription details
=> stripe said "trialing" (wrong)
=> i received webhook 2 (subscription.updated)
=> i asked stripe for latest subscription details
=> stripe said "active" (right)
Those timestamps aren't accurate down to the millissecond. If you make a request 2 milliseconds after an update on an Invoice occurred, it might take Stripe 3 milliseconds to update the Subscription it's attached to. There's no such thing as "instant" when it comes to the Stripe APIs. Every request, read, write, etc. action happens some number of milliseconds after the originating event that caused the state to change. If they occur at the exact same time (e.g. you send a request while a write procedure is taking place) you'll get a request rate limit error.
So really it's all about either: (a) checking your data to make sure you're getting the latest data down to the millisecond, or (b) overwrite the database each time you get a webhook, or (c) make fewer API requests (e.g. why don't you just wait for the subscription.updated event instead of sending a request to get it when you receive an invoice.paid event?)
i have a credit-based subscription system so it's complicated
// NOTE: We use this to handle a customer purchasing more credits.
case "checkout.session.completed": {
...
}
// Sent when the subscription is created.
// NOTE: We use this to update our User table with the Stripe information.
case "customer.subscription.created": {
...
}
// Sent when a subscription starts or changes. For example, renewing a subscription, adding a
// coupon, applying a discount, adding an invoice item, and changing plans all trigger this event.
// NOTE: We use this to handle downgrades gracefully.
case "customer.subscription.updated": {
...
}
// Sent when a customer’s subscription ends.
case "customer.subscription.deleted": {
...
}
// Sent when the invoice is successfully paid. You can provision access to your product when
// you receive this event and the subscription status is active.
// NOTE: We use this to create/update the subscription and add any credits.
case "invoice.paid": {
...
}
// A payment for an invoice failed. The status of the subscription continues to be incomplete
// only for the subscription’s first invoice. Otherwise it's past_due.
// NOTE: We use this to update the subscription and cancel any running jobs.
case "invoice.payment_failed": {
...
}```
"invoice.paid" is responsible for allocating credits
"subscription.updated" is just for running side-effects associated with downgrades
why don't you just wait for the subscription.updated event instead of sending a request to get it when you receive an invoice.paid event
i was under the impression that i can't guarantee the concurrency of any of these events; one may happen w/o the other
checking your data to make sure you're getting the latest data down to the millisecond
how?
it's really confusing to make this webhook code bulletproof given the out-of-order race-condition situation. i've written a ton of tests and stuff like this still comes up in practice.
Ahhhh, okay
I see your dilemma a bit more clearly now. I thought that Stripe sends HTTP requests with Date headers that are accurate to the millisecond. Am I wrong about that? I might need to test real quick to make sure I'm 100%
You could check the timestamp of both your webhooks and your requests. So if you make a request to get the Subscription object, and you receive subscription.updated within milliseconds of that request, you'll know to prioritize updates from the webhook instead. So you could overwrite the state in your database appropriately.
i thought it was considered good practice to process the webhooks in a queue though
so i don't actually handle them concurrently, despite receiving them concurrently
it just feels a bit weird, what you're saying. to special case "subscription.updated" as this indication to the other webhook handlers that their data is bad
the way i have it now, all the webhooks are supposed to be independent and rely on stripe for add'l metadata
(aka for the latest subscription details, bc that's the foundation of my logic)
If you were only ingesting webhook data and not making immediate subsequent requests to check state on related objects, then that would be fine. But it sounds like the issue is that your requests are "beating" Stripe to the Subscription update (not an awful problem to have a speedy system capable of that, but still a complicated one to deal with).
If you're provisioning access to your product based on invoice.paid, is there a good reason to check the state of the Subscription to begin with?
i check because different statuses => different credit packages
for example:
free trial = 20 credits
paid plan = 100 credits
also, when allocating the credits, i set them up to expire at the same time as the subscription
so the two fields i'm relying on are: "status" (trialing or not) and "current_period_end"
const { status, current_period_end } = subscription;
if (status === "active" || status === "trialing") {
const creditPackages = getCreditPackages(status, current_period_end);```
Ahhhhh, okay that makes sense. Is there a UI that's dependent on knowing these state changes before proceeding to the next step in a workflow? Like, in your invoice.paid webhook handler, could you add a listener that gets notified of any subscription.updated events you received (or that checks the timestamps on updates to subscriptions in your database), then sleep Invoice requests for 1 second if none are present?
there's no UI where the user is following along
do you mean "if some are present"?
i wonder if i should just sleep for 1 second always, for invoice.paid
i coud also add some logic to my queue s.t. it doesn't execute straight away - it could wait 500ms for requests to pile up, then addresses them a priority order (subscription.* ahead of invoice.*)
what do you think about that? versus the "always sleep for 1 second"
the 500ms thing feels more elegant but adds more wait time overall. the 1s thing feels hacky but it sounds like it would get the job done
I think that if you have the time to build logic into your queuing system, then that will always be cleaner than just blanket sleep'ing actions. That's a decision you'd want to make based on knowledge/demands of your system that only you know
Hate giving that answer, but it sounds like you are on the right track