Fixing Slow WooCommerce Imports: A Complete Guide to Importing Large Product Catalogues
If a supplier feed takes eight hours to import, dies halfway through, or quietly creates a second copy of every product, the size of the file is almost never the real problem. A 50,000-row CSV is a few megabytes. Something is turning those few megabytes into hours of work, and the useful question is what.
This guide covers what makes WordPress imports slow, how to find your own bottleneck rather than guessing at it, and what to do about each one. Plenty of it costs nothing and needs no new plugin.
Why WordPress imports get slow
Most import plugins do the honest, documented thing: they call WordPress’s own functions once per row. wp_insert_post() for the product, update_post_meta() for each field, wp_set_object_terms() for each category and attribute.
That is correct code. It is also the reason the import takes hours, for three compounding reasons.
Every call fires the whole hook stack. WordPress does not know you are running an import. Each save_post runs every callback every active plugin has registered. Your SEO plugin recalculates a score, your cache plugin purges, your search plugin reindexes the row, your ERP connector queues a webhook. On a plugin-heavy store one product insert can execute code from twenty plugins.
Term counting is quadratic. WordPress recounts the number of posts in a term every time a relationship changes. Import 50,000 products into 300 categories and it recounts, and recounts, and recounts. The work grows with the square of the catalogue rather than in step with it, which is why an import that was tolerable at 5,000 products becomes impossible at 50,000.
Images are downloaded one at a time, then resized several times each. A product with one featured image and eight gallery images is nine HTTP requests to somebody else’s server, followed by nine rounds of thumbnail generation across however many image sizes your theme registers. This is usually the single largest cost in the whole run, and it is bounded by the supplier’s server speed, not yours.
Rows multiply by fields, fields multiply by plugins, and images multiply by registered image sizes. A “50,000 product import” can easily be tens of millions of individual database writes and hundreds of thousands of HTTP requests.
Measure before you change anything
Import tuning attracts guesswork, and guesswork is expensive here because each experiment costs you a full run. Find out which part is slow first.
The thing you want is a per-stage breakdown: how long was spent reading the file, matching rows, writing posts, writing meta, assigning taxonomies, and fetching images. Without that you are optimising in the dark.
If your importer reports stage timings, read them. If it does not, you can still get a rough answer by running a small subset of the feed with images turned off, then again with images on, and comparing. The difference is your image cost, and on most catalogue imports it is the majority of the run.
Raising memory_limit and max_execution_time is the usual first move and it almost never helps. Those settings stop an import from being killed; they do nothing about the amount of work it is doing. If your import completes but takes six hours, PHP limits are not your problem.
Super Speedy Imports records duration, peak memory and throughput per stage, plus a rows-per-second figure and a per-taxonomy breakdown. Diagnosing slow imports walks through reading those numbers, and Import Stages explains what each stage does.
What to fix first, whatever importer you use
These apply to WP All Import, WebToffee, the WooCommerce CSV importer, a custom script, or anything else. Several are free.
Defer term counting
This is the big one, and it is the fix most people have never heard of.
WordPress ships a core function, wp_defer_term_counting(), that tells it to stop recounting after every single relationship and do it once at the end instead. Wrapping an import in it converts that quadratic problem into a linear one.
wp_defer_term_counting( true );
// ... run the import ...
wp_defer_term_counting( false ); // recounts once, here
If you are writing your own importer, use it. If you are not, you need something that does it for you, because most importers do not.
Deferred term counting is the single biggest import speedup available without changing your importer at all. On a catalogue with a lot of categories or attributes it can be the difference between hours and minutes on its own.
Scalability Pro applies this site-wide, so it works for whichever importer you are running, and it switches itself off automatically during bulk edits so admin work behaves normally. It also lets you control the eventual recount separately: disable it, run it daily or weekly, pick the hour it starts, or trigger it immediately, with the queue working through 500 terms per batch so the recount itself never becomes the new problem. Defer Term Counting covers the setting itself.
Index the lookups your importer makes for every row
Before an importer writes anything, it asks the database whether each row is a new product or an update to one it has seen before. It matches on the SKU, the title, a custom field, or its own record of previous imports. On a 50,000-row file that is 50,000 lookups, and without an index each one scans a large table. That cost never shows in the importer’s own progress screen, so it gets blamed on the importer.
Scalability Pro installs indexes for exactly those lookups: the WooCommerce SKU lookup table, post titles, attachment GUIDs, custom-field matching, and WP All Import’s own tracking and image tables. With those lookups indexed, the database is not scanning tables for every row, so the rest of the site stays usable while the import runs.
In our 50,000-product update benchmark, WP All Import took 20 hours without Scalability Pro and 2 hours 23 minutes with it (the full results, on a 4-vCPU server with WP All Import Pro 5.0.6).
Cut the number of image sizes WordPress generates
Every image you download gets regenerated into every registered size. WordPress core registers three. WooCommerce adds its own. Your theme typically adds several more, and page builders add more again. Twelve registered sizes is common, and nobody has ever counted them.
That means one downloaded image is twelve resize operations, twelve files written to disk, and twelve rows of metadata. Multiply by 50,000 products and the CPU cost of thumbnailing alone can exceed the download cost.
Most of those sizes are never used by your theme’s front end. Auditing them is worth doing regardless of which importer you run:
- List what is registered, with
wp_get_registered_image_subsizes() - Work out which ones your templates actually request
- Unregister the rest before a bulk import
Scalability Pro has an Import tab option for this, which inventories the registered sizes, lets you deselect the ones you do not want, and removes only those you explicitly chose. A size registered later by a new theme or plugin is preserved rather than silently dropped.
On a bulk import, cutting image-size generation is often the difference between hours and minutes. It is also the change with the least risk attached, because you can regenerate thumbnails afterwards if you get it wrong.
Run the import from the command line
A browser-driven import is at the mercy of PHP’s execution time limit, your browser staying open, and whatever proxy or firewall sits in front of the site. WP-CLI has none of those constraints and generally runs faster because it skips the whole HTTP request stack.
If your host gives you SSH, use it. wp import, wp ssi <import-id>, or whatever your importer provides, run under screen or tmux so a dropped connection does not kill the job.
Stop other plugins doing work during the run
Search indexers, SEO analysers, cache purgers, activity loggers, related-product calculators and ERP connectors all hook save_post. During an import they are doing 50,000 times more work than they were designed for, and almost all of it gets thrown away.
Deactivating them for the duration of the import is crude but effective. Rebuilding a search index once at the end is much cheaper than maintaining it row by row.
Give MySQL room to work
Imports are write-heavy in a way that normal site traffic is not, and default MySQL configurations are not tuned for it. The settings that matter most are innodb_buffer_pool_size (should hold your working set), innodb_log_file_size, and the sort and join buffers used during large index builds.
Raising these permanently is a hosting decision. Raising them for the duration of one connection is safer, and Super Speedy Imports Pro has an opt-in database tuning option that does exactly that with SET SESSION, affecting only the import’s own connection. It targets term-relationship assignment, the stage whose large joins spill temporary tables to disk at default buffer sizes. It uses up to around 600MB while the tuned stage runs, and it deliberately applies to command-line and scheduled runs only, never to browser runs.
Check your object cache is not the bottleneck
An object cache is unambiguously good for front-end performance. During a write-heavy import it can be a cost rather than a saving, because every write invalidates cache entries, and a remote Redis or Memcached adds a network round trip to work that would otherwise be local.
Worth measuring rather than assuming in either direction. We ran a million-product import with the object cache on and off and published both sets of numbers, and it is not the result most people expect.
Images: where do you actually want them to live?
For a catalogue import, images are usually the majority of the run time. There are three genuinely different answers, and which is right depends entirely on where you want the image files to end up.
Option one: download them, but generate fewer sizes
The files live in your media library. You own them, they survive a supplier going out of business, and everything in WordPress behaves normally.
This is the right answer when the images are yours, when you are not going to re-import frequently, and when you have the disk space. Make it bearable by cutting the registered image sizes as described above.
The cost is time and disk. Fifty thousand products at nine images each is 450,000 downloads and, at twelve registered sizes, 5.4 million generated files.
Option two: never download them at all
Serve the images straight from wherever they are, using the supplier’s URL. Nothing is downloaded, nothing is thumbnailed, nothing takes disk space, and the image portion of your import time goes to zero.
This is what External Images does. It stores the URLs as post meta and teaches WordPress to render from them, including WooCommerce galleries and variation images. Because WordPress has not downloaded the file, it cannot know the pixel dimensions, so the plugin parses them out of the URL where the CDN encodes them and corrects them in the browser once the image loads.
- Affiliate and datafeed sites, where you never owned the images anyway
- Catalogues large enough that the disk cost of images is a real number
- Feeds you re-import often, where downloading the same images repeatedly is pure waste
- Suppliers with a fast, reliable CDN of their own
- The supplier’s URLs are unstable, or they block hot-linking
- You need to edit, crop or watermark the images yourself
- You need the images to outlive your relationship with the supplier
Option three: import the catalogue now, fetch the images in the background
The middle path, and for most stores the right one. The import writes supplier URLs to post meta and finishes in minutes. Visitors see the real images immediately, hot-linked from the supplier. Meanwhile a background queue downloads them into your media library at a polite rate, and each product switches over to the local copy as its images arrive.
You get the fast import and you still end up owning the files.
This is what Delayed Image Import does. The parts that matter are in the queue rather than the deferral: it rotates between supplier hostnames so no single host gets a burst, expands a batch into individual images rather than whole products so a twelve-image gallery is interleaved rather than fired at one server back to back, and trips a circuit breaker with persistent backoff when a host starts failing. A partially-downloaded gallery renders complete, local copies first and hot-linked remainder after.
At the default rate, one batch of 100 posts per minute, that is roughly 6,000 posts’ worth of images an hour, so a 50,000-product catalogue drains overnight. It relies on WP-Cron, which only fires when the site gets traffic, so a staging site nobody visits will not tick.
- You want the images locally, but not at the cost of an eight-hour import
- Suppliers you should not hammer with 450,000 requests in an hour
- Stores that need to be live and correct immediately after the import
Choosing between them
| Download during import | External Images | Delayed Image Import | |
|---|---|---|---|
| Import time cost | Highest | None | None |
| Disk space | Full | None | Full, eventually |
| Who serves the image | You | The supplier | Supplier, then you |
| You own the files | Yes | No | Yes, eventually |
| Survives the supplier vanishing | Yes | No | Yes, once drained |
| Can crop, edit or watermark | Yes | No | Yes, once drained |
Both image plugins integrate with Super Speedy Imports without configuration. With External Images active, automatic column mapping sends image columns to external URLs. With Delayed Image Import active, deferring is on by default, and there is a per-import checkbox if you want one particular run to download inline.
A faster importer
Everything above makes any importer faster. At some catalogue size the importer’s own architecture becomes the limit, because calling wp_insert_post() 50,000 times is inherently slow no matter how quiet you make the hooks.
The alternative is set-based SQL: load the feed into a staging table, then insert posts, postmeta and term relationships in bulk statements rather than one row at a time. This is the whole reason Super Speedy Imports is fast. In our published benchmark, Super Speedy Imports Pro 2.73 imported 1,000,000 simple WooCommerce products, with around 10 million term relationships and no image downloads, in about 22 minutes on a 4-vCPU server.
Three other things carry most of the remaining weight:
Parallel workers. Stages that can be segmented run across several workers, each handling its own range of rows: concurrent requests in the browser, and separate operating-system processes on the command line where the host allows it. A machine with eight cores can use them.
Adaptive batching. In the browser, each worker grows its rows-per-request while calls keep returning quickly, then backs off as it approaches the PHP time limit, so reading a very large file takes far fewer round trips.
Indexes where the joins happen. Unindexed joins are what turn a large import into an overnight job. Super Speedy Imports indexes the columns that variable-product processing self-joins on before those joins run, and indexes term-relationship assignment, so neither stage has to compare every row against every other.
Super Speedy Imports reads CSV, XML, JSON and JSON Lines files as ordinary import sources in every edition, Lite included. Choose the file, confirm which repeating element holds one record, then map and run it exactly as you would a CSV, with no external converter. Importing XML and JSON feeds covers record paths and field names.
There is a free edition, Super Speedy Imports Lite, which imports posts, pages and simple WooCommerce products with the same bulk-SQL engine, concurrent workers and automatic column mapping. Variable products, gallery images, custom post types, scheduled imports and export are in Super Speedy Imports Pro. Pro vs Lite lists the split in full.
A fast import that corrupts your catalogue is worse than a slow one
Speed is the easy half. The failure that actually costs money is a re-run that creates a second copy of all 50,000 products, or wipes the stock levels of everything missing from a partial feed.
Pick the right unique identifier. This is the single most important setting in any importer. It decides whether a row updates a product or creates a new one. SKU is the usual answer but not always the right one: if your supplier reuses SKUs across colours you need a composite identifier combining several fields, or you get one product where you wanted six. See Choosing your unique identifier.
Decide what a missing row means. A product absent from today’s feed might be discontinued, or the supplier’s export might have failed halfway. Deleting on the first assumption when the second is true is an expensive morning. Configure deletes deliberately, and prefer trash to permanent deletion.
Check for duplicate identifiers before you write anything. Two rows sharing an identifier, or one row matching two products, should stop the import rather than quietly importing renamed copies.
Get taxonomy separators right. Hierarchical categories arrive as Parent > Child > Grandchild with a separator that varies by platform, and multiple paths per cell with a different separator again. Getting this wrong produces a category called Home & Garden > Furniture rather than two nested terms.
Understand your variable product structure. Feeds express variations in four different shapes: parent and child rows together, variations only with a grouping column, parents only, or variations against parents that are in the database. Picking the wrong one produces either orphaned variations or no variations at all.
Test on a copy. Import 100 rows into a staging clone, look at what you got, then run the full file. An import you can undo is good; an import you did not need to undo is better.
Keeping the catalogue current
The reason a fast import matters is not the first run. It is that a fast import changes what is possible: an import that takes four minutes can run every hour, and one that takes eight hours can run once a night at best, and only if nothing goes wrong.
Most feeds do not need a full import on every run. Restricting a run to the stages that matter is what makes hourly updates practical:
- Only update prices and stock, skipping taxonomy, image and relationship work entirely
- Only create new items, for topping up rather than re-processing
- Only re-sync taxonomies, after a category restructure
- Only refresh images, when a supplier changes their CDN
Combine that with scheduling, an email after every run and a daily summary, and the feed maintains itself. Complete Guide to Updating Stock and Prices covers the details.
Moving from another importer or another platform
If you are replatforming, the mapping is usually the work rather than the import. Exports from Shopify, Magento, Squarespace, Wix, BigCommerce, WooCommerce’s own exporter and WebToffee all describe the same products in incompatible ways: Shopify calls the regular price “Compare At Price” and the selling price “Variant Price”, Magento prefixes category paths with a root you do not want, BigCommerce splits variants across rows with no grouping key at all.
Auto-mapping handles the recognised formats, and reports rather than guesses where a wrong guess would be worse than none. Upsell and cross-sell references, source-site IDs and per-row pivoted attributes are explained in plain English rather than mapped, because they cannot resolve correctly during a single pass.
If you are moving off WP All Import specifically, its PHP function mapping has a direct equivalent, covered in Migrating WP All Import functions.
What this looks like in practice
Two customers, both with numbers they measured themselves:
How Paradigm PCs cut an 8-hour WooCommerce import to 1 minute. The interesting part is not the speed-up, it is what it changed: an import short enough to run whenever the supplier updates meant their stock levels stopped being a day out of date.
From multi-hour imports to hourly updates. Same pattern, larger catalogue.
If you would rather see controlled tests than customer stories, the benchmarks are run on a fixed Hetzner box from a clean snapshot, and both sides of every comparison are published, including where the numbers are unflattering:
- Super Speedy Imports vs WP All Import Pro
- WebToffee vs WP All Import Pro vs Super Speedy Imports
- 1,000,000 products, Pro vs Lite
- 1,000,000 products, Redis object cache on vs off
More import guides
Getting started
- Quick Start Guide – your first import, start to finish
- WooCommerce Imports Quick Start Guide – the products-specific version
- A tour of the import mapping screen – what every control does
- Automatic column mapping – how much it works out on its own
- Importing XML and JSON feeds – XML, JSON and JSON Lines as ordinary sources
- Trying the demo and sample imports – test without a feed of your own
- Super Speedy Imports: Pro vs Lite – what the free edition covers
Finding and fixing a slow import
- Diagnosing slow imports – read the stage timings and find the real cost
- Import Stages – what each stage of the pipeline does
- Reading the run and completion screens – what the numbers mean
- Choosing parallel workers per stage – how many processes your server can use
- Database tuning for Super Speedy Imports – session-level MySQL tuning
- Controlling which stages of an import actually run – skip the work you do not need
Making WordPress itself faster during an import
- Defer Term Counting – the quadratic recount, and how to switch it off
- Remove Image Sizes Globally – cut what gets generated per uploaded image
- Scalability Pro speeds up your imports – the full set of import-time optimisations
- Diagnosing WP All Import performance issues – if you are staying on WP All Import
- Avoid expensive WP All Export meta_key enumeration – the same problem on the export side
- Optimise Action Scheduler – stop background jobs piling up during bulk work
- Managing the wp_options table and autoload – what a failed import tends to leave behind
Images
- Do I Need External Images? – the honest decision guide
- How External Images Works – the mechanism, including how dimensions are resolved
- Delayed Image Import Quick Start Guide – fast import, images in the background
- How Delayed Image Import Works Under the Hood – the queue, the round-robin and the circuit breaker
- Guide to third party storage – S3, Cloudinary, Bunny and the rest
- How to alter import jobs to import external images – wiring another importer up to it
- Allowing image downloads from private or loopback hosts – staging and internal networks
Getting your data right
- Choosing your unique identifier – the setting that decides update versus duplicate
- Handling duplicate term slugs – when two categories share a name
- Mapping hierarchical and multi-path taxonomies – separators, nesting and multiple paths per cell
- Mapping CSV columns to custom postmeta – custom fields and ACF
- Importing Variable Products – the four file structures and which you have
- Importing product identifiers, shipping and visibility – GTIN, shipping class, catalogue visibility
Keeping the catalogue current
- Complete Guide to Updating Stock and Prices – the update-only path
- Scheduling Automated Imports – running it without you
- Scheduling imports and getting alerts – knowing when a run fails
- Trashed items and deletes – what happens to products missing from the feed
When it goes wrong
- Troubleshooting Super Speedy Imports – the common failures and their causes
- Undoing an import – which recovery action fits what you need back
- Wiping WordPress for a fresh import – starting genuinely clean
- Deleting all WooCommerce products quickly using SQL – when the admin bulk delete will not finish
- Common questions – the short answers
Moving from another tool
- Migrating WP All Import functions – porting your PHP mapping functions
- WP All Import vs Super Speedy Imports – the full comparison, including where they win
- WebToffee Product Import Export vs Super Speedy Imports – the other common incumbent
- WooCommerce CSV Importer vs Super Speedy Imports – versus the built-in importer
- Cart2Cart vs Super Speedy Imports – replatforming from another cart
For developers
- Architecture Overview – how the staging table and stage pipeline fit together
- CLI reference – every command and flag
- Stage system reference – registering your own stage
- Customising imports using SQL filters – transform data in the staging table
- Run your own code when an import completes – completion hooks
- Creating add-ons – extending the importer
- Creating custom templates – your own import type
- Using the export CLI command – round-tripping a catalogue between sites
- Exporting products for a Google XML product feed – straight SQL, no plugin
Further reading
Written for Super Speedy Imports 3.41.0.

Comments