The ceilings a catalogue actually meets
It is worth listing what a large catalogue runs into on Shopify, because most of it is not tied to the plan at all.
The variant ceiling is 2,048 per product on every plan. Array inputs are capped at 250 items on every API. The one documented throttle on catalogue growth — once a store holds 500,000 variants, no more than 10,000 new ones can be created per day — names no plan, and a 28,000-product catalogue is nowhere near it.
Plus does raise some ceilings. URL redirects go from 100,000 to 20,000,000. Metaobject definitions go from 128 to 256. Those are real, and a catalogue of this size meets neither: a serious consolidation produces a few hundred redirects, and a catalogue that needs more than 128 kinds of structured entity has a modelling problem rather than a plan problem.
What Plus raises by the largest margin is API throughput. The GraphQL Admin API restores 100 points a second on a standard plan, 200 on Advanced and 1,000 on Plus. Tenfold is the number that makes people reach for the upgrade. The question is whether the migration ever spends it.
The import never touches the rate limit
It does not, if it is built the way a migration of this size should be built.
Bulk operations, Shopify says, “don’t have the max cost limits or rate limits that single queries have”. A catalogue import expressed as bulk mutations — a JSONL file, staged, submitted, results collected — runs outside the bucket entirely, on every plan. That is also the right shape for reasons that have nothing to do with speed, which the CSV import article covers: line-level errors you can read, and a file you can resubmit.
So for the import itself, the tenfold is spent on nothing. The plan is not what decides how long it takes.
Where the plan shows up: the second week
The import is not the expensive part of a large migration. The corrections are.
After the first full run there is a long tail of work that does not fit a bulk operation: a supplier sends forty fixed descriptions; a mapping rule was wrong for one product type and three hundred products need one field changed; a price file arrives on Tuesday. These are individual calls, each one charged against the bucket, and they happen every week for months. On the Fabric Co. migration — roughly 28,000 supplier-driven products — the case study’s own words are that automation made “repeat runs and incremental corrections feasible across tens of thousands of products”. That is the workload where the plan is visible.
The arithmetic is simple and worth doing before anyone argues about it. Twenty-eight thousand single calls at ten points each is 2,800 seconds of restore time on a standard plan — about forty-seven minutes — and 280 seconds on Plus. A correction pass that touches a nested connection and costs 400 points a call takes ten times longer again. If the team runs that pass daily, the difference between forty-seven minutes and five is a difference in how the business works. If it runs once, it is a difference nobody notices.
/** GraphQL Admin restore rates, points per second. */
const RESTORE_RATE = { standard: 100, advanced: 200, plus: 1000, enterprise: 2000 } as const;
type Plan = keyof typeof RESTORE_RATE;
type Job = {
calls: number;
/** Measured, not guessed: extensions.cost.actualQueryCost from a real call. */
measuredCostPerCall: number;
/** Can this be expressed as a bulk operation? */
bulkEligible: boolean;
};
/**
* How long a job takes on a plan, and whether the plan is what decides it.
* Steady-state only: the bucket absorbs a burst, then the restore rate rules.
*/
export function estimate(job: Job, plan: Plan): { route: "bulk" | "throttled"; seconds: number | null } {
if (!Number.isInteger(job.calls) || job.calls < 0) throw new Error("calls must be a non-negative integer");
if (!(job.measuredCostPerCall > 0)) throw new Error("measure the cost first");
// Bulk operations have no rate limit on any plan, so the plan does not
// decide how long they take — and this function will not pretend to know.
if (job.bulkEligible) return { route: "bulk", seconds: null };
return { route: "throttled", seconds: Math.ceil((job.calls * job.measuredCostPerCall) / RESTORE_RATE[plan]) };
}Measure the cost before you plan
The number everyone gets wrong is the cost per call, because it is not a constant. A mutation starts at ten points, but the cost depends on what the call returns, and a query that pulls variants with their metafields can cost many times what the documentation’s example suggests.
Shopify reports the actual cost in the response to every call. So the planning step is one real call against a development store with the real shape of the data, reading the reported cost, and multiplying. It takes five minutes and replaces an argument with a number.
Two other limits belong in the same calculation, and neither moves with the plan. A single query cannot cost more than 1,000 points on any plan, so a correction that wants to fetch and update a lot in one call has to be split whether the store is on Basic or Plus — Plus raises how fast the bucket refills, not how much one call may take from it. And list inputs are capped at 250 items on every API, which is the lever worth pulling before the plan is. Where a mutation accepts a list, one call can carry many changes, and cutting the number of calls by a factor of fifty does more than raising the refill rate by ten. Not every correction fits that shape — many mutations work on one product at a time — but the ones that do should be batched before anybody prices an upgrade.
The order of operations, then: express what you can as a bulk operation, batch what you cannot, measure what is left, and only then ask whether the plan is the constraint. On most catalogues it turns out not to be.
What this does not say
It does not say you should not be on Plus. Checkout extensibility, B2B at scale, expansion stores and custom apps with Functions are all reasons a business runs on Plus, and none of them is in this article. If you are on Plus for those, the throughput is a bonus.
It does not cover bursts. The bucket absorbs a burst and the restore rate governs the average; for a long correction pass, the average is what matters, which is why the estimate above uses it.
And it does not make single calls the right tool. Anything expressible as a bulk operation should be one, on any plan. Throughput matters for the work that cannot be.
When this needs an engineer
It does not need one to decide the plan. Do the arithmetic above with one measured call, and if the answer is minutes on the plan you have, the plan is not your problem.
It needs one when the catalogue is large enough that corrections are a standing workload rather than an event — when supplier data changes weekly and the store has to keep up — because then the split between bulk and single calls, the measured cost, and the correction pipeline are the system, and the plan is one input to it. That work sits with our other migrations and catalogue data engagements, and on a Plus store it is part of what Shopify Plus engineering means.
Send us the store and the symptom.
---
