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 Nadia Maar ·
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.
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.
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.
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.
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
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.