stripe-cli: Webhooks are being sent in incorrect order.
The more information we have the easier it is for us to help. Feel free to remove any sections that might not apply
Issue
Sometimes webhooks are being sent incorrectly. Specific events:
customer.subscription.createdcustomer.subscription.updated
This causes our system to record the subscription status incorrectly. In the stripe dashboard, the actual status of the subscription is already active, however our system recorded incomplete because we received the created event later that it’s updated counterpart.
I wonder if this will also happen in production?
Expected Behavior
Expected behavior would be:
customer.subscription.createdfirst- then
customer.subscription.updated
Steps to reproduce
Note network throttling might help. I have a slow internet.
- Create a checkout session
- Checkout using session id
- Checkout success
- Webhooks successful but, events are send incorrectly.
Traceback
Correct (created then updated)

Wrong (updated then created)

Environment
ArchLinux
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 6
- Comments: 19
@tomer-stripe How is this not considered a bug? I am seeing the same issue, and it is wreaking havoc with my integration. If I can’t rely on the subscription.created > subscription.updated > session.completed events coming in that order, how do I know what the current status is of my subscription? Note that those events also need to be synchronous - subscription.updated shouldn’t be sent until Stripe receives a 200 response from the subscription.created webhook (same thing with session.completed waiting for subscription.updated)… otherwise, this could lead to a race condition on my server.
I am relying on the subscription status field, and if the last webhook event that I receive happens to be subscription.created, it will have a status of “incomplete” which will overwrite the status of “active” sent by the subscription.updated event that arrived first.
@tomer-stripe This is super problematic for your clients. It would be possible to send them in order from your side, by sending requests to webhooks, one at a time, and by waiting for the 200 status code response (or a failure after a number of retries) and only then sending the next event. Instead you move the complexity to your clients.
The fact that it is recommended to ignore webhook request payload by your support and query API is a complete BS. There can still be a race condition, unless client ensures that only 1 request per given customer is processed globally. Did you try to implement a reliable solution using your webhooks yourself? It is a nightmare.
Finally, could you at least provide more fine-grained event samples (instead of seconds) such that your clients can reorder the events on their own?
Really sloppy design on your side, heavily disappointed and we are now strongly considering another payment processor.
I understand this is by ‘design’ as per the event-ordering docs I spent a whole day building webhooks that rely on the lifecycle of the subscription, but now I realise the order is not gaurenteed, I don’t know what to do.
And polling or calling the API everytime the user logs seems like the wrong thing to do, whats the recommended solution here? Are consumers of the stripe API supposed to implement some form of events bus system and ordering the events by
createdtime?I know the stripe CLI has nothing to do with this, but if someone in the team can re-direct us to the right place for this, that would be great!
So the solution I came up with is to completely ignore the subscription data that is posted when my webhook handler is called. Rather than relying on that data, I use the subscription ID to call the subscriptions.retrieve API to get the current subscription data. THAT data is up-to-date, so I don’t have to worry about the order of the subscription and session events arriving.
I think the documentation on handling session completion is misleading. Under “webhooks” on this page https://stripe.com/docs/payments/checkout#payment-success it reads:
While this may be technically correct, it implies that the whole process has completed, when in actuality the corresponding events may not have yet been sent. I think that should be clarified to avoid confusion.
Hi, also came into this topic while implementing a new subscription model and found no way to determine the order of events, making webhook data quite useless 😦 and only to be used as a trigger to fetch data from the Stripe side. Is that correct? any new solution around?
:: EDITED :: 2 cents
objective I want to SAVE in my DB all the events in the correct order. I don’t want to use a pre-defined order of events (“customer.created goes before customer.updated” etc)
what we know
first conclusion
solution
damm
I know this is an old issue but let me introduce what kind of “workaround” I’ve implemented on my end.
Note: Even if this “solution” provides a way of consistency it comes with multiplied cost of performance and resources overhead.
First of all, IMHO the events should be fully sequential. When state of an entity (customer, subscription, invoice) is involved each event of the sequence should be co-dependent on the previous one and the processor (Stripe in this case) must guarantee on their end that you receive them in the proper order. I still have doubts that they went with this asynchronous approach because of huge technical architecture issues. It’s unacceptable to point in their documentation that ensuring event ordering is job of the integrator. See
Allowing multiple events on you server to be processed simultaneously even with refreshing the #StripeObject by ID from their SDK’s is potentially is error prone and dangerous because you’ll end up in huge percentage of cases with Unique Constraint Violation. This would happen because of the low time gap between the event triggering from Stripe’s processor side.
Imagine following event sequence:
You have database table for storing the subscriptions. On your server web hook implementation you optimistically try to insert new subscription if does not exist each time an event is received. Due to low time gap in huge percentage of the cases there will be serious race conditions due to uniqueness. Moreover… if your application is designed to respond with errors for such cases, Stripe automatically will retry calling your server which I find as an overkill.
What we want to achieve:
Implementation:
webhook.php
worker.php
Running into this as well. First thinking that my webhooks may be too slow, and I need transactions on my database. Then I saw the events coming in the “wrong” order. Unfortunately, the
createdfield resolution is too low to use it as a workaround. I’m afraid of running into usage limits some day when fetching data from stripe on every event to receive the newest data.In some of my tests, multiple webhooks for a resource have come through with the same
createdvalue. That seems not unlikely, since the field is relatively low resolution at whole seconds.For now I’ve settled on treating webhooks as a signal to retrieve, using Redlock to ensure no single resource has parallel retrieve operations happening.
The workaround that has worked for me without problems so far is to use a
createop forcustomer.subscription.created, andupsertforcustomer.subscription.updated.I have a unique key constraint on the Stripe subscription ID in my database. If creating a new record in response to
customer.subscription.createdfails with a unique key constraint error, I just ignore it. I know the initial record could only have been created in response to acustomer.subscription.updatedevent.It’s not an ideal solution, but it’s the best I could come up with. I’m watching this space for a better solution!
Here’s the high-level TypeScript code:
I think this was a little miss leading as well but each event object contains a
createdtimestamp. Checking the events it seems like thiscreatedfiled is accurate. @tomer-stripe can you confirm this?I’m guessing the best solution is to store the
createdtimestamp and only update if the new event happened after the last event. This can be a simpleupsertstyle update which a check on this column if you’re using traditional db like postgres.calling retrieve is probably the simplest solution though.