Skip to content
heapbyte - A name of excellence

Engineering · 5 October 2026

Plus doesn’t move a big catalogue faster. It fixes it faster

The migration plan has a line for upgrading to Plus, and the justification is one sentence: twenty-eight thousand products is a lot. It is. Nobody in the meeting asks which limit the catalogue is expected to hit, because the question sounds pedantic next to a number that size, and Plus is the plan for big stores. The upgrade goes in, the import runs, and it takes exactly as long as it would have done on Basic.

7 min read
Written by the HeapByte engineering team

Shopify's analytics are strong, and they only see Shopify's orders.

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.

ts
/** 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]) };
}
Two refusals carry this. The first is the cost: a mutation starts at ten points, but the real figure depends on what the call returns, and the only reliable number is the one Shopify reports back on a real call — so the function will not run on an estimate of zero, and a plan built on a guessed cost is a plan built on nothing. The second is the bulk branch, which returns no number at all. Bulk operations have no rate limit on any plan, so their duration is not something the plan decides, and a planning tool that printed one would be inventing the very comparison this article argues against.

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.

---

Insights

Apply this to your store.

An audit turns the general principle into a specific list of changes, ordered by what actually pays back.