Direct answer
WooCommerce scheduled imports fail because the schedule is usually mistaken for the system. A dependable import needs six separate controls: a real scheduler that runs without shopper traffic; an immutable copy of the source feed; a stable product identity such as supplier plus SKU; small idempotent batches; a durable run ledger with row-level errors; and reconciliation that compares what should have changed with what did. If price or stock matters to trading, run the queue from server cron or WP-CLI, not only from page-load-driven WP-Cron.
Executive summary
- “Last run: success” is not enough; prove source freshness and business outcomes.
- Separate fetching, validation, product writes and channel propagation into checkpoints.
- Never let two jobs or plugins own the same stock or price field without an explicit rule.
- Design replay before the first failure: immutable inputs, stable keys and set-based writes.
A real UK stock sync where the cron ran—but the channel was wrong
In 2021, Appycodes supported an established UK automotive retailer whose WooCommerce catalogue also fed an eBay shop. The supplier did not provide a numeric inventory count. The operating rule was therefore deliberately qualitative: a WooCommerce product marked in stock should publish quantity 1 to eBay; an out of stock product should publish 0. Appycodes had written a stock job that ran every two hours.
The job was running, yet the commercial state was still wrong. Project correspondence records products correctly showing quantity 1 in the WordPress-side listing workflow while eBay showed them out of stock. In a later test, a sale correctly reduced a marketplace listing to 0, but it was still 0 three days after the WooCommerce product had returned to “in stock”. The scheduler was alive; the handoff contract was not.
The difficult decision was ownership. A custom job could derive 1/0 from WooCommerce, but the marketplace extension controlled how that quantity was published. Vendor support confirmed that the installed behaviour did not support the intended rule cleanly and supplied an updated build plus per-profile “custom quantity” and in-stock/out-of-stock settings. The repair path therefore had to align the script, WooCommerce stock semantics and the extension profile instead of adding another cron that wrote the same field.
This was a channel sync rather than a CSV upload, but it exposes the same scheduled-import mistake: “the cron ran” describes one boundary only. Product identity, field ownership, source semantics, downstream acknowledgement and reconciliation determine whether the business state is right.
The seven ways a scheduled import fails
WordPress explains that WP-Cron checks due events on page loads rather than running continuously; a job due at 02:00 may not start until the next request. WooCommerce’s Action Scheduler normally wakes through WP-Cron too, so a healthy-looking queue can still start late on a low-traffic shop. A server scheduler can remove that traffic dependency. WordPress: how WP-Cron works · WooCommerce: scheduled actions
Low traffic, disabled WP-Cron, a broken loopback request, DNS, authentication or a cached cron URL stops the first step.
Use server cron and record scheduler heartbeat separately from import success.A 200 response may contain yesterday’s file, an HTML login page, renamed columns or only half an FTP upload.
Snapshot bytes first; record hash, size, source time and schema version.Titles change, SKUs collide, variation identifiers disappear, or a supplier reuses a code.
Define a stable composite key and quarantine ambiguity.PHP time or memory ends mid-file, leaving a catalogue that looks partly current.
Use bounded batches and checkpoint every committed page.The next schedule starts before the first finishes, or price and stock jobs update the same product simultaneously.
Enforce one active run per feed and explicit field ownership.Relative “increase” operations double stock; create paths duplicate products; emails and webhooks resend.
Prefer absolute set operations and idempotency keys.A mapping silently rejects variations, deletion rules remove valid products, or a marketplace never acknowledges the update.
Reconcile expected rows, rejections and downstream state.Renewals, webhooks, emails and imports share Action Scheduler unless groups and capacity are operated deliberately.
Watch oldest pending age, not just total queue count.WP All Import’s manual scheduling makes the timeout boundary explicit: each import has a trigger URL and a processing URL, and the processing URL is intended to run every two minutes because a single web request may finish only part of the import. Its documentation also recommends a manual test before scheduling. WP All Import: cron scheduling · WP All Import: scheduled WooCommerce products
The Appycodes Import Reliability Score
Score each control from 0 to 3: 0 means absent, 1 assumed, 2 implemented, and 3 proven with retained evidence. The 0–18 total is a release gate, not a plugin rating. A zero for Identity or Replay is an automatic stop because retries can corrupt the catalogue even if the other controls look healthy.
Predictable runner, heartbeat and missed-run alert.
Immutable input, hash, size, source time and schema.
Durable product/variation key; ambiguity quarantined.
Idempotent writes, one owner and overlap control.
Run, batch and row errors visible to operators.
Expected-versus-actual and downstream reconciliation.
| Import type | Safe identity | Idempotent operation | Closure test |
|---|---|---|---|
| Supplier stock | Supplier + warehouse + SKU/variation SKU | Set absolute available quantity | Expected SKU count, unmatched rows, channel stock sample |
| Supplier price | Supplier + price list + SKU + currency | Set approved price version | Changed/unchanged/rejected totals and margin guardrail |
| New product feed | Supplier + immutable source product ID | Upsert draft by source ID | Created, updated, quarantined and missing-media totals |
| ERP catalogue | ERP item ID + variation ID | Apply version only if newer | Version watermark and field-level ownership report |
| Marketplace return sync | Channel + listing ID + SKU | Set state from acknowledged event | WooCommerce/channel divergence queue |
A reliable WooCommerce import has a ledger
Do not stream a changing supplier URL straight into product writes. Fetch once, save the exact bytes outside the public web root, calculate a checksum, validate the header and create a run record. Every batch then reads the same snapshot. This makes a retry explainable and prevents row 1,000 from seeing a different file from row 1.
The minimum run ledger should hold run_id, feed identity, source timestamp, checksum, schema version, status, expected rows, seen rows, updated rows, rejected rows, cursor, started/completed times and last error. Keep a separate rejection table with the row number, stable source key and a safe error code; do not copy customer data or supplier credentials into general logs.
Action Scheduler is useful because it provides traceable, grouped jobs, not because it removes operational limits. Its default web runner is deliberately conservative: the project documentation describes a 30-second request limit and one concurrent batch by default, and recommends WP-CLI for large queues and long-running tasks. Action Scheduler: background processing at scale · Action Scheduler: WP-CLI runner
Implementation: trigger, batch, checkpoint, reconcile
This site-plugin sketch schedules one daily trigger, takes an atomic trigger lock, snapshots the source, processes 100 rows at a time and uses WooCommerce’s SKU and stock APIs rather than writing product metadata directly. The helper functions represent your own run table and feed adapter. The important part is the contract: a batch receives a run ID and offset, uses an immutable input and performs absolute writes that can be repeated.
<?php
/**
* Site-plugin sketch: reliable stock-feed batches with Action Scheduler.
* The feed fetcher must save an immutable file and create the run record first.
*/
const AC_IMPORT_GROUP = 'appy-imports';
add_action('action_scheduler_init', function () {
if (!as_has_scheduled_action('ac_import_trigger', [], AC_IMPORT_GROUP)) {
as_schedule_recurring_action(
time() + MINUTE_IN_SECONDS,
DAY_IN_SECONDS,
'ac_import_trigger',
[],
AC_IMPORT_GROUP,
true
);
}
});
add_action('ac_import_trigger', function () {
global $wpdb;
// add_option is an atomic "only one trigger wins" gate.
if (!add_option('ac_import_trigger_lock', time(), '', false)) {
return;
}
try {
$snapshot = ac_fetch_and_store_feed(); // {path, sha256, source_time}
$run_id = ac_create_run($snapshot, 'queued');
as_enqueue_async_action(
'ac_import_batch',
['run_id' => $run_id, 'offset' => 0],
AC_IMPORT_GROUP,
true
);
} finally {
delete_option('ac_import_trigger_lock');
}
});
add_action('ac_import_batch', function (int $run_id, int $offset) {
$run = ac_get_run($run_id);
$rows = ac_read_snapshot_rows($run['path'], $offset, 100);
$stats = ['seen' => 0, 'updated' => 0, 'rejected' => 0];
foreach ($rows as $row) {
$stats['seen']++;
$sku = trim((string) ($row['sku'] ?? ''));
$qty = filter_var($row['quantity'] ?? null, FILTER_VALIDATE_INT);
if ($sku === '' || $qty === false || $qty < 0) {
ac_reject_row($run_id, $offset + $stats['seen'] - 1, $row);
$stats['rejected']++;
continue;
}
$product_id = wc_get_product_id_by_sku($sku);
if (!$product_id) {
ac_record_unmatched_sku($run_id, $sku);
$stats['rejected']++;
continue;
}
// "set" is retry-safe for an absolute supplier quantity.
wc_update_product_stock($product_id, $qty, 'set');
ac_checkpoint_row($run_id, $sku, $qty);
$stats['updated']++;
}
ac_update_run_counts($run_id, $stats);
if (count($rows) === 100) {
as_schedule_single_action(
time() + 5,
'ac_import_batch',
['run_id' => $run_id, 'offset' => $offset + 100],
AC_IMPORT_GROUP,
true
);
return;
}
ac_reconcile_and_finish($run_id); // expected, seen, updated, rejected
}, 10, 2);WooCommerce exposes wc_get_product_id_by_sku() as the standard SKU lookup and wc_update_product_stock() for stock changes; the latter uses direct product data-store operations and supports set, increase and decrease. For supplier snapshots, choose set because replaying an absolute quantity leaves the same result. WooCommerce: product lookup functions · WooCommerce: stock update function
Run the queue independently of storefront traffic. On a server where WP-CLI and Action Scheduler are available, a conservative starting point is:
*/5 * * * * cd /srv/www/current && wp action-scheduler run --group=appy-imports --batch-size=50 --batches=4 --quietUse a non-overlapping system scheduler or host control panel rather than pasting that line blindly; paths, PHP user, locks and monitoring are host-specific. Action Scheduler warns that separating groups or hooks can break implicit ordering, so keep dependent import steps in one explicit chain. Do not disable its default runner until the WP-CLI path has been observed working in production.
Monitor age, divergence and recovery—not green ticks
WooCommerce exposes Scheduled Actions under WooCommerce → Status, including pending, in-progress, complete and failed jobs with logs. Its System Status report also exposes cron status, Action Scheduler version and counts, memory limit and log writability. These are useful evidence, but the business monitor should sit one level higher. WooCommerce: System Status report
- Scheduler heartbeat. Alert if the runner has not checked in within two expected intervals—even when no feed was due.
- Feed freshness. Reject or hold a feed older than its agreed service window; “download succeeded” is not freshness.
- Queue age. Track the oldest pending import action and active-run duration, not only the number of actions.
- Volume reconciliation. Compare expected, seen, updated, unchanged, unmatched and rejected rows against thresholds.
- Business guardrails. Stop a price run with impossible currency, zero-price spikes or margin breaches; stop stock if the feed suddenly empties.
- Downstream divergence. Sample or reconcile marketplace/listing state where WooCommerce is not the final sales surface.
- Recovery drill. Replay a retained snapshot in staging, resume from a checkpoint and prove that no duplicate or relative write occurs.
Define deletion separately. “Missing from this file” can mean discontinued, temporarily omitted, filtered out, delayed or feed corruption. Never unpublish or delete automatically until the source contract explicitly defines absence and the run passes completeness checks. A safer pattern is two-stage: mark the source record unseen, wait through a grace window, then require a second confirmed feed or an operator decision.
What Appycodes recommends for UK ecommerce teams
WP All Import can be a sensible adapter. Put its trigger and processing calls on real cron, use SKU identity, retain the source file and send one daily exception summary.
Write down whether ERP, WooCommerce or the marketplace owns availability. Reconcile every handoff; do not let each connector “help” by rewriting the same quantity.
Normalise multiple supplier feeds into a staging table first. Resolve warehouse, supersession, currency and variation mappings before WooCommerce receives a write.
Export existing import definitions, list cron and Action Scheduler hooks, identify field owners, inspect failed actions and run a representative snapshot in staging.
After real implementations, Appycodes recommends treating a catalogue import as a small data product rather than a WordPress setting. The UK automotive sync failed at field ownership and downstream acknowledgement even though its two-hour job existed. The same discipline scales to supplier CSVs: freeze the input, name the authority, write by stable identity, checkpoint batches and make every exception explainable.
We apply this through our UK ecommerce development service, covering WooCommerce integrations, catalogue pipelines, marketplace handoffs and operational support.
Frequently asked questions
- Why does a scheduled WooCommerce import work manually but fail overnight?
- Manual and scheduled runs often use different triggers and runtime limits. WP-Cron depends on site requests unless a server scheduler invokes it; URL-triggered imports may also be cached, blocked or stopped by web-request timeouts. Verify the scheduler, queue, logs and completion checkpoint separately.
- Should WooCommerce imports use WP-Cron or a real server cron?
- Use a real server scheduler for business-critical imports. It can invoke WP-Cron or Action Scheduler through WP-CLI at a predictable interval, while WordPress still owns the job records and product updates. Keep a tested fallback and do not disable visitor-triggered WP-Cron until the replacement is confirmed.
- How do you stop duplicate products during a retry?
- Give every source row a stable identity such as supplier plus SKU, store the feed checksum and run ID, and make writes idempotent. A retry should update or skip the same record, not create another product. Never use a product title as the identity key.
- Is WP All Import reliable enough for a large WooCommerce catalogue?
- It can be, if the import is split into trigger and processing runs, the processing URL or CLI command runs frequently enough, identities and deletion rules are explicit, and completion is reconciled. The plugin cannot compensate for an unstable source feed, ambiguous SKUs, competing writers or missing operational monitoring.
- What should a UK retailer monitor after a stock import?
- Monitor feed age, expected versus processed rows, unmatched SKUs, rejected values, run duration, queue age and the gap between WooCommerce and each sales channel. Alert on a stale successful run as well as an explicit failure, because a job can finish without making the expected business change.
Primary sources
- WordPress: Cron plugin handbook
- WooCommerce: scheduled actions
- WooCommerce: System Status report
- Action Scheduler: recurring actions and reliability
- Action Scheduler: WP-CLI queue processing
- WP All Import: cron scheduling
- WooCommerce code reference: product functions
- WooCommerce code reference: stock updates
Technical and operational guidance, not accounting, tax or legal advice. Plugin behaviour, hosting limits and marketplace APIs change; re-check current documentation and test against your own catalogue and contracts.
UK topic cluster
Payments & ecommerce
UK checkout, catalogue, fulfilment and cross-border implementation decisions.
Related guide
Royal Mail Click & Drop with WooCommerce
Make the asynchronous order-to-label handoff observable and recoverable.
Related guide
Bulk-edit a Shopify catalogue safely
Use snapshots, dry runs and result files to make high-volume catalogue changes reversible.
Case study
Creoate wholesale marketplace
Long-running catalogue and commerce engineering for a UK B2B marketplace.














































