Back to BlogTroubleshooting

Testing Inventory and Order Sync Across Channels (No Overselling)

Rupak Amin

Founder & Lead Engineer, RAITHub

8 min read

When you sell the same stock on more than one channel, overselling comes from the gap between them: two orders for the last unit arrive at once, or a stock update applies twice or out of order. The fix: make every stock change idempotent and serialised at the database, reconcile channels on a schedule, and test the race directly by placing two concurrent orders for the last unit.

If you would rather have your sync audited and tested, see how RAITHub would test this below. First, where it breaks and how to prove it does not.

Why does multi-channel selling cause overselling?

Each channel holds its own idea of what is in stock, and they update each other with a delay. In that delay, two channels can both sell the last unit. Even on one channel, two buyers checking out at the same moment can both pass a naive "is there stock?" check before either decrement runs. The root cause is the same as a single-store race condition, covered with the core SQL in stopping overselling with stock reservation; multi-channel just adds a second way to hit it, through sync messages that arrive late, out of order or more than once.

Where exactly does inventory sync break?

FailureWhat happensTest that catches it
Concurrent ordersTwo orders for the last unit both succeedFire both at once; assert one 200 and one out-of-stock
Duplicate sync messageA stock update applies twice; counts driftReplay the same message; assert stock changes once
Out-of-order updatesAn old level overwrites a newer oneApply updates with versions out of order; assert newest wins
Lost updateA channel never receives a decrementReconciliation job flags the mismatch
Partial failureOrder created, stock not reserved (or vice versa)Kill mid-transaction; assert all-or-nothing

How do you make a stock change safe to run twice?

Do the whole order-and-reserve in one database transaction, lock the stock row, and tag each sync message with a unique id so a replay is ignored. The lock serialises concurrent orders; the id makes the update idempotent.

-- Reserve stock for one line, safely, inside the order transaction.
-- Returns a row only if enough stock was available; locks the row so a
-- concurrent order waits instead of racing.
UPDATE inventory
   SET available = available - :qty,
       version   = version + 1
 WHERE sku = :sku
   AND available >= :qty
RETURNING available, version;
-- No row returned = not enough stock = reject the order.

For updates that come from another channel (not an order), record the message id and skip duplicates, and ignore an update whose version is older than what you already hold:

-- Apply an external stock level, idempotently and in order.
INSERT INTO sync_messages (id) VALUES (:message_id)
  ON CONFLICT (id) DO NOTHING;         -- already applied: stop here
-- Only when the insert created a new row:
UPDATE inventory
   SET available = :level, version = :incoming_version
 WHERE sku = :sku
   AND :incoming_version > version;      -- newest wins

How do you test the race directly?

Do not trust a single sequential test; fire the conflicting operations at the same time and assert the invariant. For concurrency, launch both requests together:

// tests/api/no-oversell.spec.ts
import { test, expect } from '@playwright/test'
import { setStock, order, stockOf } from './helpers'

test('two orders for the last unit: exactly one wins', async ({ request }) => {
  await setStock('SKU-1', 1)
  const [a, b] = await Promise.all([order(request, 'SKU-1', 1), order(request, 'SKU-1', 1)])
  const ok = [a, b].filter((r) => r.status() === 200)
  expect(ok.length).toBe(1)        // one succeeds
  expect(await stockOf('SKU-1')).toBe(0) // never negative
})

Add a duplicate-message test (apply the same sync id twice, assert stock changes once) and an out-of-order test (apply an older version after a newer one, assert the newer level stays). These three cases cover the ways sync oversells. The general approach to payment and event races is in testing payments and webhooks.

How do you catch drift you did not predict?

Run a reconciliation job on a schedule that compares each channel's stock to your source of truth and flags any mismatch beyond a tolerance. Overselling is often a slow leak, not a single dramatic failure, so the reconciliation is what turns "a customer complained" into "the job alerted us at 2am". Make the reconciliation itself a tested path, and test it end to end after every deploy (regression testing after every deploy).

Buy, build or hire?

OptionWhat you getChoose this when
An inventory-sync SaaSConnectors between channels and a central stock viewYou sell on standard channels and accept the tool's sync model and latency
Your platform's built-in multi-channelSync handled within one ecosystemAll your channels live inside that one platform
A custom sync layer you testFull control of idempotency, ordering and reconciliationYour channels, warehouses or rules do not fit an off-the-shelf connector
A managed QA teamThe concurrency, duplicate and reconciliation suites in CIYou have sync but overselling keeps happening and nothing tests the races

How long does it take to test this yourself?

For an existing sync, expect about one to two weeks of one engineer's time to add the concurrency, duplicate-message and out-of-order tests, make the reconciliation job testable, and gate them in CI. The main risk of doing it yourself is writing sequential tests that pass while the real failure only appears under concurrency: a test that places two orders one after another will never catch the race that two simultaneous orders do.

Why RAITHub for this

RAITHub built and runs TheSkinProof, the founder's own marketplace, with per-variant stock decremented inside the order transaction, and Sundor Skin, whose 146-table core enforces stock and credit rules in the database with 530+ tests. Testing the money and stock races is part of the QA as a Service offer, and the same engineers can fix what the tests expose. See the eCommerce industry page for the wider picture.

When you don't need us

  • All your channels sit inside one platform's native multi-channel. Its own sync is tested by the vendor.
  • You already test concurrency, duplicates and reconciliation. Keep adding a case with every incident.
  • You need a certified audit of your logistics provider. That is outside application-level QA.

How RAITHub would test this

  • Scope: add concurrency tests for the last unit, duplicate-message and out-of-order sync tests, an all-or-nothing transaction test, and a tested reconciliation job; gate them in CI and run them after every deploy.
  • Ways to buy it: a fixed-price audit with a ranked bug report, a monthly QA plan, or a dedicated QA team RAITHub manages and bills monthly. RAITHub does not place testers under your management.
  • Timeline: fixed in the written quote after the audit, based on how many channels and warehouses you sync.
  • What you receive: the concurrency and reconciliation suites in your repository, a ranked bug report, a fixed quote to fix the issues if you want, IP assigned to you and an NDA as standard.
  • Next step: a free 15-minute technical audit, then a fixed written quote.

See the app testing service, read about a full SaaS build, or book the free 15-minute audit.

Documentation checked on 10 October 2026.

Frequently asked questions

Why do I oversell even though I track stock?

Tracking is not the same as serialising. Two orders for the last unit can both pass a naive stock check before either decrement runs, and a sync message from another channel can arrive late, out of order or twice. Overselling lives in those gaps, not in the count itself.

How do I test that I never oversell?

Fire two orders for the last unit at the same time and assert exactly one succeeds and stock never goes negative. A sequential test will pass while the real race fails, so the test must run the conflicting operations concurrently.

How do I make inventory sync idempotent?

Tag each sync message with a unique id, record applied ids, and skip duplicates. Ignore any update whose version is older than what you already hold, so a late message cannot overwrite a newer stock level.

What is a reconciliation job and why do I need one?

A scheduled check that compares each channel's stock to your source of truth and alerts on any mismatch. Overselling is usually a slow leak, so reconciliation turns a customer complaint into an automatic alert before orders are affected.

Can RAITHub test sync it did not build?

Yes. RAITHub starts with the stock and order paths, writes concurrency, duplicate and out-of-order tests plus a tested reconciliation job, and gates them in CI, through its QA as a Service.

inventory sync testingmulti-channel inventoryoversell preventionorder syncstock reconciliationconcurrency testing

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.