Split big WooCommerce background processing jobs

Big bold words Split the Job beside an isometric machine cutting one large slab into small blocks on a conveyor.

The predictable failure on a growing store is a WooCommerce background processing job that got bigger than its request. A nightly repricing or stock recalculation runs fine for a year, the catalog grows, and one night the request dies mid-loop with no error logged and no record of where it stopped.

The first fix people reach for is fighting the limits: ini_set(‘memory_limit’, ‘-1’), set_time_limit(0), a bigger PHP-FPM timeout. That sometimes buys a few months. It fails because most hosts will still kill a long-running request, and because even a successful run is all-or-nothing: one fatal on a single bad product throws away everything the loop already handled.

Berend Markhorst wrote a walkthrough of Benders’ decomposition for stochastic programs whose single giant model stops fitting in memory. Store work doesn’t need LP duality, and I won’t pretend it does. But the structural move in that post is worth stealing. When one small set of decisions couples everything together, fix those, and the rest of the work splits into independent pieces you can finish one at a time.

Why the one-request job dies

A monolithic run has the same shape as the deterministic equivalent in the source post. Everything shares one process, so nothing can finish until everything finishes: all product objects held in memory at once, one database connection pinned for the whole run, all progress living inside a request the host may kill at any point. There is nothing to resume from, only a loop counter that died with the request.

The naive shrinks fail the same way here as they do there. Solving each product on its own and averaging the answers doesn’t work when the decision has to be consistent across the catalog, and solving with average demand is exactly the approach that looks fine until the scenario you didn’t model shows up on a Monday.

The shape behind WooCommerce background processing

Split it instead. A small piece of state, a cursor plus whatever you’re accumulating, is the master problem. Each batch of products is a subproblem: independent, small enough to finish inside your limits, and able to hand back one line of progress. Action Scheduler is built for this and ships with WooCommerce, where it already processes subscription payments and webhook deliveries in batches of 20 by default, with several queues running at once. There’s no worker daemon to set up, just a loop that reschedules itself.

With bbioon_reprice_product() holding your per-product logic:

add_action( 'bbioon_reprice_chunk', 'bbioon_reprice_chunk' );

function bbioon_reprice_chunk() {
    $state = get_option( 'bbioon_reprice_state', array( 'offset' => 0, 'done' => 0 ) );

    $ids = wc_get_products( array(
        'limit'  => 200,
        'offset' => $state['offset'],
        'status' => 'publish',
        'return' => 'ids',
    ) );

    if ( empty( $ids ) ) {
        delete_option( 'bbioon_reprice_state' );
        return; // full pass finished
    }

    foreach ( $ids as $id ) {
        bbioon_reprice_product( $id ); // one small subproblem
    }

    $state['offset'] += 200;
    $state['done']   += count( $ids );
    update_option( 'bbioon_reprice_state', $state );

    as_schedule_single_action( time() + 10, 'bbioon_reprice_chunk' );
}

A couple of practical notes. Run the queue from a real server cron entry with wp action-scheduler run rather than depending on loopback WP-Cron, which tends to never fire on a quiet store. And if you haven’t decided yet whether the job should be batched at all, spend ten minutes up front on the batch versus stream tradeoff. I’m not sure the ten-second gap between batches matters either way, but it keeps the queue from stacking up behind itself.

If you have a store job that has outgrown its request, I do this kind of work often: rewriting monolithic nightly runs into resumable, scheduled pipelines. I’m happy to look at what you’re running and tell you whether it needs the split.

The assumption that stalls the loop

Benders quietly assumes relatively complete recourse: every subproblem must be solvable for whatever the master proposes. When that fails, a scenario comes back infeasible and you need a second kind of cut before the loop can converge.

The store equivalent is assuming every batch can finish under the limits, whatever the data looks like. When a batch is too large, or one product throws a fatal, the loop doesn’t fail loudly. It reschedules forever, or the cursor stops advancing, and the job looks alive in the admin while doing nothing. So store a heartbeat timestamp in the state and alert when it goes stale, size the batch for the smallest environment the store might move to, and time one batch before you commit to the design.

And don’t do any of this if the job finishes comfortably inside one request. Decomposition buys resumability and parallelism, but it costs you state to maintain and a loop to debug, which isn’t worth it for a job that already fits.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.

Leave a Comment