Published Oct 25, 2026

I won’t run an LLM in checkout unless idempotency is bulletproof

By Kevin Champlin

The night a checkout “help” prompt duplicated an order

It was a Fortune 500 apparel brand—high traffic, strict fraud controls, and a WooCommerce checkout that already had a lot of careful plumbing. We didn’t add the LLM to “charge” anything. The idea was simple: during checkout, the LLM would review shipping notes and flag likely mistakes (wrong address format, delivery instructions that conflict with carrier rules). We’d show a message, maybe auto-correct a field, then let the order complete like normal.

In staging, it worked. In production, it broke in the most expensive way possible: two requests hit nearly simultaneously for the same cart, and both spawned an LLM call. The LLM response returned, our code wrote the “enriched checkout fields” to the order meta, and then the customer’s confirmation flow fired twice (because the client retried the final AJAX step when it didn’t get a fast enough response).

The actual payment wasn’t duplicated—because WooCommerce guarded the payment gateway transaction—but the order lifecycle got weird: one legitimate order, one near-identical “pending” record, and a third record that got created and then immediately marked as failed by an automated reconciliation job. Our team spent 5 hours untangling it, and support saw a 0.7% spike in checkout anomalies within the first hour after release. That’s not a rounding error when your conversion pipeline is doing real volume.

My position: LLMs don’t belong in the critical path unless you can prove idempotency

I know the conventional advice: “Make the call asynchronous” or “Cache the prompt.” Those are necessary, but not sufficient. The real problem is that the checkout critical path is a concurrency problem, not a latency problem.

The LLM becomes a non-deterministic side-effect generator. Even if your prompt is stable, the output can vary, and it can arrive in a different order than the requests that triggered it. If you write results directly into WooCommerce order meta or change what the customer can submit, you’ve effectively inserted an eventually-consistent system into a strongly ordered one.

So my rule is blunt: if an LLM touches checkout state that can affect order creation or payment, it must be idempotent, replayable, and kill-switchable—and those guarantees have to be enforced in your code, not hoped for in configuration.

Failure mode #1: race conditions in WooCommerce request flow

Here’s the production pattern I’ve now seen twice:

  • User hits “Place order.”
  • Frontend sends an AJAX request to confirm/update checkout fields.
  • Network hiccup causes the frontend to retry (or the browser double-submits).
  • Two nearly identical backend handlers run concurrently.
  • Each handler calls the LLM.
  • Each handler writes meta fields to the same order/cart reference.

In WooCommerce, “order creation” and “meta updates” aren’t always atomic relative to your custom endpoints. If your handler does any of the following, you’re inviting races:

  • Writing to order meta before the order is finalized.
  • Using cart-derived IDs that can change (especially with session updates).
  • Triggering LLM work inside request threads without a lock around the side effect.

We fixed it by introducing a request-scoped idempotency key and by refusing to let the LLM call write “final” state during the checkout request. The LLM can propose; it can’t commit.

The idempotency key we used

We generated a key from stable inputs and used it to ensure we only ever commit one “proposal” per checkout attempt:

checkout_key = hash('sha256', session_id + quote_id + customer_id + cart_checksum)

Where cart_checksum is a hash of relevant items and shipping/billing selections. Then we stored a row with:

  • key
  • state (NEW, RUNNING, READY, COMMITTED, FAILED)
  • llm_request_id
  • response payload hash
  • audit fields (timestamps + actor)

If two PHP workers hit the same handler, the second one sees the key is already RUNNING/READY and never launches another LLM call.

Failure mode #2: non-idempotent LLM calls and retries

The second failure mode is subtle: even if your backend is “correct,” upstream retry behavior will create duplicates unless the LLM call itself is controlled.

In our case, the LLM integration layer could retry on transient network failures. That’s fine for pure “read” tasks, but in checkout you treat the LLM output as a proposed action. You don’t want two different versions of “proposed corrections” racing to overwrite user-facing checkout fields.

Conventional “cache the prompt” advice falls down because the prompt isn’t the only changing variable—you also have model selection, system messages, tool versions, and redaction logic. Cache keys that only include the prompt often become cache poisoning in production.

Instead, we use a two-step commit model:

  • Proposal step stores raw LLM output (or a bounded/validated form) associated with the idempotency key. It is never allowed to directly mutate checkout fields.
  • Commit step happens only once, only when the WooCommerce order is in a safe state, and only after validation + policy checks.

Failure mode #3: missing audit trails (and the post-incident nightmare)

If you’ve ever done a PCI-adjacent incident, you know the real time sink isn’t the bug—it’s proving what happened. When you put an LLM anywhere near checkout, you need audit trails that survive disputes.

We stopped trusting “logs” and started persisting structured audit events. That audit trail must answer:

  • Which checkout attempt did this relate to? (idempotency key + quote/order IDs)
  • What prompt + model + tool versions produced this? (stored with hashes)
  • What did the model propose? (bounded payload, not full secrets)
  • Who/what decided to commit it? (service identity + policy version)
  • What changed in WooCommerce state and when? (before/after hashes)

In our implementation, every proposal and commit produces an immutable record with a monotonic event ID. That way, if we see a duplicate order tomorrow, we can reconstruct the chain without guessing.

Kill-switch and rollback: what we actually built

We didn’t rely on a feature flag that only toggled UI. We needed operational guarantees: the system should fail safe if anything goes wrong.

Our kill-switch patterns were:

  • Hard stop: if the kill-switch is enabled, the checkout handler never calls the LLM and never attempts to commit proposals.
  • Soft degrade: the user still gets normal checkout behavior; AI messages are suppressed.
  • Budget guardrails: per checkout_key and per IP/session we cap tokens/cost; exceed caps -> mark proposal FAILED_BUDGET and never retry automatically.
  • Timeout discipline: we enforce a strict wall-clock timeout in PHP (not just HTTP client timeout). If the LLM doesn’t return quickly, we mark proposal as TIMED_OUT and proceed without AI.

Concretely: we set the checkout critical path budget to 250ms for any AI-related plumbing. The LLM itself runs asynchronously. That single number eliminated the “browser retry created duplicate processing” pattern because the endpoint responded deterministically.

Rollback strategy that doesn’t rewrite history

When we discovered a schema bug that mapped “delivery instructions” into the wrong order meta key, we almost rolled back the feature flag and left it at that. That would have been fine, except some orders had already committed proposals.

Our rollback approach was deterministic:

  • Keep the audit events (no deletion).
  • Add a compensating commit record (e.g., COMMIT_ROLLBACK) that reverts the affected meta keys to the prior value hash.
  • Exclude compromised records from future commits by marking proposal state as INVALID_SCHEMA_VERSION.

That way, we didn’t “fight” WooCommerce with ad hoc database edits—we restored consistency while preserving forensic trail.

Where to run the LLM in a WooCommerce architecture

The easiest way to get this wrong is to cram LLM logic into the WordPress request lifecycle. WordPress + WooCommerce are PHP/FPM and they’ll happily scale concurrency—but your LLM integration becomes a probabilistic external dependency in a critical path.

What we do instead:

  • WooCommerce checkout handler only creates an idempotency key + proposal request row.
  • A queue worker (separate process) runs LLM calls.
  • The worker validates output against a strict schema and policy rules (no free-form “help” text that can impact totals).
  • A commit worker applies changes only when the order is in a safe lifecycle state.

This is also where headless can help: with a headless WP storefront, you can pre-validate inputs and avoid sending anything that would require mutation mid-checkout. But the core is the same: deterministic orchestration, not “AI in the request thread.”

Applied-AI angle: “assist” models are still side-effectful

People think: “It’s only for validation.” But in production, even validation can become side effects. If the AI auto-corrects fields, changes shipping methods, or changes the order meta that downstream rules rely on, it’s effectively participating in order formation.

So you need the same discipline you’d use for any system that can mutate money-adjacent state:

  • Strict output schema (JSON schema + length limits).
  • Policy versioning (what rules were used matters during disputes).
  • Multi-model ensembles can help quality, but don’t pretend they fix orchestration. Ensemble decisions still need idempotency.

In our AI Showcase work, we treat kill-switches and budget guardrails as first-class infrastructure, not “settings.” The same philosophy belongs in WooCommerce checkout.

One last production lesson: cache keys and DKIM-sized surprises

We learned another thing the hard way: caching and concurrency bugs can look like LLM bugs. A stale cache entry can cause the system to reuse a previous proposal for a different checkout_key if your cache key omits something critical (like shipping method ID). That turns into inconsistent behavior that’s hard to reproduce.

It reminded me of email authentication incidents (DKIM alignment breaks) where the failure isn’t “the email,” it’s a dependency on a derived string. For checkout AI, cache keys must include all the state that impacts the decision, or you’ll get “impossible” behavior that your team wastes time chasing.

What you should implement before you ship “AI help” in checkout

  • Idempotency key tied to stable checkout inputs and persisted server-side.
  • Proposal vs commit: AI output can propose; only commit worker mutates order state.
  • Audit events stored as immutable records with prompt/model/policy hashes.
  • Kill-switch that fully prevents LLM calls and prevents commits.
  • Timeout + deterministic response on the checkout request (<250ms budget for AI plumbing).
  • Rollback via compensating commits using prior hashes, not ad hoc edits.

We shipped the corrected pattern to that apparel brand and the checkout anomalies dropped back down—no more duplicate lifecycle records—and support tickets returned to baseline within a day. The real win wasn’t that the LLM got smarter; it’s that the system stopped being fragile.

Quote for Monday: “If an LLM can touch WooCommerce checkout state, it must be proposal/commit idempotent, kill-switchable, and fully audited—otherwise it can break payments indirectly.”

At Champlin Enterprises, we treat AI as an orchestrated subsystem, not a drop-in feature: deterministic queues, persisted audit events, and rollback-safe commits are part of our default engineering approach in WordPress/WooCommerce modernization and our Laravel-based applied-AI products. View our projects

Free Tool

See exactly what AI costs — across every provider.

MyTokenTracker is a free, multi-provider intelligence platform with live pricing across 100+ models. Compare Claude, GPT-4o, Gemini, and more side-by-side — built for developers evaluating models, teams tracking API spend, and founders building AI-native products who want to stay cost-aware before it becomes a line item worth explaining.