Diagnosing slow imports
When an import takes longer than expected, the fix is almost always to identify WHICH stage got slow and treat that one — not to add workers blindly or upgrade the server. SSI emits enough telemetry to do this in minutes.
Table of Contents
Where the timing lives
Two places:
1. The CLI summary at the end of every run
Every wp ssi <id> invocation prints a table like this at the very end:
--------------------------------------------------------------------------------
Super Speedy Imports (2.53) Import Summary
--------------------------------------------------------------------------------
Stage Metric Count CPU Time Duration
--------------------------------------------------------------------------------
load-csv Rows Processed 867,990 7604.125s 1098.28s
import-taxonomies Terms Created 0 0.000s 0.36s
import-taxonomies Hierarchical Terms Prop. 3,293 0.135s 0.36s
match-existing Posts Matched 867,990 179.738s 228.09s
update-posts Posts Updated 867,990 72.256s 72.26s
insert-posts Posts Inserted 0 29.589s 61.96s
update-postmeta Postmeta Updated <count> 523.894s 523.89s
upsert-relationships Term Relationships 2,577,210 0.000s 326.45s
fix-attributes Attributes Processed 867,990 5061.829s 658.18s
--------------------------------------------------------------------------------
Total Duration (s) 3562.20
- Count — what the stage actually did (rows processed, terms created, posts matched, etc.).
- CPU Time — total CPU time across all workers. On a parallel stage with 8 workers, this can be 8× Duration.
- Duration — wall-clock time the stage took.
The stages that took the most Duration are where to focus. Look for outliers — one stage taking 10× the rest is usually the optimisable one.
2. The import history panel (admin UI)
Every run also writes a row to wp_ssi_import_history and a row per stage to wp_ssi_import_history_stages. The admin UI’s “Import History” panel renders this as a per-run breakdown with the same data, accessible weeks later. Useful for comparing two runs to see what regressed.
Per-stage diagnosis
Match the slow stage to a likely cause:
load-csv slow
At 868k rows you should expect 5-20 minutes depending on the box. Slower than that:
- CSV is huge per row (lots of mapped columns, or one column with multi-MB text) — the per-row work scales linearly with row content. Check
du -hon the CSV and divide by row count. - Disk I/O is slow (the CSV lives on a network mount, or the per-import staging tables are on slow storage).
iostat -x 5on the DB host during the stage will show if disk is saturated. - Worker count is wrong for the box. See Choosing parallel workers per stage.
match-existing slow
Usually depends on:
- Postmeta size.
match-existingjoinswp_postmetato find the unique identifier — at 10M postmeta rows the join can take a while. - Missing index on
_sku(or your custom UID meta key). WordPress doesn’t indexmeta_valueby default. If your import is bigger than ~100k rows AND you use a custom UID, you may want to add a partial index onwp_postmeta(meta_key, meta_value(50)). Standard_skumatches are usually fast because the WC PA lookup table also covers them.
upsert-relationships slow
This was historically the biggest pain point. 2.47 added covering indexes; 2.51 fixed a duplicate-row explosion. If you’re on 2.51+ and this stage is still slow:
- Many taxonomies × many posts. At 868k posts × 8 taxonomies, the stage handles ~7M relationships. Even a fast SQL has to read and write all of them. 2-5 minutes is realistic.
- Temp table spilling to disk. The DISTINCT temp tables can exceed
tmp_table_size(default 16M). The opt-inenable_db_tuningAdditional Option bumps this to 512M per session. Without it, the DISTINCT spills to disk and takes minutes instead of seconds. See Database tuning for the details. assign_all_hierarchy_levelsis ON (default for new imports since 2.55.9). With this option enabled, each hierarchical post is assigned a relationship for EVERY level of its category path (parent + leaf), not just the leaf. That legitimately multiplies the row count this stage writes — a deeper category tree means proportionally more “Term Relationships”. A higher number than your old leaf-only runs is expected behaviour, not a bug; if you genuinely only want leaf assignments, turn the option off (or it’s already off on pre-2.55.9 imports).
Note on
import-taxonomies: if that stage is the slow one, adding workers won’t help — since 2.55.8 it’s forced to a single worker. Tunehierarchy_batch_sizeandenable_db_tuninginstead.
update-postmeta or insert-postmeta slow
Two common causes:
- Many mapped postmeta keys. Each key is a separate bulk INSERT/UPDATE; 30 mapped keys means 30 statements. Drop unused mappings.
- InnoDB log file too small. Each big INSERT batch can exceed the redo-log fsync threshold. Default
innodb_log_file_sizeis 96M-100M; bump to 512M-1G for big imports. Requires a MySQL restart.
upload-remote-images slow
Almost always upstream-bound:
- Remote server is slow (high response time per image). Check with
curl -w "@curl-format.txt" -o /dev/null -s <url>— anything > 1s/image at the network level is the dominant cost. - Remote server is rate-limiting. Reduce worker count for this specific stage (Performance Options → per-stage override). 8 workers hammering a rate-limited CDN is slower than 2 workers staying under the limit.
- Image files are large. Multi-MB images at 100ms latency are 10x slower than KB-sized images at the same latency. Check the size distribution:
for u in $(head -100 batch_image_urls.txt); do curl -sI "$u" | grep -i content-length; done.
fix-attributes slow
This stage rewrites _product_attributes postmeta and rebuilds the WC attribute lookup table. At 868k products with multiple attributes per product, expect 5-10 minutes. Not much to tune — it’s bottlenecked on WC’s own functions.
If you don’t care about WC attribute filtering (e.g. a catalog of simple products without variation attributes), you can skip this stage entirely via the Import mode preset or a one-off CLI stage list:
wp ssi 5 load-csv,import-taxonomies,match-existing,update-posts,insert-posts,update-postmeta,insert-postmeta,upsert-relationships
Comparing two runs to find regressions
If an import suddenly got slower (you upgraded WC, added a plugin, scaled your data), compare two recent runs:
SELECT
s.stage_name,
s.duration AS new_duration,
(
SELECT duration FROM wp_ssi_import_history_stages
WHERE stage_name = s.stage_name
AND history_id = <OLD-HISTORY-ID>
LIMIT 1
) AS old_duration,
ROUND(s.duration / NULLIF((
SELECT duration FROM wp_ssi_import_history_stages
WHERE stage_name = s.stage_name
AND history_id = <OLD-HISTORY-ID>
LIMIT 1
), 0), 2) AS ratio
FROM wp_ssi_import_history_stages s
WHERE s.history_id = <NEW-HISTORY-ID>
ORDER BY s.duration DESC;
Replace <OLD-HISTORY-ID> and <NEW-HISTORY-ID> with two wp_ssi_import_history.id values. The ratio column tells you which stages got slower or faster between the two runs.
Per-taxonomy progress (since 2.49)
The upsert-relationships stage emits per-taxonomy progress during the run AND persists those numbers to the history:
Processing 8 taxonomies for term relationships:
product_brand (hierarchical) ... deleted: 0, created: 850,000 [45.23s]
product_status (flat) ... deleted: 0, created: 850,000 [3.45s]
product_feature (hierarchical) ... skipped (no source mapped)
If one taxonomy is taking 90% of the total time, that’s your target — usually an index gap or a missing default term on a hierarchical taxonomy.
When the answer is “the data really is that big”
At 1M+ rows, some stages genuinely take hours. There’s no magic fix — the time scales with your data. The right response is:
- Run imports off-hours (overnight).
- Use the Import mode presets to skip stages you don’t need on each run.
- Cache image URLs so you don’t re-download every time. SSI does this automatically — once an image is uploaded, a row is added to the
wp_ssi_image_lookuptable; subsequent imports referencing the same URL skip the download and reuse the existing attachment. Runwp ssi resync-image-lookupto backfill the lookup table from existing attachment guids (useful after manual media-library edits, or if you’ve uploaded images outside SSI). - Consider sharding the import: split the CSV by product type or category and import each shard as a separate SSI import.
What’s next
- Choosing parallel workers per stage — the upstream lever for parallel stages.
- Database tuning — for sequential stages bottlenecked on MySQL.
- MySQL stopped mid-import — for the failure case when the DB couldn’t keep up at all.