Why the Same Wholesale Order Arrives Twice

A buyer calls about a credit note. They received two pallets of the same order, on the same day, from you.

Your system shows two orders, three minutes apart. Same purchase order number, same lines, same delivery address. Everyone assumes somebody clicked twice.

Usually nobody did. The sender never got a confirmation and tried again, and the second attempt landed on a write that had already succeeded. Then the warehouse did exactly what it was told, twice.

The short answer: Duplicate orders are almost always a retry problem, not a data quality problem. A write succeeds, the confirmation never comes back, and something tries again. Fix it by sending a stable reference on every write, upserting against that reference instead of inserting blindly, and storing the downstream identifier back on the source order.

An idempotency key is a unique value the sender attaches to a write so that repeating the same request produces the same single result instead of a second record.

Why are duplicate orders a retry problem, not a data problem?

Teams reach for the wrong tool here. They add a duplicate report, a cleanup script, a rule in the ERP, a person who checks the order list every morning. All of that treats the symptom.

The cause is almost always a system that could not tell whether its last write worked.

The IETF working group draft on the Idempotency-Key HTTP header field describes the situation exactly: a client sends a POST request and the request times out, leaving the client uncertain about the status of the resource. It does not know whether the record was created, whether the server finished processing, or whether it can safely retry. Repeating the request, the draft notes, can result in duplication.

That is your duplicate order, written down as a protocol problem years before it reached your warehouse. The sender had two bad options and picked the one that ships twice instead of not at all.

Which means the fix belongs at the write, not in the cleanup.

What are the six ways the same order gets created twice?

CauseTell-tale signFix
Request timed out after the write succeededTwo records seconds apart, identical payload, only one logged confirmationIdempotency key on the request
At-least-once delivery replaying the same eventThe same event identifier processed more than once, often minutes apartRecord processed event identifiers and ignore repeats
Batch job rerun after a partial failureA block of duplicates all created within the same minuteUpsert on the external reference instead of insert
Two writers: two integrations, or one integration and one personDuplicates with different creators, or slightly different field formattingOne system owns the write. The others read
Double submit on a manual entry screenTwo records within seconds, same user, same sessionDisable the button on submit and key the write server side
Order edited so the downstream system creates a new recordThe original plus a near-identical record with one line changedUpdate by reference on edit. Make cancel and replace explicit

Five of these six are machine behavior. Only one involves a person clicking twice, and it is the one everybody blames first.

Worth noting where the duplicates land. If your warehouse or third-party logistics provider receives orders through its own connection, a duplicate can reach the dock without ever appearing twice in your ERP. That class of problem is covered in 3PL integration errors. And if purchase orders arrive as PDFs that get read automatically, the same order emailed twice by a buyer looks like two orders unless you match on the purchase order number, which is part of AI order entry accuracy.

What do real systems document about duplicates?

Two concrete, published examples. Neither is an edge case. Both are the documented normal behavior of systems B2B teams run every day.

Shopify tells you to expect duplicate webhooks and gives you the tool to drop them. Shopify's guidance on ignoring duplicate webhooks states that your app might receive the same webhook more than once, for example after a network timeout or a retry, and recommends two options: process webhooks using idempotent operations so receiving the same one twice does not change the outcome, or extract the X-Shopify-Webhook-Id header, check it against a persistent store, and skip processing if you have seen it. It also distinguishes that header from X-Shopify-Event-Id, which correlates deliveries originating from the same merchant action, so more than one subscription to the same topic produces separate deliveries you can still tie back to one event.

The retry side is documented too. Shopify's webhook troubleshooting page states that failed calls are retried up to eight times over a four-hour period, and that your endpoint has five seconds to respond before a delivery counts as failed. A slow endpoint that finishes the work in six seconds has created the order and reported failure. Shopify then retries, correctly, and you have two.

Stripe replays the original result instead of doing the work again. Stripe's idempotent requests documentation describes the pattern in full: the client generates a unique key, sends it in an Idempotency-Key header, and Stripe saves the status code and body of the first request made with that key, whether it succeeded or failed. Subsequent requests with the same key return that same saved result. Three details matter for order integrations. Keys are up to 255 characters. The idempotency layer compares incoming parameters against the original request and errors if they differ, which stops a changed order quietly reusing an old key. And keys can be removed after they are at least 24 hours old, after which a reused key generates a new request.

That last point is the one to take home. An idempotency key protects you against retries within a short window. It is not a permanent record of what you have already created. For an order that can be resent tomorrow, you also need a stable reference stored on both sides.

Message systems say the same thing in their own words. Google Cloud's Pub/Sub subscription overview states that at-least-once delivery with no ordering guarantees is the default on all subscription types, that messages may be delivered more than once, and that accommodating this requires your subscriber to be idempotent. Their exactly-once delivery feature exists, but only for pull subscriptions. Push and export subscriptions do not support it.

Duplicates are not a defect in these systems. They are the advertised behavior, and the receiver is expected to handle them.

What are the fixes, in order of value?

Do them in this order. Each one is cheaper than the one that follows it, and each one catches what the previous one misses.

  1. Put an idempotency key or stable external reference on every write. One value derived from the order itself, not from the moment of sending. Your store order ID is usually the right choice. A timestamp or a random value generated per attempt is the wrong choice, because a retry generates a new one and you are back where you started.
  2. Upsert against that reference instead of inserting blindly. Look for a record with this reference. If it exists, update it. If it does not, create it. One decision, one place, every time. This is the single change that removes most duplicates, and it works even for the batch job rerun that idempotency keys do not cover.
  3. Store the downstream identifier back on the source record. When the ERP returns its order number, write it onto the store order. Now every order carries proof it was synced, a human can answer "did this go through?" in one click, and your reconciliation has something to join on.
  4. Add a natural key check as a safety net. Customer, plus purchase order number, plus order date. Flag matches for review rather than blocking them, because a buyer occasionally does order the same thing twice in a day and that order is real.
  5. Run a duplicate report somebody actually reads. Weekly, one screen, grouped by natural key. Its job is not cleanup. Its job is to tell you that steps one to four have a hole in them.

Steps one and two are a build task. Steps three to five are operational. Do not let the operational ones substitute for the build ones, which is the trade most teams make by accident.

Pair this with detection. A duplicate and a missing order come from the same class of plumbing problem, and the daily count that catches one catches the other, as covered in what happens when your order flow stops at 3am.

What does a duplicate cost once it reaches the warehouse?

This is why prevention beats cleanup, and the gap is not close.

A duplicate caught in the order list is a few minutes of somebody's attention. The same duplicate caught after dispatch is a physical object in a van, and it comes back as a return, a credit note and a conversation with a buyer who now wonders what else you get wrong.

Illustrative cost of one duplicate that shipped as a pallet:

LineIllustrative cost
Second pick and pack, 12 minutes at €35 per hour€7
Outbound freight€45
Return freight€55
Receiving and put away, 20 minutes€12
Credit note and bookkeeping, 15 minutes€9
Customer service, 20 minutes€12
Totalabout €140

Illustrative figures. Use your own freight rates and hourly cost.

Now compare the same duplicate caught at different points:

Where the duplicate is caughtIllustrative cost
At the write, by an idempotency key or upsert€0
In the order list, before pickingabout €3
After picking, before dispatchabout €19
After it shippedabout €140

At four shipped duplicates a month, that is roughly €560 a month, or €6,720 a year, before you count the buyer's opinion of you. Those are illustrative numbers. The ratio is the point, not the euros: preventing it costs a build task once, and cleaning it up costs a bill every time.

What do you do about the duplicates already in your system?

Fix the cause first. Otherwise you will clean the same list every month and call it maintenance.

Then:

  1. Find them with the natural key. Group orders by customer, purchase order number and date, and list groups with more than one record. Do this as a report, not as a delete script.
  2. Sort by what happened physically. Not shipped, shipped, or shipped and returned. These are three different jobs, and only the first one is easy.
  3. Cancel, do not delete. Cancel the unshipped copy so the audit trail, the stock movement and the accounting entry all survive. Deleted orders break reconciliation later and hide the evidence you need to prove the cause is fixed.
  4. Reconcile the money separately. A shipped duplicate has already touched stock and possibly an invoice. Route those to whoever owns your ledger rather than fixing them in the order screen.
  5. Count what you found. That number is your baseline, and you will want it in six weeks.

When is chasing duplicates not worth it?

Say no when:

  • It has happened twice this year. Two duplicates a year on 3,000 orders does not justify rebuilding a write path. Add the natural key check and move on.
  • The duplicate is real. Some buyers genuinely reorder the same items the same week. Flag those for review. Never auto-merge them.
  • Orders still arrive by email and get typed in. A duplicate here is a people and process problem. Fixing the order intake is worth more than fixing the write, and that comes earlier in the B2B e-commerce automation sequence.
  • The connector is not yours to change. If a packaged integration does a blind insert and offers no setting, your leverage is a check on the receiving side, not a rewrite.
  • Nobody owns the write path. Without an owner, step one never gets built and you inherit a permanent duplicate report instead.

How do you measure whether it worked?

Write these down before you change anything.

  • Duplicates per 1,000 orders. The headline number. Rates, not counts, so growth does not flatter you.
  • Where each one was caught, using the four stages in the cost table above. Duplicates moving from "after it shipped" to "at the write" is the result, even before the rate falls.
  • Credit notes attributed to duplicates, per month.
  • Share of orders carrying a downstream identifier. If this is not close to 100%, your reconciliation cannot see what is missing.
  • Retries per day on the order write. Falling retries mean you fixed the timeout, which is the root cause behind the most common duplicate of all.

Check again after a few normal weeks. If the rate is unchanged but every duplicate is now caught before picking, you still removed most of the cost.

Do this this week

  1. Pull last quarter's orders and group them by customer, purchase order number and date. Count the duplicates. Most teams find more than they expected.
  2. For each one, work out which of the six causes produced it. The timestamps tell you more than the payloads.
  3. Check whether your order write sends a stable reference today. If it sends nothing, that is your first build task.
  4. Turn the blind insert into an upsert on that reference.
  5. Write the downstream order number back onto the source order, so tomorrow's question has a one-click answer.

That is the whole fix, and it is smaller than the cleanup process it replaces. It is how we approach software integrations generally: make the write safe once, rather than watching it forever.

Not sure where yours come from? Book a 30-minute call and we will trace one duplicate with you.

Find out where your duplicates come from

Bring one pair of duplicate orders from the last month, with their timestamps, and we’ll trace it with you on a 30-minute call.

On the call, we’ll:

  • Work out which of the six causes produced it, using the timestamps and the payloads
  • Check what your order write sends today: a stable reference, an idempotency key, or nothing
  • Recommend the smallest change that stops it, and what to do with the duplicates you already have

We start with the store, ERP and tools you already run. If the honest answer is “this happens twice a year, leave it,” you’ll hear that too. No technical brief needed.

Book my 30-minute call

FAQ

What causes duplicate orders in an e-commerce integration?

Most duplicates come from retries, not from bad data. A write succeeds but the confirmation is lost to a timeout, so the sender tries again. The other common causes are at-least-once event delivery replaying the same message, a batch job rerun after a partial failure, two systems writing the same order, a double submit on a manual entry screen, and an order edit that creates a new downstream record.

What is an idempotency key and how does it stop duplicate orders?

It is a unique value the sender attaches to a write so the receiver recognizes a retry of the same request. Stripe's documentation describes the pattern well: the status code and body of the first request are saved against the key, and later requests with that key return the same saved result instead of doing the work again. Derive the key from the order itself, never from the moment of sending.

Do idempotency keys expire?

Yes, and this catches people out. Stripe documents that keys can be removed once they are at least 24 hours old, and that reusing a pruned key generates a new request. So an idempotency key protects you against retries within a short window, not against the same order being resent next week. For that you need a stable external reference stored on both sides and an upsert rather than an insert.

Why do webhooks deliver the same order event twice?

Because at-least-once delivery is the normal guarantee. Google Cloud's Pub/Sub documentation states that at-least-once delivery is the default on all subscription types and that consumers must be idempotent. Shopify's documentation says your app might receive the same webhook more than once after a network timeout or retry, and recommends deduplicating on the X-Shopify-Webhook-Id header or making your processing idempotent.

Should I delete duplicate orders from my system?

Cancel them rather than deleting them. Cancelling keeps the audit trail, the stock movement and the accounting entry intact, while deletion hides the evidence and breaks reconciliation later. Handle unshipped duplicates in the order screen, and route anything already shipped or invoiced to whoever owns your ledger, because stock and money have already moved.

How do I tell a duplicate from a genuine repeat order?

Compare the customer, the purchase order number and the order date, then look at the gap between the two records. Records created seconds or minutes apart with identical lines are almost always a retry. A genuine repeat usually carries a different purchase order number, a different delivery date or a changed quantity. Flag close matches for a person to review instead of merging them automatically.

Sources

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *