79770952

Date: 2025-09-21 15:54:20
Score: 0.5
Natty:
Report link

Great try brother but I see exactly what’s happening.

The 503 Backend fetch failed error is almost never coming from WooCommerce itself - it’s your server (PHP-FPM, Apache, or Nginx proxy maybe) timing out or choking when too many requests or heavy payloads come in quickly.

Here’s how to fix this systematically:

Server/WordPress Limits

Before touching your code, check:

You can override in .htaccess or php.ini if your host allows:

max_execution_time = 300
memory_limit = 512M
max_input_vars = 10000
post_max_size = 64M
upload_max_filesize = 64M

Reduce Payload Size

Even though WooCommerce allows 100 products per batch, in practice chunk size 20–30 is safer when updating stock/price.

Change:

$chunks = array_chunk($products, 50);

to:

$chunks = array_chunk($products, 20); // safer for heavy sites

Throttle Requests (AJAX Queue)

Instead of sleep() (which blocks PHP execution), you should queue the next request only after the previous AJAX response succeeds.

Example flow:

This avoids overloading PHP with one giant loop.

Adjust Your sendBatchRequest (Retry + Delay)

Sometimes WooCommerce REST API throttles requests. Add retry logic with exponential backoff:

private function sendBatchRequest($data) {
    $attempts = 0;
    $max_attempts = 3;
    $delay = 2; // seconds

    do {
        $ch = curl_init($this->apiUrl);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode($data),
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
            CURLOPT_USERPWD => $this->apiKey . ':' . $this->apiSecret,
            CURLOPT_TIMEOUT => 120,
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode >= 200 && $httpCode < 300) {
            return [
                'success'   => true,
                'response'  => json_decode($response, true),
                'http_code' => $httpCode
            ];
        }

        $attempts++;
        if ($attempts < $max_attempts) {
            sleep($delay);
            $delay *= 2; // exponential backoff
        }
    } while ($attempts < $max_attempts);

    return [
        'success'   => false,
        'response'  => json_decode($response, true),
        'http_code' => $httpCode
    ];
}

Here let me restructure your class so it processes products in batches via AJAX queue instead of looping all at once.

Here’s a production-ready rewrite (safe for 500–1000+ products):

<?php
/**
 * Class StockUpdater
 * Processes CSV file and updates WooCommerce products in batches
 */
class StockUpdater {

    private $apiUrl;
    private $apiKey;
    private $apiSecret;

    public function __construct($apiUrl, $apiKey, $apiSecret) {
        $this->apiUrl    = $apiUrl;
        $this->apiKey    = $apiKey;
        $this->apiSecret = $apiSecret;

        // AJAX hooks
        add_action('wp_ajax_start_stock_update', [$this, 'ajaxStartStockUpdate']);
        add_action('wp_ajax_process_stock_batch', [$this, 'ajaxProcessStockBatch']);
    }

    /**
     * Parse CSV into product data
     */
    private function parseCSV($csvFile) {
        $products = [];
        if (($handle = fopen($csvFile, 'r')) !== false) {
            while (($data = fgetcsv($handle, 1000, ',')) !== false) {
                $sku = trim($data[0]);
                $id  = wc_get_product_id_by_sku($sku);

                if ($id) {
                    $products[] = [
                        'sku'   => $sku,
                        'id'    => $id,
                        'stock' => !empty($data[1]) ? (int) trim($data[1]) : 0,
                        'price' => !empty($data[2]) ? wc_format_decimal(str_replace(',', '.', trim($data[2]))) : 0,
                    ];
                }
            }
            fclose($handle);
        }
        return $products;
    }

    /**
     * Start the update (first AJAX call)
     */
    public function ajaxStartStockUpdate() {
        check_ajax_referer('stock_update_nonce', 'security');

        $csvFile  = ABSPATH . 'wp-content/stock-update.csv'; // adjust path
        $products = $this->parseCSV($csvFile);

        if (empty($products)) {
            wp_send_json_error(['message' => 'No products found in CSV']);
        }

        // Store products temporarily in transient
        $batch_id = 'stock_update_' . time();
        set_transient($batch_id, $products, HOUR_IN_SECONDS);

        wp_send_json_success([
            'batch_id' => $batch_id,
            'total'    => count($products),
        ]);
    }

    /**
     * Process next batch (subsequent AJAX calls)
     */
    public function ajaxProcessStockBatch() {
        check_ajax_referer('stock_update_nonce', 'security');

        $batch_id = sanitize_text_field($_POST['batch_id']);
        $offset   = intval($_POST['offset']);
        $limit    = 20; // products per batch (safe)

        $products = get_transient($batch_id);

        if (!$products) {
            wp_send_json_error(['message' => 'Batch expired or not found']);
        }

        $chunk = array_slice($products, $offset, $limit);

        if (empty($chunk)) {
            delete_transient($batch_id);
            wp_send_json_success(['done' => true]);
        }

        $data = ['update' => []];
        foreach ($chunk as $product) {
            $data['update'][] = [
                'id'             => $product['id'],
                'sku'            => $product['sku'],
                'stock_quantity' => $product['stock'],
                'regular_price'  => $product['price'],
            ];
        }

        $response = $this->sendBatchRequest($data);

        wp_send_json_success([
            'done'      => false,
            'next'      => $offset + $limit,
            'response'  => $response,
            'remaining' => max(0, count($products) - ($offset + $limit)),
        ]);
    }

    /**
     * Send batch request to WC REST API with retry logic
     */
    private function sendBatchRequest($data) {
        $attempts = 0;
        $max_attempts = 3;
        $delay = 2;

        do {
            $ch = curl_init($this->apiUrl);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => json_encode($data),
                CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
                CURLOPT_USERPWD => $this->apiKey . ':' . $this->apiSecret,
                CURLOPT_TIMEOUT => 120,
            ]);

            $response = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            if ($httpCode >= 200 && $httpCode < 300) {
                return json_decode($response, true);
            }

            $attempts++;
            if ($attempts < $max_attempts) {
                sleep($delay);
                $delay *= 2;
            }
        } while ($attempts < $max_attempts);

        return ['error' => 'Request failed', 'http_code' => $httpCode];
    }
}
jQuery(document).ready(function ($) {
    $('#start-stock-update').on('click', function () {
        $.post(ajaxurl, {
            action: 'start_stock_update',
            security: stockUpdate.nonce
        }, function (response) {
            if (response.success) {
                processBatch(response.data.batch_id, 0, response.data.total);
            } else {
                alert(response.data.message);
            }
        });
    });

    function processBatch(batch_id, offset, total) {
        $.post(ajaxurl, {
            action: 'process_stock_batch',
            batch_id: batch_id,
            offset: offset,
            security: stockUpdate.nonce
        }, function (response) {
            if (response.success) {
                if (response.data.done) {
                    alert('Stock update complete!');
                } else {
                    let remaining = response.data.remaining;
                    console.log(`Processed ${offset + 20} of ${total}. Remaining: ${remaining}`);
                    processBatch(batch_id, response.data.next, total);
                }
            } else {
                alert(response.data.message);
            }
        });
    }
});
wp_enqueue_script('stock-update', plugin_dir_url(__FILE__) . 'stock-update.js', ['jquery'], null, true);
wp_localize_script('stock-update', 'stockUpdate', [
    'nonce' => wp_create_nonce('stock_update_nonce'),
]);

And here I build you a ready to use mini plugin that does exactly this:

PHP File - wc-stock-updater.php

<?php
/**
 * Plugin Name: WooCommerce Stock Updater (CSV)
 * Description: Upload a CSV (sku, stock, price) and batch update products safely via WooCommerce REST API.
 * Version: 1.0
 * Author: Jer Salam
 */

if (!defined('ABSPATH')) exit;

class WC_Stock_Updater {
    private $apiUrl;
    private $apiKey;
    private $apiSecret;

    public function __construct() {
        $this->apiUrl    = home_url('/wp-json/wc/v3/products/batch');
        $this->apiKey    = get_option('woocommerce_api_consumer_key');
        $this->apiSecret = get_option('woocommerce_api_consumer_secret');

        add_action('admin_menu', [$this, 'add_menu']);
        add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']);

        // AJAX
        add_action('wp_ajax_start_stock_update', [$this, 'ajaxStartStockUpdate']);
        add_action('wp_ajax_process_stock_batch', [$this, 'ajaxProcessStockBatch']);
    }

    public function add_menu() {
        add_submenu_page(
            'woocommerce',
            'Stock Updater',
            'Stock Updater',
            'manage_woocommerce',
            'wc-stock-updater',
            [$this, 'render_admin_page']
        );
    }

    public function enqueue_scripts($hook) {
        if ($hook !== 'woocommerce_page_wc-stock-updater') return;

        wp_enqueue_script('wc-stock-updater', plugin_dir_url(__FILE__) . 'stock-update.js', ['jquery'], '1.0', true);
        wp_localize_script('wc-stock-updater', 'stockUpdate', [
            'nonce' => wp_create_nonce('stock_update_nonce'),
            'ajaxurl' => admin_url('admin-ajax.php'),
        ]);
    }

    public function render_admin_page() {
        ?>
        <div class="wrap">
            <h1>WooCommerce Stock Updater</h1>
            <form method="post" enctype="multipart/form-data">
                <?php wp_nonce_field('wc_stock_upload', 'wc_stock_nonce'); ?>
                <input type="file" name="stock_csv" accept=".csv" required>
                <input type="submit" name="upload_csv" class="button button-primary" value="Upload CSV">
            </form>

            <?php
            if (isset($_POST['upload_csv']) && check_admin_referer('wc_stock_upload', 'wc_stock_nonce')) {
                if (!empty($_FILES['stock_csv']['tmp_name'])) {
                    $upload_dir = wp_upload_dir();
                    $csv_path   = $upload_dir['basedir'] . '/stock-update.csv';
                    move_uploaded_file($_FILES['stock_csv']['tmp_name'], $csv_path);
                    echo '<p><strong>CSV uploaded successfully.</strong></p>';
                    echo '<button id="start-stock-update" class="button button-primary">Start Update</button>';
                }
            }
            ?>
            <div id="stock-update-log" style="margin-top:20px; font-family: monospace;"></div>
        </div>
        <?php
    }

    private function parseCSV($csvFile) {
        $products = [];
        if (($handle = fopen($csvFile, 'r')) !== false) {
            while (($data = fgetcsv($handle, 1000, ',')) !== false) {
                $sku = trim($data[0]);
                $id  = wc_get_product_id_by_sku($sku);

                if ($id) {
                    $products[] = [
                        'sku'   => $sku,
                        'id'    => $id,
                        'stock' => !empty($data[1]) ? (int) trim($data[1]) : 0,
                        'price' => !empty($data[2]) ? wc_format_decimal(str_replace(',', '.', trim($data[2]))) : 0,
                    ];
                }
            }
            fclose($handle);
        }
        return $products;
    }

    public function ajaxStartStockUpdate() {
        check_ajax_referer('stock_update_nonce', 'security');

        $upload_dir = wp_upload_dir();
        $csvFile = $upload_dir['basedir'] . '/stock-update.csv';

        $products = $this->parseCSV($csvFile);

        if (empty($products)) {
            wp_send_json_error(['message' => 'No products found in CSV']);
        }

        $batch_id = 'stock_update_' . time();
        set_transient($batch_id, $products, HOUR_IN_SECONDS);

        wp_send_json_success([
            'batch_id' => $batch_id,
            'total'    => count($products),
        ]);
    }

    public function ajaxProcessStockBatch() {
        check_ajax_referer('stock_update_nonce', 'security');

        $batch_id = sanitize_text_field($_POST['batch_id']);
        $offset   = intval($_POST['offset']);
        $limit    = 20;

        $products = get_transient($batch_id);

        if (!$products) {
            wp_send_json_error(['message' => 'Batch expired or not found']);
        }

        $chunk = array_slice($products, $offset, $limit);

        if (empty($chunk)) {
            delete_transient($batch_id);
            wp_send_json_success(['done' => true]);
        }

        $data = ['update' => []];
        foreach ($chunk as $product) {
            $data['update'][] = [
                'id'             => $product['id'],
                'sku'            => $product['sku'],
                'stock_quantity' => $product['stock'],
                'regular_price'  => $product['price'],
            ];
        }

        $response = $this->sendBatchRequest($data);

        wp_send_json_success([
            'done'      => false,
            'next'      => $offset + $limit,
            'response'  => $response,
            'remaining' => max(0, count($products) - ($offset + $limit)),
        ]);
    }

    private function sendBatchRequest($data) {
        $attempts = 0;
        $max_attempts = 3;
        $delay = 2;

        do {
            $ch = curl_init($this->apiUrl);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => json_encode($data),
                CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
                CURLOPT_USERPWD => $this->apiKey . ':' . $this->apiSecret,
                CURLOPT_TIMEOUT => 120,
            ]);

            $response = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            if ($httpCode >= 200 && $httpCode < 300) {
                return json_decode($response, true);
            }

            $attempts++;
            if ($attempts < $max_attempts) {
                sleep($delay);
                $delay *= 2;
            }
        } while ($attempts < $max_attempts);

        return ['error' => 'Request failed', 'http_code' => $httpCode];
    }
}

new WC_Stock_Updater();

JS File - stock-update.js

jQuery(document).ready(function ($) {
    $('#start-stock-update').on('click', function () {
        $('#stock-update-log').html('<p>Starting stock update...</p>');

        $.post(stockUpdate.ajaxurl, {
            action: 'start_stock_update',
            security: stockUpdate.nonce
        }, function (response) {
            if (response.success) {
                processBatch(response.data.batch_id, 0, response.data.total);
            } else {
                $('#stock-update-log').append('<p style="color:red;">' + response.data.message + '</p>');
            }
        });
    });

    function processBatch(batch_id, offset, total) {
        $.post(stockUpdate.ajaxurl, {
            action: 'process_stock_batch',
            batch_id: batch_id,
            offset: offset,
            security: stockUpdate.nonce
        }, function (response) {
            if (response.success) {
                if (response.data.done) {
                    $('#stock-update-log').append('<p style="color:green;">Stock update complete!</p>');
                } else {
                    let processed = offset + 20;
                    $('#stock-update-log').append('<p>Processed ' + processed + ' of ' + total + ' products. Remaining: ' + response.data.remaining + '</p>');
                    processBatch(batch_id, response.data.next, total);
                }
            } else {
                $('#stock-update-log').append('<p style="color:red;">' + response.data.message + '</p>');
            }
        });
    }
});

How to use

  1. Upload the plugin folder (wc-stock-updater) with both files:

    • wc-stock-updater.php

    • stock-update.js

  2. Activate it in WP Admin.

  3. Go to WooCommerce → Stock Updater.

  4. Upload your CSV (sku, stock, price).

  5. Click Start Update.

It will process 20 products per batch until all are done without 503 errors.

Cheers Brother.

Reasons:
  • Blacklisted phrase (1): Cheers
  • Long answer (-1):
  • Has code block (-0.5):
  • Low reputation (1):
Posted by: Jer Salam