Webhooks do not arrive in the order they were sent
Two events a second apart can overtake each other on the way to your automation. The older one lands last, overwrites the newer value, and nothing in the log looks wrong. How to make order irrelevant, and how to check whether it has already happened to you.
Webhooks do not arrive in the order they were sent
Every provider says so somewhere in the documentation. Stripe says it, Shopify says it, HubSpot says it too. Almost nobody reads it, because when you test you fire one event at a time and the problem never turns up.
In production it does turn up, and it does not look like an ordering problem. It looks like a system that occasionally forgets what you told it.
A logistics client of ours wired their carrier's status webhooks into a customer portal. Four statuses per shipment: accepted, in transit, out for delivery, delivered. Three weeks in, a customer watched a shipment go to "delivered" and then, twenty minutes later, back to "out for delivery". The carrier had it right in their own system. The events had been sent in the right order. They just did not land in it.
Why the order breaks
Nothing exotic is happening. The sender works its queue with several processes at once, so whichever finishes first depends on load. A delivery that fails, maybe your endpoint took two seconds too long, gets retried half a minute later, and the next event walks straight past it while it waits. On your side, Make and n8n handle incoming calls concurrently, so run two can finish ahead of run one.
Events hours apart will effectively always arrive in order. Events a second apart are a coin flip. And a second apart is ordinary: someone creates a record and immediately fixes a typo in it, a driver scans twice, a batch job reports ten lines at once.
Last write wins, even when it should not
The default build has no concept of order. Webhook in, map fields, update record. Whoever writes last wins. When the last write carries the older event, the stale value sits there permanently, because nothing further is coming to correct it.
Anything holding a state rather than a history is exposed: shipment status, order status, delivery address, payment status, stock level.
Stock is where it costs money. If your warehouse reports absolute numbers, 40 units and then 12 after picking, and the 40 lands last, your shop is advertising stock that does not exist. It will keep selling it. You find out when someone tries to pack the order.
Your received-at timestamp proves nothing
Plenty of workflows stamp the event on arrival with now(). That makes every event the newest one by definition, and the check you believe you built checks nothing at all.
The timestamp has to come out of the payload: occurred_at, event_time, updated_at, whatever the provider calls it. If no such field exists, treat that as an argument for fetching rather than trusting the push. And normalise everything to UTC before comparing, or the comparison will be wrong twice a year.
Three ways to fix it
Add a field to the target record, something like last_event_at. The workflow reads it, compares it against the timestamp in the payload, and writes only when the incoming event is newer. Older events go to a log instead of the record. Two extra modules, and it covers most of what you will meet.
Where the payload carries no usable timestamp, work with allowed transitions instead. Give each status a rank and only ever write upwards. Nothing goes from delivered back to out for delivery unless a human cancels it, and cancellation gets its own path.
The sturdiest option treats the event as a doorbell and nothing more. When it rings, the workflow calls the API and writes whatever the source reports at that moment. Order stops mattering. You pay one extra call per event, which rules the approach out when you are already close to a rate limit.
One caveat applies to all three. If two runs for the same shipment execute at the same time, both can read the same old value before either writes, and the comparison does nothing for you. Closing that gap needs a conditional update in the target system, or a queue that allows one run per record at a time.
The update that beats the create
The version we run into most has nothing to do with status. A source system emits "created" and then "updated" moments later. The update overtakes, the workflow searches for a record that does not exist yet, and depending on how it was built it either fails or creates one. Then "created" arrives and creates a second.
Use an upsert keyed on the source system's ID rather than a search-then-create. If the target cannot do that, wait a few seconds and search once more before creating anything.
Test it on purpose
You will never trigger this by hand. So before go-live, post two events to your endpoint in swapped order, with the timestamps still in their original sequence, and check which status survives. Send the same event twice while you are in there.
Checking whether it has already happened
If your runs are logged, look for records where a later run carried an earlier event timestamp. Without logs, look for states that ran backwards: a shipment back on the road after delivery, an order that reopened after it closed.
The logistics client had 61 of them across six weeks, four in a thousand. That sounds survivable until you count it in phone calls. Sixty-one customers saw something in the portal that contradicted the person they rang up about it.
The workflow compares timestamps now and drops older events into a separate table. Seventy-four rows in the first fortnight.