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:
Before touching your code, check:
max_execution_time → at least 300
memory_limit → 512M or higher
max_input_vars → 5000+
post_max_size / upload_max_filesize → bigger than your JSON payload
(500 products × attributes = quite big!)
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
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
Instead of sleep() (which blocks PHP execution), you should queue the next request only after the previous AJAX response succeeds.
Example flow:
Upload CSV → store data in an option or transient
First AJAX call updates batch #1
When it completes, JavaScript triggers AJAX call for batch #2
Repeat until done
This avoids overloading PHP with one giant loop.
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:
Adds a menu in WooCommerce → Stock Updater
Lets you upload a CSV (sku, stock, price)
Shows a “Start Update” button
Runs the AJAX queue processor to update products in safe batches
<?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();
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>');
}
});
}
});
Upload the plugin folder (wc-stock-updater) with both files:
wc-stock-updater.php
stock-update.js
Activate it in WP Admin.
Go to WooCommerce → Stock Updater.
Upload your CSV (sku, stock, price).
Click Start Update.
It will process 20 products per batch until all are done without 503 errors.
Cheers Brother.