The bridge for dropshipping: syncing 30,000 products to Shopify without selling stock you no longer have

Why a loop that calls the API product by product locks up at the third supplier, how you build a bridge that survives Shopify's rate limit, and the number that actually matters: how many orders get sold against stock that is already gone.

by ·

Multi-supplier dropshipping has a problem that is not commercial: it is plumbing. Three price lists that change price and stock several times a day have to flow into one Shopify store, and the first thing anyone writes to do it — a loop that calls the API once per product — works perfectly on 200 items and stops working exactly when the catalogue grows large enough to be worth automating.

It does not fail with a clean error. It fails halfway: two thousand products updated, eight thousand not, and the store shows a mix of old and new prices for a few hours until somebody notices. The worst damage is not the delay, it is overselling — selling an item the supplier has already run out of, because the sync that would have told you was queued behind nine thousand other calls.

This piece is the architecture that holds: how you turn a set of supplier feeds into a single source of truth, how you respect Shopify's rate limit without discovering it by trial and error, and the real code of a client that stops itself before it gets throttled.

1 · Why the simple loop breaks, and why it breaks silently

The code everyone writes first is a loop: for each supplier product, call Shopify's API and update price and stock. It is correct, it is readable, and it holds up until the products become numerous enough to exceed the call limit Shopify enforces — a limit built to protect the platform, not your sync job.

The sign the simple loop is about to break

If total sync time grows faster than linearly with the number of products — twice the products taking more than twice the time — the bucket is already draining and your code is waiting longer and longer on each call. That is the moment to stop, not to add a longer timeout.

2 · The bridge: three stages, not one loop

The fix is not calling the API faster: it is calling it less. The architecture that holds splits the work into three separate stages, each with one job, and the handoff between the second and the third is what solves overselling: you only sync what has CHANGED, not the whole catalogue on every run.

Normalise — 2 min (3 suppliers)

One format for three suppliers — Every supplier feed — CSV, XML, or a proprietary API call — comes in here and leaves in the same schema: canonical code, price, stock, timestamp. The mapping table between each supplier's code and the canonical code lives here, written and versioned, not guessed on the fly.

Receives: Three supplier feeds, three formats. Produces: One list, one format

Diff — 1 min (340 diffs)

Only the rows that actually changed — The normalised list is compared against the last known state, row by row. A product whose price and stock have not changed generates no call at all: it is the difference between syncing 12,000 products and syncing the 340 rows that genuinely changed since an hour ago.

Receives: Normalised list + last known state. Produces: Only the diffs, in priority order

Write — 4 min (~340 calls)

A queue that respects the bucket — The diffs enter a queue that calls Shopify according to the cost Shopify itself reports on every response — not a fixed number of calls per second, but a live read of how full the bucket still is. Stock drops to zero go first: those are the ones that prevent overselling.

Receives: The diffs, in priority order. Produces: The store aligned, no throttling

The stage that changes everything is the second one. A 30,000-product catalogue updated every hour almost never has more than a few hundred rows that genuinely changed — the rest are prices identical to an hour ago. Comparing before writing turns a sync from twelve thousand calls into a few hundred, and it is the difference between staying inside the bucket and draining it every time.

3 · The code that stops itself before it gets throttled

Shopify's GraphQL API reports the cost of every call and how much bucket is left in the response itself — you do not need to guess, you need to read it. This is a minimal client that waits the right amount of time instead of discovering the limit through a string of 429 errors.

shopify-client.ts

async function callRespectfully<T>(query: string, variables: object): Promise<T> {
  const response = await fetch(SHOPIFY_GRAPHQL_URL, {
    method: "POST",
    headers: { "X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  })

  const body = await response.json()
  const cost = body.extensions?.cost?.throttleStatus

  if (cost && cost.currentlyAvailable < cost.maximumAvailable * 0.2) {
    // Bucket below 20%: wait for the refill instead of calling again
    // and discovering the throttle on the very next request.
    const missing = cost.maximumAvailable * 0.5 - cost.currentlyAvailable
    const waitMs = (missing / cost.restoreRate) * 1000
    await new Promise(r => setTimeout(r, waitMs))
  }

  if (body.errors?.some((e: { extensions?: { code?: string } }) =>
    e.extensions?.code === "THROTTLED")) {
    // Throttled anyway: exponential backoff with jitter, not an immediate
    // retry that would land in the same still-empty bucket.
    const wait = 1000 * (1 + Math.random())
    await new Promise(r => setTimeout(r, wait))
    return callRespectfully(query, variables)
  }

  return body.data as T
}

The two conditions are not redundant: the first prevents throttling by reading the bucket BEFORE calling again; the second handles the case where another part of the system — an installed app, a webhook — consumed bucket in the meantime, which no local count can ever predict.

Notice what this code does NOT do: it does not compute “how many calls per second I'm allowed” as a fixed number. Shopify varies the cost per point depending on the store's plan and the operation type, and a fixed number written today is wrong the day you change plans. Reading the bucket from the response is the only version that never goes stale.

4 · The number that matters: not speed, overselling avoided

A client with three suppliers and around 30,000 shared SKUs moved from an overnight batch sync — with a twelve-hour risk window during which the site could sell stock already gone — to a diff run every fifteen minutes, on the same infrastructure, without upgrading their Shopify plan.

When this architecture is NOT worth it

If you have one supplier and under two thousand SKUs, a simple loop with a fixed delay between calls is enough, and building three separate stages would be complexity paid for a problem you do not have yet. The signal to move to the three-stage architecture is the same as in section one: sync time growing faster than linearly, or the first overselling incident that generates a refund.

Half an hour on your case, before the next overselling incident

If you already sync with Shopify and the risk window feels too long, bring the numbers: how many suppliers, how many SKUs, how often you update today. In half an hour we work out whether the problem is volume, feed format, or simply a loop that calls the API without reading the bucket.

If your catalogue is still under two thousand rows, I will tell you: you do not need this architecture, a well-tuned fixed delay is enough. Building it now would be complexity paid up front for a problem that may never arrive.

The service behind this article

Custom ecommerce development

If this was useful, carry on from here

FAQ

How do you sync multiple suppliers to Shopify without exceeding the API limits?

By splitting the work into three separate stages: normalise supplier feeds into one format, diff against the last known state to isolate ONLY the changed rows, and write to Shopify respecting the cost the API itself reports on every response. The stage that matters is the second one: a 30,000-product catalogue updated hourly rarely has more than a few hundred rows that genuinely changed, and syncing only those cuts calls by 90% or more.

What is Shopify's leaky-bucket rate limit and how do you handle it?

Every Shopify store has an amount of available “cost” that gets spent on each API call and refills over time. A client that reads this value from the response itself — instead of guessing a fixed number of calls per second — can slow itself down before getting throttled, and know exactly how long to wait instead of retrying blindly.

How do you prevent overselling in multi-supplier dropshipping?

By prioritising stock drops to zero over price updates in the write queue. It is the only kind of change that, if delayed, results in a sale against stock that no longer exists — which is why it has to be written first, not in arrival order.

REST or GraphQL for bulk Shopify updates?

GraphQL, for a specific reason: it reports the cost of every call and the bucket level directly in the response (extensions.cost.throttleStatus), while REST requires inferring the limit from headers and reacting afterwards. On a high-volume sync, knowing a call's cost in advance beats discovering it through a string of errors.

Nadia Maar