Retail & E-commerce// diagnostic

The price in the cart does not match the price on the product page

In short

Take 3 numbers, not 2: the price rendered on the page, the price the commerce API returns for the same product in the same context, and the price on the cart line. Which 2 of the 3 agree tells you whether you have a stale render, a rounding rule applied at 2 layers, a promotion the cart applies first, or a price list resolved for the wrong buyer.

Key takeaways

  • Compare 3 numbers in one identical context: rendered page, commerce API, cart line. Two numbers cannot locate a fault.
  • The context is part of the query. Market, currency, customer group and session all change the answer.
  • A difference that grows with quantity is rounding. A difference that appears at a basket threshold is a promotion.
  • The most misdiagnosed case has nothing stale in it: the cart is simply the first surface that applies a cart-level rule.
  • Never fix a mismatch by making the cart trust the page. The cart is the surface money is taken against.

A shopper reports one price on the product page and a different one in the basket. The usual investigation compares those 2 numbers, finds they disagree, and starts clearing caches. That comparison cannot name the fault, because at least 4 mechanically different faults produce exactly this symptom and 2 of them involve nothing stale at all.

The third number settles it. Ask your commerce API for the same product, in the same market, currency and customer context the page rendered in, and read the price it returns. Now you have a triangle, and the pair that agrees points straight at the layer that is wrong.

Take the three numbers in one identical context

Most failed investigations fail here, by comparing a page rendered for a logged-in shopper in one market against an API call made anonymously in another. The context is part of the query, not background detail.

  1. Reproduce as the shopper. Same market, same currency, same logged-in state, same customer group, same locale. Note the exact time, because half the causes below are time-dependent.
  2. Read the rendered number from the served HTML, not from what the browser eventually shows. Fetch the page source directly and search for the figure. If it is not in the source, a script wrote it, and that is already a finding.
  3. Query the commerce API directly for the same product and variant, passing the same market, currency and customer context. Record the response, including any tax and currency fields it returns alongside the number.
  4. Add the item to a cart and read the cart line — unit price, line total, and any discount the cart reports.
  5. Repeat at quantity 3. A difference that scales with quantity is a different bug from one that does not, and this single extra step separates rounding from everything else.
  6. Check the response headers on the page request. Age, cache status and any variation headers tell you whether you were served a copy and how old it was.

Which two agree names the fault

PatternWhat it meansNext check
API and cart agree; the page differsA render-path fault. The page is showing a value that was correct earlierCache age on the page response, and whether the figure is in the HTML source or written by a script
Page and API agree; the cart differsA cart-time rule. Something is applied when the line is created, not when the page is renderedWhether the gap equals a promotion amount, and whether it scales with quantity
Page and cart agree; the API differsYour API call used a different context from the storefront. The storefront is self-consistentThe market, currency and customer context you passed. Fix the query, not the site
All 3 differTwo independent faults, or a context mismatch on top of one of the othersFix the context first, re-run, and only then diagnose what remains
Reading the triangle

The first row is the family everyone expects, and it is the same mechanism that makes an availability badge lie on a page that was cached with the value baked in — set out in the in-stock badge is a cached lie. If the figure is absent from the HTML source and appears only after scripts run, look at what a marketing tool may be rewriting in the browser: currency switchers and personalisation snippets both edit displayed prices, and both can be installed without a release, which is the wider problem in the storefront got slower and nobody shipped code.

Rounding: the difference that grows with quantity

Assume a net price of 16.66 and a tax rate of 20%. The gross unit price is 19.992. The product page rounds the unit for display and shows 19.99. The cart computes the line net first — 3 units at 16.66 is 49.98 — then applies tax to the line, giving 59.98. The page implies 59.97. Nobody is stale, nothing is cached, and the shopper is looking at a penny they can see.

  • Rounding per unit and rounding per line produce different totals, and the gap grows with quantity. That is the signature: 1 unit agrees, 3 units do not.
  • Currency conversion doubles the problem when the storefront converts at display time and the cart converts from a stored base, because the 2 conversions happen at different moments and possibly at different rates.
  • The fix is a single rounding authority. One layer decides the shopper-facing number, at one point in the calculation, in one direction, and every other surface renders what it is given rather than recomputing.
  • Where prices are shown tax-inclusive, decide whether the inclusive figure or the exclusive base is the stored truth. Storing both and letting each surface derive the other guarantees a drift you will chase for months.

Two numbers tell you a mismatch exists. Three numbers tell you which layer produced it.

The promotion nobody thinks is a bug

This is the most misdiagnosed of the 4, because there is nothing to fix in the plumbing. A cart-level rule — spend over a threshold, buy 2 get 1, a code, a customer-group discount — cannot be evaluated on a product page, because the page does not know what else is in the basket. The cart is simply the first surface with enough information to apply it, and it applies it correctly.

  • The gap equals a promotion amount exactly, not approximately, and it does not scale with quantity in the way rounding does.
  • It appears only above a threshold, or only with a code, or only for a customer group — and disappears when you remove that condition.
  • It never shows on a single-item basket below any threshold, which is exactly the test most teams do not run.
  • The right response is presentational rather than technical: say on the page that a basket discount applies, or accept the difference and make the cart explain it. Trying to render a cart-level rule on a product page is how a storefront ends up computing prices in 2 places.

The price list resolved for the wrong buyer

The fourth cause is an entitlement fault: the page was rendered against one price list and the cart resolved another. It shows up wherever prices differ by who is asking — contract pricing, trade accounts, customer groups, membership tiers — and the mechanism is almost always a cache key that does not include the buyer.

The signature is that the mismatch is consistent per group and vanishes when you log out. A page cached for an anonymous visitor is served to an account with a negotiated rate; the cart, which is never cached, resolves the correct list. Splitting the page into a shared cached shell and an authenticated price fragment with its own key is the standard answer, worked through in rendering contract pricing without leaking it.

One near neighbour is worth ruling out before you go further. If the page shows the default variant's price while the shopper added a different variant, nothing is wrong with pricing at all — the picker is. That case, and the lattice that prevents it, is covered in a variant picker that never offers a dead end.

Where the tree ends, and who owns each ending

  1. Stale render. Owned by whoever owns the cache strategy. Fix the invalidation path for price specifically — price is not a field that can share a lifetime with the product description.
  2. Client-side rewrite. Owned by whoever approves third-party scripts. A tool that edits displayed prices in the browser is a pricing system, and it should be governed like one.
  3. Rounding. Owned by engineering, settled once, in one layer. Write the rule down: where rounding happens, at what precision, and in which direction.
  4. Cart-level rule. Owned by merchandising. Not a defect — a communication problem, and the fix belongs in the page copy.
  5. Entitlement. Owned by whoever set the cache key. Add the buyer dimension to the key, or move the price out of the cached document.

It is worth checking how many surfaces are quoting a price at all. A native app, a marketplace listing and an AI assistant are 3 more renderings of the same figure, each with its own cache and its own staleness — and an assistant grounded on an index refreshed overnight will confidently quote a number nobody has charged for weeks, which is one of the mechanisms in the assistant keeps recommending discontinued stock.

Price consistency is an architecture property rather than a bug to be squashed: it comes from one authority, one rounding rule and one invalidation path, decided when the storefront is designed. That is storefront architecture work inside retail and e-commerce software, and where the existing shape cannot be made consistent, rebuilding the pricing path is a scoped product build rather than a patch.

Frequently asked questions

Short answers to the follow-ups this page tends to raise.

Why does the price only differ for some shoppers?

Because the price depends on who is asking and the cache key does not. Customer groups, trade accounts, market and currency all change the answer, so a page cached for one audience and served to another will disagree with a cart that resolves the buyer correctly. The test is to reproduce logged out and logged in: a mismatch that only appears in one state is an entitlement fault, not a staleness fault.

The difference is only one penny. Does it matter?

Yes, because the mechanism behind it is not small. A one-unit gap is almost always rounding applied at 2 different layers, which means 2 layers are both computing prices — and the same duplication produces much larger errors on multi-buy promotions, tax-inclusive markets and currency conversion. Fix it as an architecture issue: one authority, one rounding rule, everyone else renders.

How long should a price stay cached on a product page?

Shorter than any other field on the page, and with an explicit purge on price change rather than a timer. Descriptions and imagery can live for hours; a price that is wrong for 10 minutes is a promise you either honour or explain. If your platform cannot purge a single field, split the price out of the cached document and fetch it separately.

Should the product page show cart-level discounts?

Show that one applies, not what it computes to. The page cannot know the rest of the basket, so any figure it renders for a threshold or multi-buy rule is a guess that the cart will then contradict. A short line saying the discount exists sets the expectation without moving promotion logic onto a surface that lacks the inputs to evaluate it.

  • pricing
  • caching
  • storefront architecture
  • diagnostics
// shipped work

The work behind this page

Builds from our portfolio that this page draws on.

Read next

Working on something in this space?

Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.

Start the conversation