UK payments & ecommerce

Why WooCommerce Scheduled Imports Fail—and How to Make Them Reliable

A cron expression only starts work. Reliability comes from preserving the feed, controlling identity, making retries safe and proving the catalogue reached the expected state.

Guide cover: make WooCommerce scheduled imports reliable
By Ritesh Agarwal14 min read

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.

TriggerWake the system predictablyServer cron or managed scheduling; verify the runner rather than trusting a saved time.
ProcessBatch with identityFreeze the feed, update by durable keys and make the same row safe to replay.
ProveReconcile the resultExpected, processed, rejected and stale records must be visible to an owner.

Executive summary

  1. “Last run: success” is not enough; prove source freshness and business outcomes.
  2. Separate fetching, validation, product writes and channel propagation into checkpoints.
  3. Never let two jobs or plugins own the same stock or price field without an explicit rule.
  4. 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.

Evidence boundary. Connected Appycodes delivery email from July–August 2021 verifies the UK retailer, the two-hour stock job, the 1/0 business rule, downward sync after a sale, failure to restore the marketplace quantity, the vendor limitation and the proposed plugin/profile repair. It does not prove a long-term resolution, sales value or a measured revenue outcome, so none is claimed. The client is anonymised and credentials, addresses and private identifiers are excluded.

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

01 · Wake-upThe trigger never fires—or fires late

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.
02 · SourceThe feed moved, changed shape or went stale

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.
03 · IdentityRows cannot resolve the intended product

Titles change, SKUs collide, variation identifiers disappear, or a supplier reuses a code.

Define a stable composite key and quarantine ambiguity.
04 · ExecutionOne request tries to do all the work

PHP time or memory ends mid-file, leaving a catalogue that looks partly current.

Use bounded batches and checkpoint every committed page.
05 · ConcurrencyTwo runs overlap

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.
06 · ReplayA retry repeats side effects

Relative “increase” operations double stock; create paths duplicate products; emails and webhooks resend.

Prefer absolute set operations and idempotency keys.
07 · ProofThe job completes but the business state is wrong

A mapping silently rejects variations, deletion rules remove valid products, or a marketplace never acknowledges the update.

Reconcile expected rows, rejections and downstream state.
Queue pressureOther plugins consume the same worker budget

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.

Six controls × 0–3Maximum 18
TTrigger
Predictable runner, heartbeat and missed-run alert.
SSnapshot
Immutable input, hash, size, source time and schema.
IIdentity
Durable product/variation key; ambiguity quarantined.
RReplay
Idempotent writes, one owner and overlap control.
OObservability
Run, batch and row errors visible to operators.
CClosure
Expected-versus-actual and downstream reconciliation.
15–18 · releaseAll controls are proven and Identity/Replay are non-zero. Keep exception ownership and recovery drills.
10–14 · conditionalThe import may run, but one unproven boundary is likely to become an overnight incident.
0–9 · stopDo not automate writes. Repair identity, replay or evidence before trusting the schedule.
Import typeSafe identityIdempotent operationClosure test
Supplier stockSupplier + warehouse + SKU/variation SKUSet absolute available quantityExpected SKU count, unmatched rows, channel stock sample
Supplier priceSupplier + price list + SKU + currencySet approved price versionChanged/unchanged/rejected totals and margin guardrail
New product feedSupplier + immutable source product IDUpsert draft by source IDCreated, updated, quarantined and missing-media totals
ERP catalogueERP item ID + variation IDApply version only if newerVersion watermark and field-level ownership report
Marketplace return syncChannel + listing ID + SKUSet state from acknowledged eventWooCommerce/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.

ONE FEED · ONE IMMUTABLE RUN · MANY REPLAYABLE BATCHESServer cronpredictable wake-upheartbeatFeed snapshotbytes · hash · timeschema versionRun ledgerrun ID · countscursor · statusBatch queue100 rowsone active runWooCommerceSKU identityabsolute writesCLOSURE PLANE · A COMPLETED QUEUE IS NOT YET A CORRECT CATALOGUEExpectedsource row countfreshness · totalsProcessedupdated · unchangedcheckpointedExceptionsunmatched · rejectedsafe error reasonDownstream proofchannel acknowledgementdivergence queue · ownerClose only when the numbers explain the runexpected = processed + quarantined · alert on stale successNEVER DELETE FROM AN INCOMPLETE FEED · NEVER RETRY A RELATIVE STOCK WRITEFIG. 01RELIABLE SCHEDULED IMPORT
Fig. 01 The scheduler only creates a run. Reliability comes from freezing the input, processing retry-safe batches and reconciling the business result before the run is closed.scroll →

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.

Action Scheduler batches with stable identity and retry-safe stock writesphp
<?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:

Server scheduler: process only the import group every five minutesbash
*/5 * * * * cd /srv/www/current && wp action-scheduler run --group=appy-imports --batch-size=50 --batches=4 --quiet

Use 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

  1. Scheduler heartbeat. Alert if the runner has not checked in within two expected intervals—even when no feed was due.
  2. Feed freshness. Reject or hold a feed older than its agreed service window; “download succeeded” is not freshness.
  3. Queue age. Track the oldest pending import action and active-run duration, not only the number of actions.
  4. Volume reconciliation. Compare expected, seen, updated, unchanged, unmatched and rejected rows against thresholds.
  5. Business guardrails. Stop a price run with impossible currency, zero-price spikes or margin breaches; stop stock if the feed suddenly empties.
  6. Downstream divergence. Sample or reconcile marketplace/listing state where WooCommerce is not the final sales surface.
  7. 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

Small UK DTC retailerUse the plugin, own the evidence

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.

Multi-channel UK retailerChoose one stock authority

Write down whether ERP, WooCommerce or the marketplace owns availability. Reconcile every handoff; do not let each connector “help” by rewriting the same quantity.

High-SKU distributorStage outside the catalogue

Normalise multiple supplier feeds into a staging table first. Resolve warehouse, supersession, currency and variation mappings before WooCommerce receives a write.

Agency or inherited storeAudit before rescheduling

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.

Our ruleAn import is not reliable when the cron runs. It is reliable when the same feed can be replayed safely and an operator can prove which products changed, which did not, and why.

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

Published 28 Sep 2026Reviewed 28 Sep 2026Reviewer Appycodes Editorial Team

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.

Our clients

UK · Europe · Worldwide

Selected case studies

What we built, how it works and the results for our clients.

Creoate product interface01
B2B commerce

Eight years behind a wholesale marketplace

Next.js storefront, Python ingestion pipelines, DynamoDB data layer and AWS infrastructure.

8+ yearsdevelopment and support
Ontick product interface02
Event technology

Ticketing owned by the event team

Multi-organiser commerce, Stripe instalments and two native apps in one connected platform.

£2M+ticket sales processed
Easyship product interface03
Global logistics

Helping shippers compare their options

Rate, tax and duty calculators, server-rendered courier pages and a custom MongoDB CMS.

550+couriers in the calculator
TEFL.ie product interface04
Education & training

Connecting course sales to the classroom

WordPress and WooCommerce, a Moodle LMS, Stripe deposits and Zoho CRM, tied together with Zapier automation.

Since 2017development and support
All White Laser product interface05
Medical aesthetics

From equipment finance to clinic support

A lead-to-billing system on GoCardless Direct Debit, provider certification, and a React Native app for machine owners.

9 yrsdevelopment and support
Decofetch product interface06
Luxury commerce

A custom home for designer furniture

Server-rendered Next.js commerce over a Laravel API, bespoke operations tooling and re-architected AWS infrastructure.

0→livemarketplace development
BA Engine Room product interface07
AI operations

Connecting discovery, contracts and delivery

Discovery briefs, e-signed contracts, Stripe deposits, delivery milestones and time tracking in one operational system.

0→1custom platform development
PlusHeat product interface08
Home services

Helping customers choose their boiler cover

Custom plan configuration, postcode-qualified lead journeys, CRM synchronisation and campaign landing pages.

5 yrswebsite development and support
Léonia product interface09
Beauty commerce

Shopify shaped around a beauty brand

Custom theme, customer accounts, loyalty rewards, referrals and gift-with-purchase offers.

5 yrsShopify development and support
Shutters 365 product interface10
Home improvement

From window measurements to a priced order

A seven-step product builder with live previews, sample orders and supplier tools.

7-stepproduct configurator
Bloc Ads Manager product interface11
Advertising

From targeted ads to venue check-ins

Campaign creation, audience targeting, in-app ads and reporting linked to venue check-ins.

check-inscampaign attribution
Bloc product interface12
Social events

Four years across the app and operations

Mobile app, backend, advertising tools, a digital marketplace and website.

4+ yrssupport across five codebases
Zonely product interface13
Social mobile

Two apps, one real-time conversation marketplace

Customer and buddy apps with per-minute billing, wallets, moderation and admin tools.

2 appsfor iOS and Android
Player Profile Hub product interface14
Grassroots football

Helping grassroots players get discovered

Verified profiles, video highlights, coach discovery and safeguarding on web and mobile.

0→1custom platform development
DeepSpatial product interface15
Geospatial AI

Connecting clients, investors and emerging talent

Corporate and investor pages, the Xploor talent platform and ongoing releases on AWS Amplify.

2 yrsdevelopment and support
Yippee Malta product interface16
Travel

A booking journey the tour team owns

A multilingual website connected to the booking API, with deposits, coupons and affiliate tracking.

6languages across the booking journey
Professional Energy product interface17
Energy brokerage

Tenders, contracts and accounts brought together

Supplier tenders, contract management, brokerage accounting and client records.

100+suppliers per tender

Tell us what you are trying to build.

A thirty-minute call with the engineer who would run it.

Discuss your project