If you have 50,000 SKUs to move onto Shopify, the admin CSV importer is not going to be the whole answer, but it will do more than people expect. The short version: split your catalogue into files under 15 MB each, load products as drafts with no images, then handle images, multi-location inventory, manual collection membership and metafields as separate passes. If you want it done in one run instead of eleven, use the GraphQL Admin API's bulk operations with a staged JSONL file. Everything else in this post is the detail of why bulk product import on Shopify falls over at scale, and what we do about it.
The limit that actually bites is 15 MB, not a row count
Shopify's product CSV importer doesn't publish a maximum number of rows. It publishes a maximum file size: 15 MB. That sounds generous until you remember that the product CSV is variant-level. One row per variant, plus extra rows for every image past the first, plus extra rows for every option value. A single product with 12 variants and 6 images is 12 rows, some of them carrying a 4 KB HTML description in the Body column.
So do the arithmetic before you touch the importer. Take 50,000 products averaging 3 variants: 150,000 variant rows. Add images beyond the first — say 4 per product, three of which need their own rows: another 150,000. You're at 300,000 rows. If the average row weighs 700 bytes once you strip the description out of everything but the first row of each product, that's 210 MB. Divide by 15: fourteen files, minimum. In practice we cut to 18,000–20,000 rows per file and check the byte count rather than the row count, because one long description column can double a file's weight without changing the row count at all.
Each of those files goes into a queue. The importer emails you when it finishes, and it will happily report success while quietly skipping rows it didn't like. Which brings us to the real problem with CSV at this scale: you don't find out what failed until you count.
What the product CSV silently cannot do
The columns look comprehensive. They aren't. The gaps we hit on almost every large migration:
- Multi-location inventory. The product CSV carries one inventory quantity per variant. If you run a warehouse plus three retail counters, that number lands in one place and the rest of your stock ledger is wrong. You reconcile through the separate inventory CSV export/import or the API. This is the single most common thing that breaks on a first pass, and it breaks quietly, because the products all look fine.
- Manual collection membership. Product CSV rows don't say which manual collections a product belongs to. Smart collections built on tags, type or vendor will populate themselves, which is why we push merchandisers hard to express as much of the taxonomy as possible in tags before migration. Anything genuinely hand-curated needs its own import or an API pass.
- Variants beyond 100 per product. The default ceiling is 100 variants on a product. If you're importing something like a made-to-measure or industrial-fastener catalogue where a single parent has 400 combinations, you split the product or you restructure the options. Decide which before you export, not after.
- Metafields at volume. The importer does accept metafield columns, and it works. But definitions have to exist first, types have to match exactly, and a list-type metafield formatted wrong fails the cell without failing the row. For Indian catalogues this is where HSN codes, GST rate, FSSAI licence number, country of origin and net quantity live, so it isn't optional detail.
- Redirects. Nothing in the product CSV creates a 301 from your old URL. That's a separate CSV in the URL Redirects section, and on a 50,000-URL catalogue it's the difference between keeping your organic traffic and rebuilding it.
One more, because it costs people real money: the Handle column is the match key. Re-import the same product with a different handle and you get a duplicate, not an update. We've seen a store end up with 78,000 products because someone regenerated handles from a cleaned-up title field between passes.
API rate limits, and why 2 requests a second is the number that shapes your plan
Once you go the API route, throughput is capped by a leaky bucket, not by your server. On standard Shopify plans the REST Admin API allows about 2 requests a second with a burst bucket of 40. Plus plans get roughly ten times that. GraphQL is metered differently, on a calculated cost per query with a points-per-second restore rate rather than a request count, which is why a well-shaped GraphQL mutation moves far more data per unit of throttle than the REST equivalent. Shopify has revised these numbers more than once, so check the current API docs against your plan before you size a job.
Here's what that means for 50,000 products on a standard plan, written naively. Create the product, attach images, set inventory: call it three REST calls per product. 150,000 calls at 2 per second is 75,000 seconds. Twenty hours and change, assuming nothing 429s, nothing retries, and your script never dies at 3 a.m. It will. Add exponential backoff on 429 responses and honest resume-from-last-success logic, or you'll re-run the whole thing and create duplicates.
Note also that Shopify has moved product and variant writes toward GraphQL, with the REST product endpoints treated as legacy. If you're building the migration tooling now, build it on GraphQL. Anything you write against REST product endpoints is work you'll redo.
Bulk operations: the right tool for a 50,000-SKU import
The GraphQL Admin API supports bulk mutations. You stage a JSONL file — one JSON object per line, one product per line — upload it, and hand Shopify the file to chew through asynchronously. It runs against a mutation like productSet, which upserts by identifier, so a re-run corrects rather than duplicates. That last property is worth more than the speed.
Two constraints to plan around. Only one bulk operation runs per store at a time, so you can't parallelise your way out of a slow pass; you queue passes in sequence. And the result comes back as a JSONL of successes and errors that you have to actually parse. Nobody does this on the first attempt and nobody should skip it on the second. A 50,000-line run that reports 49,812 successes has 188 products missing from your storefront, and the only way you'll find them is by diffing against the source.
We generally end up with a hybrid: bulk operations for products, variants, prices and metafields; CSV for redirects because it's genuinely easier; and a purpose-written script for inventory across locations. If you'd rather not build any of it, Matrixify is a mature third-party app that handles chunking and throttling for you, and for a lot of one-off migrations it's the cheaper decision than three weeks of engineering time. We say that even though we write migration tooling for a living. Pick the boring option when the boring option works.
Sequence the passes, and put images last
Order matters more than speed. What we run, in order:
- Metafield definitions, sales channels, locations and markets. Structure before data.
- Products and variants as drafts, no images, no inventory.
- Reconcile counts. Products, variants, SKUs, distinct handles.
- Inventory per location.
- Collections — smart rules first, manual membership after.
- Images.
- URL redirects from the old catalogue.
- Publish to channels, in batches, watching the sitemap regenerate.
Images are last because they're the slowest and the most likely to fail. Shopify pulls each image from the URL you supply, so 50,000 products with 4 images each is 200,000 HTTP fetches from your old host. If that host rate-limits, sits behind a login, or serves 5 MB unoptimised JPEGs, the import crawls and a percentage of images just don't arrive. Stage them on a plain public bucket first, resized to something sane, and expect to run a reconciliation pass that finds every product with zero images and retries only those.
One thing to do before any of this: disable or pause the apps that fire on product create. Feed apps, review apps, search indexers, email platforms, ERP connectors. Fifty thousand product-create webhooks will flood a Google Merchant Center feed and can get a diagnostics warning against your account before you've even launched. It also means your Omnisend or Klaviyo product catalogue syncs three times over.
Reconciliation is the deliverable, not the import
The import is done when the numbers agree, not when the email arrives. We build a simple comparison table before the first pass: source SKU count, source variant count, count of products with at least one image, count of products with non-zero inventory, count of products in at least one collection. Then we run the same five counts against Shopify after each pass and look at the deltas.
On a 50,000-SKU catalogue we typically find 0.5% to 2% of rows need a second look on the first pass — bad characters in handles, duplicate SKUs from a legacy system that allowed them, prices with currency symbols embedded, weights in mixed units. That's 250 to 1,000 products. Finding them by browsing the admin is not a plan. Export what you loaded, diff it against what you meant to load, fix, re-run the same upsert.
Nobody can find anything in a 50,000-product store
Getting the catalogue in is the engineering problem. Making it shoppable is the commercial one. Shopify's native collection filtering is fine for a 300-SKU apparel brand and thin at 50,000 SKUs, where customers arrive with a specific part, size, metal, wattage or grade in mind. Default search will miss most of it. Plan the filter taxonomy while you're still shaping the export, because filters run off tags and metafields and it is dramatically cheaper to write them into the import than to backfill them across 50,000 products later. If you'd rather not build the faceting layer yourself, our FilterPro app adds filters and AI search designed for exactly this size of catalogue.
Timeline, and when not to do this
For a 50,000-SKU migration, budget two to three weeks of data work before the first import touches the live store, then a catalogue freeze of 24 to 72 hours for the cutover depending on how much of the inventory ledger you're moving. The freeze is the hard conversation with the merchandising team, and it's non-negotiable: you cannot reconcile a moving target.
Don't schedule the cutover inside a festive window. If you sell in India, that means staying clear of the run-up to Diwali; if you sell into the US or the Gulf, keep away from late November. We've turned down October go-live dates for this reason and been thanked for it in December. Do the import in a slow month, run the store in parallel for a week, then switch DNS.
If you want a second pair of eyes on the plan before you commit, send us the source export and the target structure — a free audit will tell you where the count will drift. If you're earlier than that and still scoping, the migration overview covers what moves and what doesn't, and bringing in a developer for the API passes alone is usually cheaper than a full-service build if your team can handle the data cleaning.

