Guide

WooCommerce checkout failures you don't see in the dashboard — how to detect them

The customer fills in the cart, enters their card, clicks Place Order, and the spinner just keeps spinning. Or the page reloads with a generic "There was an error processing your order" notice.

1. Problem

The customer fills in the cart, enters their card, clicks Place Order, and the spinner just keeps spinning. Or the page reloads with a generic "There was an error processing your order" notice. Or — worst case — they get redirected to a blank page and never come back.

You open WooCommerce → Orders. Nothing new. No failed order, no pending order, no record at all. The dashboard looks healthy. Sales for the day look normal-ish. Stripe and PayPal show a few abandoned attempts but nothing flagged as a hard failure on their side either.

This is woocommerce checkout failing silently — and it's the worst class of WooCommerce bug because the dashboard is the last place it shows up. Search results for "woocommerce orders missing" and "woocommerce checkout error not recorded" fill up with generic "clear your cache" advice, but the real cause is almost always a fatal PHP error inside a payment hook, and it's sitting in your error log right now.

Below is how to find it, how it manifests across logs, and how to make sure you hear about the next one in seconds rather than days.

2. Impact

A silent checkout failure is the single most expensive bug a WooCommerce store can ship. Every minute of broken checkout is direct, measurable revenue loss — not degraded UX, not future churn, money the customer was actively trying to give you.

It compounds three ways:

  • Lost conversions. A customer who hits a failed checkout once converts at roughly half the rate on retry. Most don't retry at all.
  • Inventory drift. If the failure happens after stock reservation but before order creation, your stock counts can desync and oversells follow.
  • Payment provider risk. Repeated authorization attempts that don't resolve into orders trigger fraud heuristics on Stripe, Adyen, and Klarna. You can end up with elevated decline rates or, in extreme cases, a compliance review.

And because the order record is never written, none of the standard WooCommerce health metrics — orders/hour, conversion rate, payment success rate — react. The store looks fine on the dashboard while the funnel is bleeding.

3. Why It’s Hard to Spot

WooCommerce was built on the assumption that fatal errors are rare and that gateway round-trips are the main source of failure. The whole "Order failed" → "Order pending" → "Order processing" state machine assumes the order record exists. When the fatal happens before wp_insert_post() runs, none of the state transitions fire and there is nothing for the admin UI to surface.

Three more reasons it stays invisible:

  • Uptime monitors don't catch it. A pinger hitting / or /shop gets a 200 response. The failure only happens on the authenticated POST during checkout, which external uptime tools never exercise.
  • APM tools sample at the wrong tier. Most APM hooks instrument page renders, not AJAX endpoints with cart state. A 500 on ?wc-ajax=checkout looks like noise.
  • The error log is on the server. WooCommerce admins rarely have shell access. The PHP fatal is sitting in /var/log/php-fpm/error.log or /var/log/nginx/error.log and nobody reads it until someone opens a support ticket.

The result is that "woocommerce checkout error not recorded" is one of those bugs that gets discovered when a customer emails support saying "I tried to order three times and nothing happened" — usually a day or two after the failures started.

4. Cause

The vast majority of these incidents are not WooCommerce itself failing. They are PHP fatals raised inside woocommerce_checkout_process, woocommerce_checkout_order_processed, or one of the gateway-specific hooks (woocommerce_payment_complete, woocommerce_thankyou).

When that fatal fires, you produce a php.fatal signal: a record that PHP execution stopped abruptly, with the exception class, message, file, and line of the failure. The hook chain halts mid-execution. Whatever was supposed to happen after the fatal — write the order to wp_posts, fire woocommerce_new_order, send the customer the order_received email, redirect to the thank-you page — does not happen.

From WooCommerce's perspective, the order was never created. From the customer's perspective, the page either died, returned a 500, or returned a 200 with a JavaScript error because the AJAX response (POST /?wc-ajax=checkout) was a half-formed HTML error page instead of valid JSON.

The supporting signals trace the same event from different angles:

  • http.request captures the failed HTTP transaction — usually a 500 on POST /?wc-ajax=checkout, sometimes a 200 with malformed JSON when the fatal is caught and rendered as HTML.
  • wp.email captures the absence of the customer email. The order_received template never gets rendered because WC_Emails::send_transactional_email() is downstream of the fatal.
  • perf.hook_timing captures the abrupt cutoff: woocommerce_checkout_process starts but never reports a completion duration, leaving a partial timing record.

This combination — a php.fatal correlated with a 5xx on the checkout AJAX endpoint and the absence of a wp.email order_received within seconds — is the unambiguous fingerprint of a failed checkout that produced no order.

5. Solution

5.1 Diagnose (logs first)

Every diagnosis path starts with the php.fatal signal. The order in which you check things matters because the fatal evidence is the cheapest and most decisive.

1. Grep the PHP-FPM log for fatals during checkout. This surfaces the php.fatal signal directly.

grep -E "PHP Fatal|Uncaught" /var/log/php-fpm/error.log | grep -iE "checkout|woocommerce|gateway"

Realistic hit:

[27-Apr-2026 14:32:11 UTC] PHP Fatal error: Uncaught Error:
Call to undefined method WC_Stripe_API::request_with_retry()
in /wp-content/plugins/woocommerce-stripe/includes/class-wc-stripe-payment-gateway.php:412
Stack trace:
#0 /wp-includes/class-wp-hook.php(310): WC_Stripe_Payment_Gateway->process_payment()
#1 [internal function]: WP_Hook->apply_filters()
#2 /wp-content/plugins/woocommerce/includes/class-wc-checkout.php(1142): apply_filters()

That stack trace alone tells you: a Stripe gateway plugin is calling a method that no longer exists — almost certainly after a partial update where the WooCommerce Stripe extension was upgraded but a dependent helper plugin wasn't.

2. Correlate with the http.request 5xx on the checkout endpoint. This confirms the fatal is what the customer experienced.

grep "wc-ajax=checkout" /var/log/nginx/access.log | awk '$9 >= 500'

You're looking for POST /?wc-ajax=checkout HTTP/1.1" 500 entries clustered around the same minute as the fatal. That clustering produces the http.request signal pattern Logystera flags as "checkout endpoint 5xx burst".

3. Check for the missing order_received email. This surfaces the absence of the wp.email signal.

grep -i "order_received\|new_order\|woocommerce_email" /var/log/php-fpm/error.log | tail -50

In a healthy store you'll see WooCommerce email sending log lines for every successful order. During a silent failure window, those entries stop while access logs still show checkout POSTs — that gap is the symptom.

4. Inspect WooCommerce's own structured log directory. WooCommerce writes gateway-specific logs here, and they often capture the upstream call that preceded the fatal.

ls -lt /wp-content/uploads/wc-logs/ | head
grep -l "ERROR" /wp-content/uploads/wc-logs/fatal-errors-*.log

fatal-errors-*.log is created by WooCommerce's own fatal-error handler and typically duplicates the php.fatal with cart context attached.

5. Cross-check hook timing. If you have any hook timing instrumented (the perf.hook_timing signal), look for woocommerce_checkout_process records that started but never completed:

grep "woocommerce_checkout_process" /var/log/wp-perf.log | grep -v "completed"

A burst of started-but-not-completed timings around the fatal is the third confirming angle.

By the end of this sequence you should have: the exception message, the file and line, the gateway involved, and a count of how many customers hit it. That's enough to ship a fix.

5.2 Root Causes

(see root causes inline in 5.3 Fix)

5.3 Fix

Root causes, ordered by frequency in real production stores:

1. Plugin version drift after an auto-update. The most common scenario: WooCommerce or a payment gateway extension updates, but a dependent helper, theme integration, or custom plugin still calls the old API surface. Produces php.fatal with Call to undefined method or Class not found. Often correlated with a recent wp.state_change (plugin activation/update event) within the last 24 hours.

Fix: roll the offending plugin back to the previous version (wp plugin install slug --version=X.Y.Z --force), then update its dependencies in lockstep on staging before re-deploying.

2. PHP version incompatibility. Host upgrades PHP from 8.1 to 8.3, or 8.0 to 8.2, and a plugin using deprecated dynamic property access raises a fatal. Produces php.fatal with Creation of dynamic property ... is deprecated escalating to a fatal in strict mode.

Fix: identify the failing plugin from the stack trace, update to a PHP-compatible version, or pin PHP back temporarily. The vendor's changelog will tell you which release added strict-mode support.

3. Memory exhaustion during gateway callbacks. Long carts, many fees, complex tax calculations, or an overweight woocommerce_checkout_create_order filter pushes the request past memory_limit. Produces memory_near_limit warnings followed by php.fatal with Allowed memory size of N bytes exhausted.

Fix: raise memory_limit to 512M as a stop-gap, then profile which hook is allocating. The perf.hook_timing signal will show which filter is consuming the budget.

4. Misconfigured payment gateway credentials. A rotated Stripe key, an expired Adyen API token, or a typo in a webhook secret. The gateway throws an exception that bubbles up as php.fatal if not caught. Often shows up as Stripe\Exception\AuthenticationException or similar in the stack trace.

Fix: rotate the credential through the gateway plugin settings, not by editing wp-options directly. Verify with the gateway's "Test connection" feature before re-enabling.

5. Database deadlock during order insert. High concurrency stores see wp_insert_post() fail with a deadlock when multiple checkouts hit the same product's stock row. Produces php.fatal with Deadlock found when trying to get lock in the stack trace.

Fix: enable WooCommerce's High-Performance Order Storage (HPOS) if not already on it — the dedicated wc_orders table with proper indexing eliminates most of the contention.

For all five, the fix is wasted unless the system that detected the original php.fatal is still watching for new ones. Which is the next section.

5.4 Verify

Verification is signal-driven. You're looking for the php.fatal to stop appearing on the checkout path, and for the supporting signals to return to baseline.

# Watch for new fatals on the checkout path — should be silent under normal traffic
tail -f /var/log/php-fpm/error.log | grep -iE "checkout|woocommerce"

# Watch for 5xx on the checkout AJAX endpoint — should drop to zero
tail -f /var/log/nginx/access.log | grep "wc-ajax=checkout" | awk '$9 >= 500'

# Confirm order_received emails are firing again
tail -f /var/log/php-fpm/error.log | grep -i "order_received\|new_order"

Healthy looks like:

  • No new php.fatal entries referencing checkout, payment gateways, or class-wc-checkout.php for at least 30 minutes under normal traffic.
  • http.request 5xx rate on ?wc-ajax=checkout at zero (or at the store's normal baseline, which should also be zero).
  • wp.email order_received entries appearing in proportion to completed checkouts in the access log.
  • perf.hook_timing for woocommerce_checkout_process shows complete start/end pairs with stable durations.

If you have a test card, push a real end-to-end order through the same gateway that produced the fatal. Then re-run the three tails. One successful order plus 30 minutes of clean logs under traffic is a reasonable confidence threshold for a small store; busier stores should wait through a peak hour before declaring resolution.

6. How to Catch This Early

Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all.

This issue surfaces as php.fatal.

The hard part of this failure mode is not the fix. The fix is usually a one-line plugin rollback. The hard part is knowing the failures are happening.

WooCommerce's admin dashboard, by design, only shows orders that were created. A php.fatal inside woocommerce_checkout_process produces no order, no admin notice, no email to the store owner, no entry in WC Status. The failure exists only in the PHP error log, and "check your error log" is not a monitoring strategy — it's a post-mortem ritual.

This is exactly the gap log intelligence closes. The php.fatal signal — emitted the moment PHP's fatal-error handler runs — is the earliest possible evidence that a checkout has broken. Pair it with the http.request 5xx rate on ?wc-ajax=checkout and the absence of wp.email order_received events, and you have a three-signal correlation that detects silent checkout failure inside one minute of the first occurrence, with no false positives from generic site errors.

This type of issue surfaces as php.fatal correlated with a checkout-endpoint 5xx, which Logystera detects and alerts on early. Once you know within 60 seconds that checkout is broken, the fix is no longer a revenue event — it's a deploy event.

7. Related Silent Failures

Same signal cluster, different surfaces — worth wiring detection for all of them at once:

  • php.fatal on woocommerce_payment_complete — order was created but the post-payment hook failed. Customer is charged, store thinks order is "pending payment" forever. Symptom: support tickets about "I was charged but no order confirmation."
  • wp.email absence after woocommerce_new_order — order created, payment captured, but the customer notification email never sends. Often a misconfigured SMTP plugin or a fatal in an email template hook.
  • http.request burst of 502/504 on /wp-admin/admin-ajax.php?action=update_order_review — front-end checkout review keeps failing, customers can't get past the review step. Usually a slow tax or shipping calculation hook timing out.
  • perf.hook_timing regression on woocommerce_checkout_process — checkout still completes but is taking 8+ seconds. Conversion rate quietly drops. Precedes most fatal-level failures by days.
  • wp.state_change (plugin update) followed by php.fatal within an hour — the canonical "auto-update broke checkout" pattern. Worth a dedicated correlation rule.

Each of these is the same shape: a high-stakes commerce path failing in a way the WooCommerce dashboard does not show, but the logs show immediately. Detect the signal, not the symptom.

See what's actually happening in your systems

Connect your site. Logystera starts monitoring within minutes.

Logystera Logystera
Monitoring for WordPress and Drupal sites. Install a plugin or module to catch silent failures — cron stalls, failed emails, login attacks, PHP errors — before users report them.
Company
Copyright © 2026 Logystera. All rights reserved.