Connecting an enterprise WooCommerce store to an Enterprise Resource Planning (ERP) system is one of the most critical engineering challenges in digital commerce. When product catalogs exceed fifty thousand stock keeping units (SKUs), daily transactions reach tens of thousands of orders, and multi-channel inventory updates occur every second, traditional point-to-point synchronous API integrations break down. Direct HTTP calls between systems lead to cascading gateway timeouts, database deadlocks, webhook storms, and inventory overselling.
A fault-tolerant bi-directional WooCommerce ERP integration requires an asynchronous, event-driven decoupled architecture built on five core engineering pillars:
- Asynchronous message buffering: Ingest webhooks and checkout events into an intermediate queue (Redis Streams or Cloudflare Queues) returning HTTP 202 Accepted in under 25ms, decoupling store traffic from ERP response latency.
- Guaranteed idempotency: Enforce strict request deduplication using
X-Idempotency-Keyheaders and atomic distributed lock registries in Redis to eliminate duplicate orders and duplicate financial entries. - Database concurrency control: Use MySQL InnoDB row-level locking (
SELECT ... FOR UPDATE) or optimistic version checks to prevent race conditions and overselling during peak traffic surges. - Resilient error handling: Implement exponential backoff with full jitter and route persistently failing payloads to an enriched Dead Letter Queue (DLQ) for automated alerting and replay.
- Dual-tier reconciliation: Combine continuous hourly delta synchronization with nightly cryptographic checksum audits (SHA-256 chunk hashes) to detect and resolve data drift.
For enterprise engineering teams planning or upgrading an e-commerce integration pipeline, exploring our specialized WooCommerce ERP integration services provides direct implementation support. This guide examines the architectural blueprints, ERP platform matrices, production PHP 8.4 implementations, and operational troubleshooting runbooks required to build and maintain an enterprise-grade integration.
Architectural foundation: decoupled event-driven integration
In naive WooCommerce architectures, store events trigger direct HTTP requests to an ERP endpoint. For example, when a shopper completes a purchase, an action: woocommerce_checkout_order_processed hook fires, initiating a synchronous cURL request to SAP S/4HANA or Comarch Optima.
This synchronous pattern creates an immediate architectural vulnerability:
[Shopper Browser]
│ (1) Checkout Submit
▼
[WooCommerce / PHP-FPM Worker] ──(2) Synchronous HTTP POST──▶ [ERP Endpoint (Slow / Down)]
│ │
│ ◀───(3) HTTP 504 Gateway Timeout (Blocked for 60s)───────────┘
▼
[Shopper sees Error Screen] ──▶ Duplicate Clicks ──▶ DB Lock Contention & Orphan Orders
When the ERP backend undergoes nightly batch processing, database maintenance, or network latency spikes, response times degrade from 200 milliseconds to thirty seconds. Because PHP-FPM workers are bound synchronously to open socket connections, the entire web server thread pool quickly exhausts available workers. New shoppers attempting to browse catalog pages or load cart sessions receive HTTP 504 Gateway Timeout errors. In addition, if the network drops connection after the ERP accepts the order but before WooCommerce receives confirmation, retry logic creates duplicate invoices and double-allocates warehouse stock.
The decoupled event broker pattern
Enterprise reliability requires decoupling event production from event consumption. The store backend and ERP communicate exclusively through an intermediate event broker.
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ DECOUPLED EVENT-DRIVEN PIPELINE │
└─────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────┐ ┌─────────────────────────┐
│ WooCommerce Store │ │ ERP System Host │
│ (Order / Customer Hub) │ │ (Stock / Master Ledger) │
└────────────┬────────────┘ └────────────▲────────────┘
│ │
(1) Fast Event Ingest (4) Batch Dispatch
(Non-blocking) (Throttled APIs)
▼ │
┌────────────────────────────────────────────────────────────┴───────────────────────────┐
│ MESSAGE BROKER & BUFFER │
│ (Redis Streams / Cloudflare Queues) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ ┌────────────────────────────┐ │
│ │ orders.incoming │ │ inventory.delta │ │ dead.letter.queue (DLQ) │ │
│ │ [Event 1][Event 2] │ │ [SKU 101][SKU 102] │ │ [Failed Payloads + Context]│ │
│ └──────────┬───────────┘ └──────────┬───────────┘ └─────────────▲──────────────┘ │
└──────────────┼─────────────────────────┼────────────────────────────┼──────────────────┘
│ │ │
(2) Read Stream with (5) Write Inventory (3) Max Retries
Consumer Group via InnoDB Locks Exceeded
▼ ▼ │
┌─────────────────────────────────────────────────────────────────────┴──────────────────┐
│ WP-CLI DAEMON WORKER POOL (PHP 8.4) │
│ │
│ - Signal Handling (SIGTERM/SIGINT) - Idempotency Validation (Redis SETNX) │
│ - Memory Management (wp_cache_flush) - Exponential Backoff with Full Jitter │
└────────────────────────────────────────────────────────────────────────────────────────┘
- Fast event capture: When a checkout occurs, WooCommerce writes a compact payload into a Redis Stream (
orders.incoming) and immediately confirms the order to the customer in less than fifteen milliseconds. - Asynchronous worker processing: A fleet of supervised background workers running via WP-CLI consumes messages from the stream according to ERP throughput capacity.
- Controlled backpressure: If the ERP throttles incoming connections or enters a maintenance window, messages accumulate safely in the broker buffer without impacting store front-end responsiveness.
- Idempotent execution: Every message carries a unique deterministic idempotency key. If a worker terminates abruptly, the replacement worker resumes from the pending entry list without duplicating database records.
Real-world ERP system matrix and protocol compatibility
Different enterprise resource planning platforms enforce distinct networking models, concurrency constraints, authentication protocols, and throughput limitations. Integrating WooCommerce successfully requires adapting to these platform-specific realities.
The following matrix contrasts four major ERP platforms commonly integrated with WooCommerce in European and global commerce:
| ERP Platform | Native Protocols | Throughput Profile | Concurrency & Locking Model | Primary Architectural Failure Modes |
|---|---|---|---|---|
| SAP S/4HANA | OData v4, IDoc over SAP BTP, RFC, Async SOAP | High batch throughput (10k+ records/min); medium single-call latency | SAP Logical Unit of Work (LUW); enqueue server locks; OData batch limits | Connection pool saturation during SAP Cloud Connector restarts; batch timeout on deep BOM structures |
| Comarch Optima / XL | Optima WebAPI, COM DLL automation, MS SQL Staging | Moderate API throughput (50-100 orders/min); high SQL staging (50k/min) | Single-Threaded Apartment (STA) in COM; MS SQL table lock escalation (sp_lock) | COM memory leaks requiring worker recycling; license slot lockouts when threads hang |
| InsERT Subiekt GT / nexo | Subiekt GT Sfera (COM/OLE), Subiekt nexo PRO SDK (.NET / WebAPI) | GT: 30-80 orders/min; nexo PRO: 250+ orders/min | SQL Server row locking on dok__Dokument and tw__Towar; desktop COM bottlenecks | COM thread deadlocks on GT modal dialogue popups; database index fragmentation on high SKU counts |
| Microsoft Dynamics 365 BC | Business Central REST API v2.0, OData v4, AL API Pages | 600 requests/min cloud tenant limit; JSON batching (100 sub-requests) | Snapshot isolation in Azure SQL; Sales Header lock timeouts during posting | HTTP 429 rate limit throttling; webhook notification drops during peak invoice generation |
SAP S/4HANA integration patterns
SAP S/4HANA environments typically expose integration interfaces through SAP Business Technology Platform (SAP BTP) and the SAP Cloud Connector. Synchronous single-document creation via standard OData v4 services (API_SALES_ORDER_SRV) introduces an average network latency of 450-800 milliseconds per call.
To achieve scale, high-volume stores must adopt asynchronous batch ingestion:
- JSON Batching: Bundle up to one hundred sales orders into a single multipart OData request changeset. This reduces HTTP handshake overhead and guarantees that either all documents in the changeset commit or none do.
- Intermediate IDoc queues: For massive catalog migrations or price recalculations, utilize intermediate document (IDoc) interfaces (such as
ORDERS05orMATMAS05) processed asynchronously through SAP background jobs. - Handling SAP LUWs: SAP uses Logical Units of Work (LUWs) to ensure database consistency. The integration client must track the SAP document number returned in the header response and store it alongside the WooCommerce order ID in an indexed reference table (
wp_wc_orders_erp_lookup).
Comarch ERP Optima and Comarch ERP XL
Comarch Optima is widely deployed in Central European enterprises. Because Optima was originally designed around a local Microsoft SQL Server architecture, modern API integration presents specific runtime challenges.
- Optima WebAPI vs COM automation: While Optima WebAPI provides a RESTful wrapper, underlying operations instantiate COM objects (
Optima.dll). In COM, every worker thread must initialize a Single-Threaded Apartment (STA) model (CoInitialize). - Worker pool allocation: If ten concurrent PHP processes attempt to create orders via COM objects simultaneously without connection multiplexing, Windows Server resource limits and COM license seat restrictions cause immediate rejection.
- The staging table architecture: For high-throughput B2B stores with tens of thousands of stock mutations per hour, the most reliable architecture avoids direct COM calls for reading data. Instead, an intermediate agent replicates stock and pricing data from read-only MS SQL database snapshots into Redis, while order creation routes through dedicated, single-threaded Windows worker daemons.
InsERT Subiekt GT and Subiekt nexo PRO
InsERT Subiekt GT relies on Sfera for Subiekt GT (an OLE Automation COM interface), whereas Subiekt nexo PRO provides a modern .NET SDK and dedicated WebAPI capabilities.
- Subiekt GT Sfera constraints: Sfera for GT runs synchronously on the Windows desktop runtime. If an unhandled exception or background dialog prompt triggers inside Sfera, the COM thread freezes indefinitely. A dedicated integration service must run as a managed Windows service (in C# or Go), wrapping Sfera calls in strict thirty-second execution timeouts and process-recycling watchdogs.
- Subiekt nexo PRO advantages: Nexo PRO allows multi-threaded operations and asynchronous execution. When syncing product trees, use nexo’s batch APIs to pull deltas based on the
Zmieniono(ModifiedAt) timestamp column, filtering only items whose version stamp exceeds the last recorded watermark.
Microsoft Dynamics 365 Business Central
Dynamics 365 Business Central in the cloud enforces strict API governance rules:
- Tenant rate limits: Microsoft caps API calls at 600 requests per minute per environment. Exceeding this quota immediately yields HTTP 429 Too Many Requests with a
Retry-Afterheader. - JSON batch endpoints: To transfer fifty checkout records without making fifty individual API requests, send a POST request to
https://api.businesscentral.dynamics.com/v2.0/{tenant}/production/api/v2.0/$batch. The body contains an array of independent HTTP sub-requests, evaluated sequentially within the Business Central sandbox. - Change data capture: Rather than polling Business Central continuously for inventory changes, subscribe to Dynamics 365 Webhooks (Graph notifications). Upon receiving a change notification ping, queue a targeted delta sync job for the specific entity ID.
Core fault tolerance patterns for high-volume transactions
Enterprise integrations must be designed under the assumption that all external network connections, database nodes, and worker processes will fail intermittently. Building resilience into WooCommerce requires implementing five foundational fault tolerance patterns.
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ IDEMPOTENCY & FAULT TOLERANCE PATTERN │
└─────────────────────────────────────────────────────────────────────────────────────────┘
Incoming Request / Webhook
│
▼
┌───────────────────────────────────┐
│ Extract X-Idempotency-Key Header │
└─────────────────┬─────────────────┘
│
▼
┌───────────────────────────────────┐
│ Redis SETNX: │
│ lock:idempotency:{key} │
└─────────┬─────────────────────────┘
│
┌─────┴────────────────────────┐
│ Key Acquired (New Request) │ Key Already Exists (Duplicate / In-flight)
▼ ▼
┌───────────────────────────┐ ┌─────────────────────────────────────────────────┐
│ Status: PROCESSING │ │ Check Status: │
│ (TTL: 86400 seconds) │ │ - If 'PROCESSING': Return HTTP 409 Conflict │
└─────────┬─────────────────┘ │ - If 'COMPLETED': Return Cached HTTP Response │
│ └─────────────────────────────────────────────────┘
▼
┌───────────────────────────┐
│ Execute Business Logic │
│ (Transaction + DB Write) │
└─────────┬─────────────────┘
│
┌─────┴────────────────────────┐
│ Success │ Failure (Exception / Timeout)
▼ ▼
┌───────────────────────────┐ ┌─────────────────────────────────────────────────┐
│ Update Redis Status: │ │ Calculate Exponential Backoff with Full Jitter: │
│ COMPLETED + Result Cache │ │ sleep = min(T_max, T_base * 2^retry) + rand() │
└─────────┬─────────────────┘ └─────────────┬───────────────────────────────────┘
│ │
▼ ▼
┌───────────────────────────┐ ┌─────────────────────────────────────────────────┐
│ Return HTTP 200/201 │ │ If retries < 5: Push to Delayed Queue │
└───────────────────────────┘ │ If retries >= 5: Route to Dead Letter Queue │
└─────────────────────────────────────────────────┘
1. Idempotent request handling (X-Idempotency-Key)
In an asynchronous architecture, network retries and duplicate webhooks are normal events. Without idempotency safeguards, receiving a duplicate “Order Paid” webhook from a payment processor or ERP connector could generate duplicate shipping manifests or double-refund a customer.
Every mutating request must include a unique idempotency key:
- Key construction: Clients supply an
X-Idempotency-Keyheader (UUIDv4) or the server computes a deterministic hash:sha256(order_id + order_status + updated_timestamp). - Atomic state locking: Before executing the payload, the worker attempts an atomic write in Redis:
SET lock:idempotency:{hash} "PROCESSING" NX EX 86400 - State resolution:
- If the key is successfully set (
OK), the worker proceeds with execution. Upon successful completion, it updates the key value to"COMPLETED:{response_json}". - If the key already exists and the value is
"PROCESSING", a concurrent worker is actively handling the request. The incoming duplicate immediately returns HTTP 409 Conflict or waits for lock release. - If the key value begins with
"COMPLETED:", the worker short-circuits execution and immediately returns the cached response payload with an added headerX-Cache-Lookup: HIT.
- If the key is successfully set (
2. Redis Streams for reliable event ordering
Simple Redis lists (LPUSH/RPOP) lose messages if a worker process crashes after popping a job but before completing the database transaction. Redis Streams provide complete transaction safety through consumer groups:
- Message append (
XADD): Events are written to an append-only stream log with monotonic millisecond IDs. - Consumer groups (
XREADGROUP): Multiple worker processes consume from a single stream without duplicating work. Redis tracks which consumer claimed each message. - Explicit acknowledgement (
XACK): A message is only removed from the Pending Entries List (PEL) after the worker completes all database commits and callsXACK. - Orphan reclamation (
XCLAIM): If a worker node suffers a kernel panic or out-of-memory kill, surviving workers inspect the PEL usingXPENDING. Any message pending for longer than sixty seconds is reclaimed viaXCLAIMand re-executed.
3. Dead Letter Queues (DLQ) and exponential backoff with full jitter
When an integration job fails, treating all errors identically causes severe system degradation. Integrations must categorize errors into two classes:
- Transient errors: Network timeouts, HTTP 502/503/504 gateway errors, HTTP 429 rate limits, and MySQL database deadlocks. These should be retried automatically.
- Terminal errors: HTTP 400 Bad Request, 404 Not Found, 422 Unprocessable Entity, schema validation failures, and invalid tax configuration. These must not be retried automatically because repeated attempts will never succeed.
For transient errors, apply exponential backoff with full jitter to avoid the “thundering herd” problem where hundreds of retrying workers synchronize and repeatedly overwhelm a recovering ERP:
$$\text{Interval} = \min\left(T_{\text{max}}, T_{\text{base}} \times 2^{\text{attempt}}\right)$$
$$\text{Sleep Time} = \text{random}\left(0, \text{Interval}\right)$$
If an event fails after a configured threshold (typically five attempts), it is routed to a Dead Letter Queue (dlq:erp_sync). The DLQ record encapsulates:
- Raw message payload.
- Complete execution exception stack trace.
- Target ERP endpoint and HTTP response code.
- Timestamp of first attempt and final failure.
- Worker process hostname and attempt counter.
Database concurrency control and inventory locking
When a promotional product drop or flash sale occurs, hundreds of shoppers may attempt to purchase the same inventory items within seconds. If two transactions read stock simultaneously, verify availability, and decrement stock in parallel, the store will oversell physical inventory.
Pessimistic locking: MySQL InnoDB SELECT FOR UPDATE
WooCommerce stores stock levels in the database. In high-concurrency environments, relying on standard get_stock_quantity() and wc_update_product_stock() calls is unsafe because standard queries perform non-locking reads.
Pessimistic concurrency control prevents race conditions by locking the database row until the active transaction commits:
Transaction A (Customer 1) Transaction B (Customer 2)
────────────────────────── ──────────────────────────
START TRANSACTION; START TRANSACTION;
SELECT stock_quantity SELECT stock_quantity
FROM wp_wc_product_meta FROM wp_wc_product_meta
WHERE product_id = 4500 WHERE product_id = 4500
FOR UPDATE; FOR UPDATE;
──▶ Row locked by Transaction A ──▶ BLOCKED (Waits for Lock)
Check stock: 5 available.
Deduct: 5 - 1 = 4.
UPDATE wp_wc_product_meta
SET stock_quantity = 4
WHERE product_id = 4500;
COMMIT; ── Releases Lock ─────────────────────▶ Lock acquired by Transaction B!
Check stock: 4 available.
Deduct: 4 - 1 = 3.
UPDATE wp_wc_product_meta SET ...
COMMIT;
While pessimistic locking guarantees absolute inventory integrity, developers must avoid holding row locks during external HTTP API calls. Never make an HTTP request to an ERP while holding an open database transaction. Lock the row, update local database tables, commit the transaction within fifty milliseconds, and then push the ERP synchronization event to the background queue.
Optimistic concurrency control (OCC)
For catalog reads where write contention is moderate, optimistic concurrency control offers higher throughput without acquiring exclusive row locks during the read phase.
Add a version integer column to the product metadata table. When updating stock:
UPDATE wp_wc_product_meta
SET stock_quantity = stock_quantity - :purchase_qty,
version = version + 1
WHERE product_id = :product_id
AND version = :expected_version
AND stock_quantity >= :purchase_qty;
If another process updated the product between read and write, the version condition fails, matching zero rows. The application detects that zero rows were affected and immediately retries the operation with the updated version stamp.
Production code listings: PHP 8.4 background queue consumer and WP-CLI daemon
The following production-tested components demonstrate how to implement these architectural patterns in WordPress and WooCommerce environments running PHP 8.4.
Listing 1: Background queue consumer daemon (QueueConsumerCommand.php)
This WP-CLI command runs as a continuous system service managed by Systemd or Supervisord. It listens to Redis Streams, processes batches, handles POSIX termination signals gracefully, and manages PHP memory.
<?php
declare(strict_types=1);
namespace WPPoland\ErpIntegration\Cli;
use WP_CLI;
use Redis;
use Throwable;
if (!defined('ABSPATH')) {
exit;
}
/**
* Supervised WP-CLI background queue consumer daemon for ERP synchronization.
*/
class QueueConsumerCommand
{
private const STREAM_KEY = 'erp:stream:orders';
private const CONSUMER_GROUP = 'erp_sync_group';
private const BATCH_SIZE = 10;
private const BLOCK_TIMEOUT_MS = 2000;
private const MAX_MEMORY_BYTES = 134217728; // 128 MB threshold before graceful restart
private Redis $redis;
private string $consumerName;
private bool $shouldRun = true;
public function __construct()
{
$this->consumerName = 'worker_' . gethostname() . '_' . getmypid();
$this->initRedis();
$this->registerSignalHandlers();
}
/**
* Entry point for: wp erp-queue consume
*/
public function __invoke(array $args, array $assocArgs): void
{
WP_CLI::line("Starting ERP Queue Consumer Daemon [{$this->consumerName}] on PHP " . PHP_VERSION);
$this->ensureConsumerGroup();
$processedCount = 0;
while ($this->shouldRun) {
// Signal processing
if (function_exists('pcntl_signal_dispatch')) {
pcntl_signal_dispatch();
}
try {
$messages = $this->redis->xReadGroup(
self::CONSUMER_GROUP,
$this->consumerName,
[self::STREAM_KEY => '>'],
self::BATCH_SIZE,
self::BLOCK_TIMEOUT_MS
);
if (empty($messages) || !isset($messages[self::STREAM_KEY])) {
$this->reclaimOrphanedMessages();
$this->checkMemoryThreshold();
continue;
}
foreach ($messages[self::STREAM_KEY] as $messageId => $payload) {
$this->processMessage((string) $messageId, $payload);
$processedCount++;
}
$this->checkMemoryThreshold();
} catch (Throwable $e) {
WP_CLI::error("Uncaught exception in consumer loop: " . $e->getMessage(), false);
sleep(2); // Throttling after infrastructure error
}
}
WP_CLI::success("ERP Queue Consumer terminated cleanly after processing {$processedCount} messages.");
}
private function processMessage(string $messageId, array $payload): void
{
$orderId = isset($payload['order_id']) ? (int) $payload['order_id'] : 0;
$idempotencyKey = $payload['idempotency_key'] ?? '';
if ($orderId <= 0 || empty($idempotencyKey)) {
WP_CLI::warning("Malformed payload in message {$messageId}. Acknowledging and routing to DLQ.");
$this->routeToDlq($messageId, $payload, 'Validation Failure: Missing order_id or idempotency_key');
$this->redis->xAck(self::STREAM_KEY, self::CONSUMER_GROUP, [$messageId]);
return;
}
// Check idempotency lock
$lockKey = "erp:lock:idemp:{$idempotencyKey}";
$acquired = $this->redis->set($lockKey, 'PROCESSING', ['NX', 'EX' => 86400]);
if (!$acquired) {
$status = (string) $this->redis->get($lockKey);
if (str_starts_with($status, 'COMPLETED')) {
WP_CLI::line("Duplicate event {$idempotencyKey} already completed. Acknowledging.");
$this->redis->xAck(self::STREAM_KEY, self::CONSUMER_GROUP, [$messageId]);
return;
}
WP_CLI::line("Event {$idempotencyKey} is currently processing by another worker. Skipping.");
return;
}
try {
// Execute synchronization logic
$this->syncOrderToErp($orderId, $payload);
// Mark completed and acknowledge
$this->redis->set($lockKey, 'COMPLETED:' . time(), ['EX' => 86400]);
$this->redis->xAck(self::STREAM_KEY, self::CONSUMER_GROUP, [$messageId]);
WP_CLI::line("Successfully synced order #{$orderId} [Msg: {$messageId}]");
} catch (Throwable $e) {
WP_CLI::warning("Failed to sync order #{$orderId}: " . $e->getMessage());
$this->redis->del($lockKey); // Release lock for retry
$attempts = isset($payload['_retry_count']) ? ((int) $payload['_retry_count']) + 1 : 1;
if ($attempts >= 5) {
$this->routeToDlq($messageId, $payload, $e->getMessage());
$this->redis->xAck(self::STREAM_KEY, self::CONSUMER_GROUP, [$messageId]);
} else {
// Re-queue with incremented attempt counter
$payload['_retry_count'] = $attempts;
$this->redis->xAdd(self::STREAM_KEY, '*', $payload);
$this->redis->xAck(self::STREAM_KEY, self::CONSUMER_GROUP, [$messageId]);
}
}
}
private function syncOrderToErp(int $orderId, array $payload): void
{
$order = wc_get_order($orderId);
if (!$order) {
throw new \RuntimeException("WooCommerce order #{$orderId} not found in database.");
}
// Example: Call ERP adapter client
// $erpClient->createSalesOrder($order);
}
private function reclaimOrphanedMessages(): void
{
// Inspect Pending Entries List for messages stuck > 60 seconds
$pending = $this->redis->xPending(self::STREAM_KEY, self::CONSUMER_GROUP, '-', '+', 5);
if (empty($pending)) {
return;
}
$staleIds = [];
foreach ($pending as $entry) {
$messageId = $entry[0];
$idleMs = $entry[2];
if ($idleMs > 60000) {
$staleIds[] = $messageId;
}
}
if (!empty($staleIds)) {
$claimed = $this->redis->xClaim(
self::STREAM_KEY,
self::CONSUMER_GROUP,
$this->consumerName,
60000,
$staleIds,
['JUSTID']
);
WP_CLI::line("Reclaimed " . count($claimed) . " orphaned message(s) from failed consumers.");
}
}
private function routeToDlq(string $messageId, array $payload, string $reason): void
{
$dlqEntry = [
'original_id' => $messageId,
'payload' => json_encode($payload, JSON_THROW_ON_ERROR),
'failure_reason' => $reason,
'failed_at' => (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))->format(\DateTimeInterface::ATOM),
'consumer' => $this->consumerName,
];
$this->redis->xAdd('erp:stream:dlq', '*', $dlqEntry);
WP_CLI::error("Message {$messageId} moved to DLQ: {$reason}", false);
}
private function checkMemoryThreshold(): void
{
// Flush WordPress internal object cache and DB queries log
wp_cache_flush();
global $wpdb;
$wpdb->queries = [];
if (function_exists('gc_collect_cycles')) {
gc_collect_cycles();
}
$memoryUsed = memory_get_usage(true);
if ($memoryUsed >= self::MAX_MEMORY_BYTES) {
WP_CLI::line("Memory threshold reached (" . round($memoryUsed / 1048576, 2) . " MB). Triggering clean restart...");
$this->shouldRun = false;
}
}
private function registerSignalHandlers(): void
{
if (!function_exists('pcntl_signal')) {
return;
}
pcntl_signal(SIGTERM, function () {
WP_CLI::line("Received SIGTERM. Finishing current batch before exiting...");
$this->shouldRun = false;
});
pcntl_signal(SIGINT, function () {
WP_CLI::line("Received SIGINT. Shutting down...");
$this->shouldRun = false;
});
}
private function initRedis(): void
{
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379, 2.5);
}
private function ensureConsumerGroup(): void
{
try {
$this->redis->xGroup('CREATE', self::STREAM_KEY, self::CONSUMER_GROUP, '0', true);
} catch (Throwable) {
// Group already exists
}
}
}
Listing 2: Secure webhook endpoint with HMAC validation (WebhookController.php)
This WordPress REST API controller receives incoming ERP notifications (such as stock adjustments or order status changes), verifies SHA-256 HMAC signatures with timing-safe comparisons, validates timestamp freshness, and pushes events into Redis Streams in under twenty milliseconds.
<?php
declare(strict_types=1);
namespace WPPoland\ErpIntegration\Api;
use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use Redis;
use Throwable;
if (!defined('ABSPATH')) {
exit;
}
class WebhookController extends WP_REST_Controller
{
protected $namespace = 'erp-sync/v1';
protected $rest_base = 'webhook';
private const WEBHOOK_SECRET_OPTION = 'erp_webhook_hmac_secret';
private const MAX_TIMESTAMP_SKEW_SECONDS = 300; // 5-minute replay window
public function register_routes(): void
{
register_rest_route($this->namespace, '/' . $this->rest_base, [
[
'methods' => 'POST',
'callback' => [$this, 'handleIncomingWebhook'],
'permission_callback' => [$this, 'validateHmacSignature'],
],
]);
}
/**
* Timing-safe HMAC verification and timestamp freshness validation.
*/
public function validateHmacSignature(WP_REST_Request $request): bool|WP_Error
{
$signatureHeader = $request->get_header('x-erp-signature-256');
$timestampHeader = $request->get_header('x-erp-timestamp');
if (empty($signatureHeader) || empty($timestampHeader)) {
return new WP_Error(
'rest_forbidden',
'Missing required authentication headers: X-ERP-Signature-256 or X-ERP-Timestamp.',
['status' => 401]
);
}
// Verify timestamp freshness to prevent replay attacks
$requestTime = (int) $timestampHeader;
$currentTime = time();
if (abs($currentTime - $requestTime) > self::MAX_TIMESTAMP_SKEW_SECONDS) {
return new WP_Error(
'rest_forbidden',
'Webhook timestamp exceeds maximum allowed clock skew window (300s).',
['status' => 403]
);
}
$rawBody = $request->get_body();
$secret = (string) get_option(self::WEBHOOK_SECRET_OPTION, '');
if (empty($secret)) {
return new WP_Error('rest_error', 'Server HMAC secret not configured.', ['status' => 500]);
}
$signedPayload = "t={$timestampHeader}.{$rawBody}";
$expectedSignature = hash_hmac('sha256', $signedPayload, $secret);
// Constant-time string comparison to neutralize timing attacks
if (!hash_equals($expectedSignature, $signatureHeader)) {
return new WP_Error(
'rest_forbidden',
'Invalid cryptographic HMAC signature.',
['status' => 403]
);
}
return true;
}
/**
* Non-blocking ingestion handler.
*/
public function handleIncomingWebhook(WP_REST_Request $request): WP_REST_Response|WP_Error
{
$params = $request->get_json_params();
if (empty($params) || !is_array($params)) {
return new WP_Error('rest_bad_request', 'Invalid JSON body.', ['status' => 400]);
}
$eventId = $request->get_header('x-idempotency-key') ?: wp_generate_uuid4();
$eventType = sanitize_text_field((string) ($params['event_type'] ?? 'inventory_delta'));
try {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379, 1.0);
// Append to stream for background processing
$streamPayload = [
'event_id' => $eventId,
'event_type' => $eventType,
'received_at' => (string) microtime(true),
'payload_json' => json_encode($params, JSON_THROW_ON_ERROR),
];
$messageId = $redis->xAdd('erp:stream:incoming_webhooks', '*', $streamPayload);
return new WP_REST_Response([
'status' => 'accepted',
'message_id' => $messageId,
'event_id' => $eventId,
], 202);
} catch (Throwable $e) {
return new WP_Error(
'rest_internal_error',
'Failed to enqueue webhook: ' . $e->getMessage(),
['status' => 500]
);
}
}
}
Listing 3: Atomic stock reservation with SELECT FOR UPDATE (StockManager.php)
This database service manages WooCommerce stock deductions during checkout. It utilizes MySQL InnoDB transactions, row-level locks, and automatic deadlock retry loops.
<?php
declare(strict_types=1);
namespace WPPoland\ErpIntegration\Database;
use wpdb;
use RuntimeException;
use Throwable;
if (!defined('ABSPATH')) {
exit;
}
class StockManager
{
private wpdb $db;
private const MAX_DEADLOCK_RETRIES = 3;
public function __construct()
{
global $wpdb;
$this->db = $wpdb;
}
/**
* Atomically deduct stock using row-level locking.
*
* @param int $productId Target product or variation ID
* @param int $quantityToDeduct Positive integer
* @return int New remaining stock quantity
* @throws RuntimeException If insufficient stock or deadlock persists
*/
public function deductStockAtomically(int $productId, int $quantityToDeduct): int
{
if ($quantityToDeduct <= 0) {
throw new \InvalidArgumentException("Deduction quantity must be greater than zero.");
}
$attempt = 0;
while ($attempt < self::MAX_DEADLOCK_RETRIES) {
$attempt++;
try {
$this->db->query('START TRANSACTION');
// Acquire exclusive row lock on product stock meta
$query = $this->db->prepare(
"SELECT meta_value FROM {$this->db->postmeta}
WHERE post_id = %d AND meta_key = '_stock'
FOR UPDATE",
$productId
);
$currentStockRaw = $this->db->get_var($query);
if ($currentStockRaw === null) {
throw new RuntimeException("Stock record for product ID {$productId} does not exist.");
}
$currentStock = (int) $currentStockRaw;
if ($currentStock < $quantityToDeduct) {
$this->db->query('ROLLBACK');
throw new RuntimeException(
"Insufficient inventory. Requested: {$quantityToDeduct}, Available: {$currentStock}"
);
}
$newStock = $currentStock - $quantityToDeduct;
// Update stock count
$this->db->update(
$this->db->postmeta,
['meta_value' => (string) $newStock],
['post_id' => $productId, 'meta_key' => '_stock'],
['%s'],
['%d', '%s']
);
// Update stock status if inventory hits zero
if ($newStock === 0) {
$this->db->update(
$this->db->postmeta,
['meta_value' => 'outofstock'],
['post_id' => $productId, 'meta_key' => '_stock_status'],
['%s'],
['%d', '%s']
);
}
$this->db->query('COMMIT');
// Clear WooCommerce object caches post-commit
wp_cache_delete($productId, 'post_meta');
if (function_exists('wc_delete_product_transients')) {
wc_delete_product_transients($productId);
}
return $newStock;
} catch (Throwable $e) {
$this->db->query('ROLLBACK');
// Catch MySQL Deadlock Error 1213
$isDeadlock = str_contains($e->getMessage(), 'Deadlock found') ||
($this->db->last_error && str_contains($this->db->last_error, '1213'));
if ($isDeadlock && $attempt < self::MAX_DEADLOCK_RETRIES) {
// Exponential backoff with jitter before retry
$backoffUs = (int) (pow(2, $attempt) * 10000 + random_int(1000, 5000));
usleep($backoffUs);
continue;
}
throw new RuntimeException(
"Database stock transaction failed [Attempt {$attempt}]: " . $e->getMessage(),
0,
$e
);
}
}
throw new RuntimeException("Exceeded maximum deadlock retries for product ID {$productId}.");
}
}
End-to-end reconciliation and split-brain resolution playbook
Even with strict transaction queues and idempotency guards, external factors - such as physical warehouse returns, manual in-store POS transactions, or database backups restored from snapshot - will eventually introduce data discrepancies between WooCommerce and the ERP.
An enterprise integration must incorporate continuous automated reconciliation mechanisms and unambiguous split-brain governance rules.
Single source of truth governance rules
To avoid conflicting bidirectional updates, system boundaries must be strictly defined:
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ SPLIT-BRAIN GOVERNANCE MODEL │
└─────────────────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ ERP SYSTEM (MASTER RECORD) │
│ │
│ - Master Stock Quantities (Physical Warehouse Inventory) │
│ - Master B2B / B2C Price Tiers and Volume Discounts │
│ - Product Master Catalog (Base SKU, Barcodes, Tax Classifications) │
│ - Invoicing, Accounting, and Financial Ledgers │
└───────────────────────────────────────────┬───────────────────────────────────────────┘
│
Authoritative Inbound Synchronisation
│
▼
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ WOOCOMMERCE (FRONT-END COMMERCE HUB) │
│ │
│ - Real-time Shopper Cart Sessions and Checkout Intent │
│ - Front-end Marketing Content, SEO Metadata, Category Taxonomies │
│ - Temporary In-flight Stock Reservations (5-minute Checkout TTL) │
│ - Customer Account Profiles and Shipping Address Revisions │
└───────────────────────────────────────────────────────────────────────────────────────┘
- Inventory quantities: The ERP is the absolute master. If a conflict occurs between WooCommerce stock and ERP stock during a reconciliation sweep, the ERP stock value overwrites WooCommerce.
- Pricing & discounts: The ERP is the master. WooCommerce computes tax and discounts for checkout rendering, but the ERP recalculates and validates financial totals upon sales order generation.
- Order creation: WooCommerce is the master for order creation intent. Once an order is placed and payment authorized online, WooCommerce holds the authoritative initial record and pushes it to the ERP. Once the ERP acknowledges and assigns an internal document ID (
ERP_DOC_ID), the ERP becomes the master for subsequent fulfillment, tracking, and cancellation states.
Dual-tier reconciliation architecture
A comprehensive reconciliation framework operates on two distinct cadences:
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ DUAL-TIER RECONCILIATION LOOPS │
└─────────────────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ TIER 1: CONTINUOUS HOURLY DELTA SYNC │
│ (High frequency, lightweight payload, near real-time recovery) │
│ │
│ Query ERP & WooCommerce: │
│ WHERE updated_at >= NOW() - INTERVAL 90 MINUTE │
│ ──▶ Identify modified SKUs and Orders ──▶ Push to Fast Priority Stream │
└───────────────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ TIER 2: NIGHTLY CRYPTOGRAPHIC CHECKSUM AUDIT │
│ (Deep verification, full catalog integrity, automated segment repair) │
│ │
│ WooCommerce Catalog (Chunk 01: SKUs 00001 - 00500) ──▶ SHA-256: e3b0c442... │
│ │ │
│ [Hash Compare] │
│ │ │
│ ERP Master Catalog (Chunk 01: SKUs 00001 - 00500) ──▶ SHA-256: e3b0c442... │
│ │
│ - If Hashes MATCH: Chunk verified. Skip to Chunk 02. │
│ - If Hashes MISMATCH: Trigger targeted item-by-item differential repair for Chunk 01.│
└───────────────────────────────────────────────────────────────────────────────────────┘
Tier 1: Hourly delta reconciliation
The hourly delta audit queries both systems for entities modified within the last ninety minutes (providing a thirty-minute overlap window).
- WooCommerce queries
wp_wc_orderswheredate_updated_gmt >= (NOW() - INTERVAL 90 MINUTE). - The ERP adapter queries modified ledger entries.
- The reconciler compares document status pairs. Any missing order or mismatched status triggers an automatic push to the high-priority recovery queue.
Tier 2: Nightly cryptographic checksum verification
Comparing one hundred thousand SKUs line-by-line across an API network every night consumes excessive bandwidth and database I/O. Instead, implement chunked cryptographic hashing:
- Divide the entire catalog into sorted lexicographical chunks of 500 SKUs (for example, Chunk 001: SKUs
A0001throughA0500). - Compute a SHA-256 hash across the concatenated string representation of sorted items in each chunk: $$\text{Hash} = \text{SHA256}\left(\sum_{i=1}^{500} \text{SKU}_i + \text{Price}_i + \text{Stock}_i\right)$$
- The ERP produces an identical hash for each chunk.
- The reconciler compares the 200 chunk hashes between systems.
- If 198 chunk hashes match, those 99,000 SKUs are mathematically guaranteed to be in full sync.
- The two mismatched chunks (1,000 SKUs total) are downloaded for granular differential repair, reducing reconciliation payload volume by 99%.
Troubleshooting playbook: resolving live production incidents
When an integration anomaly occurs in production, engineering teams must diagnose and remediate issues rapidly using structured runbooks.
Incident 1: Webhook delivery out-of-order race conditions
- Symptoms: An order in WooCommerce is updated to “Cancelled” by an admin, but five minutes later reverts to “Processing” because an older ERP status webhook was delayed in transit and processed late.
- Root cause: Asynchronous networks do not guarantee in-order packet delivery. Webhook B (sent at 14:02) arrived before Webhook A (sent at 14:00).
- Remediation runbook:
- Inspect the incoming webhook payload for an entity revision stamp (
revision_idinteger orevent_timestamp_utc). - Maintain a version column in
wp_wc_orders_erp_lookup. - Update the database only if:
UPDATE wp_wc_orders_erp_lookup SET erp_status = :new_status, last_event_version = :incoming_version WHERE order_id = :order_id AND last_event_version < :incoming_version; - If zero rows are affected because a newer version has already been recorded, discard the outdated webhook and log an informational message:
EVENT_SUPERSEDED.
- Inspect the incoming webhook payload for an entity revision stamp (
Incident 2: Database deadlocks under flash sale checkout load
- Symptoms: Shoppers receive “Error processing checkout” alerts during a major promotion. MySQL error logs report
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction. - Root cause: Two concurrent checkout transactions purchased the same two items (Product A and Product B) in reverse order. Transaction 1 locked Product A and requested Product B; Transaction 2 locked Product B and requested Product A, creating a circular lock dependency.
- Remediation runbook:
- Implement canonical lock ordering. When checking out multi-item carts, always sort the product IDs in ascending numerical order before executing
SELECT ... FOR UPDATE:$productIds = [842, 105, 330]; sort($productIds, SORT_NUMERIC); // Ordered: [105, 330, 842] foreach ($productIds as $id) { $stockManager->deductStockAtomically($id, $cartItems[$id]['qty']); } - Because all concurrent transactions now acquire locks in the exact same sequential order, circular deadlocks become mathematically impossible.
- Implement canonical lock ordering. When checking out multi-item carts, always sort the product IDs in ascending numerical order before executing
Performance benchmarking, monitoring, and operational SLAs
Operating an enterprise integration requires tracking core telemetry metrics to detect degradation before it impacts shoppers.
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ INTEGRATION MONITORING STACK │
└─────────────────────────────────────────────────────────────────────────────────────────┘
[WooCommerce PHP Workers] ──▶ OpenTelemetry Traces ──▶ [Jaeger / Tempo / Datadog]
[Redis Streams Broker] ──▶ Prometheus Exporter ──▶ [Prometheus Server]
[WP-CLI Daemons] ──▶ Custom StatsD Gauges ──▶ │
▼
[Grafana Dashboards]
│
[Alertmanager Alerts]
│
┌─────────────────────┴─────────────────────┐
▼ ▼
[PagerDuty / Opsgenie] [Slack Channel]
Essential metrics and alert thresholds
| Metric Name | Description | Target SLA | Warning Threshold | Critical Incident Threshold |
|---|---|---|---|---|
erp_queue_consumer_lag | Number of unread messages in orders.incoming stream | < 100 messages | > 500 messages for 5m | > 2,500 messages or age > 15m |
webhook_ingest_p95_ms | Ingestion endpoint latency (signature verification to 202 Accepted) | < 25 milliseconds | > 75 milliseconds | > 250 milliseconds |
order_sync_latency_p99 | Time elapsed from customer checkout submit to ERP document creation | < 30 seconds | > 120 seconds | > 600 seconds |
dlq_occupancy_count | Number of failed messages stored in erp:stream:dlq | 0 messages | > 10 messages | > 50 messages |
db_deadlock_rate | Deadlock frequency per 1,000 checkout transactions | 0.00% | > 0.10% (1 per 1,000) | > 1.00% (10 per 1,000) |
Engineering next steps
Building an enterprise-grade WooCommerce ERP integration requires strict adherence to asynchronous decoupling, deterministic idempotency, database concurrency control, and continuous reconciliation auditing.
To review existing infrastructure or design a custom integration architecture tailored to your ERP platform, explore our WooCommerce ERP integration services or connect with our enterprise WooCommerce developers at WPPoland.





