In short and in plain language
A cache is a temporary store. It makes the shop faster. But it shows a copy with an older state.
If a price or a stock level is in the copy, errors can happen. The page then shows a different price than the checkout. Or an item that is sold out can still be ordered.
Personal data like a basket does not belong in a shared cache. Otherwise a customer sees someone else's data.
The article shows how to measure such errors. We check prices and stock after every change to the shop and report differences.
A cache is not an accelerator, it is a copy - and every copy carries a timestamp. As long as only images, fonts and text fragments are stored, nobody notices. But as soon as a price, a stock level or a shipping rule sits in a copy, the database no longer decides what the customer sees. The oldest layer that still answers does. That is where cache drift begins: for a sample item the detail page shows 49.90 euros, the checkout calculates 44.90 euros, a sold-out item stays orderable - and every one of those answers arrives with status 200. No entry in the error log, no alert, no unusual load time. Anyone serving responses without an explicit expiry leaves the lifetime to the cache anyway: as a typical share of the interval since the last change, the standard names 10% (RFC 9111, Section 4.2.2). This article therefore treats caching not as a question of speed but as a question of correctness - which response may enter a shared store at all, how to measure drift between listing, detail page and cart, and which check is due after every deployment.
Key takeaways
- Without an explicit expiry a cache estimates freshness itself; as a typical share of the interval since the last change the standard names 10% (RFC 9111, Section 4.2.2). The moment of the last write then decides how long a price is considered valid.
- Personalised responses do not belong in a shared store: if a response carries the private directive, a shared cache must not store it (RFC 9111, Section 5.2.2.7). Without that directive, serving someone else's cart is not an exotic accident but the logical consequence.
- Invalidation is the real checkpoint. After an unsafe request a cache must invalidate the target URI (RFC 9111, Section 4.4), and when a product changes, every cached page showing that product has to be dropped (Shopware documentation, cache tags).
- An edge layer in front of the shop follows its own rules: a field targeted at it displaces Cache-Control and Expires entirely (RFC 9213, Section 2.2). Reading only the headers of the shop means measuring the wrong layer.
- Drift can be measured without load: query listing, detail page and cart within one minute, compare the price shown and record the cache status. Whether a response came from a store is stated by the hit parameter (RFC 9211, Section 2.1).
- A wrongly stated total price is not a cosmetic issue: the duty to state the total price is set out in the German price indication rules (Section 3(1) PAngV), and the fine reaches up to twenty-five thousand euros (Section 3(2) WiStrG 1954).
Caching is a correctness problem, not a speed problem
In almost every description caching appears as a lever for load time: fewer database queries, faster delivery, better numbers in the measurement. That is true, and it is too narrow. A cache answers the question „what was true most recently?“ and not the question „what is true now?“. In a shop those are two different things as soon as prices are maintained, stock is written forward or campaigns end. Steering caching by speed alone optimises one metric and accepts a source of error that no speed measurement will ever show. The question of whether a response is still valid therefore belongs in the same monitoring as availability and response time - not in a separate chapter for specialists.
The second reason cache drift stays undetected so long is how it behaves: it does not cause an outage. The page loads, the cart fills, the order goes through. A stale price looks exactly like a correct one, a stale stock level like an available one. The issue only surfaces in accounting, in customer service or in a complaint - days later and far away from the cause. Unlike a failure in the payment path there is no moment at which the system itself says that something is wrong. Whoever wants to find the deviation has to measure it.
What a cache hit tells you - and what it does not
Three layers, three truths
A typical setup has at least three stores in a row. The object cache holds the results of individual calculations: resolved prices, category trees, rule sets. The page cache of the shop holds whole HTML responses and saves the entire page build; the developer documentation lists it as mandatory for every production system (Shopware documentation, HTTP cache). In front of that there is often an edge cache keeping responses close to the visitor. Each of these layers has its own lifetime, its own purge and its own key. Drift does not appear because one layer works incorrectly, but because three correctly working layers are of different ages.
The key is the underestimated part. Two responses that look like the same page to a human can be two entries for the cache - or, the other way round, two different responses can be a single entry. If a response is separated by the Vary field by language, currency or customer group, the stored response may only be used without revalidation when the nominated header fields of the new request match those of the original one (RFC 9111, Section 4.1). Without that field the customer group with special conditions gets the list price - or everybody else gets the special price.
| Layer | What is stored there | Typical drift case |
|---|---|---|
| Object cache | resolved prices, rule sets, category trees | The pricing rule is maintained, the stored value is from yesterday |
| Page cache in the shop | whole HTML responses per key | The detail page shows the old price, the checkout calculates the new one |
| Edge cache | responses kept close to the visitor | According to RFC 9111 the layer guesses roughly 10% of the interval since the last change |
| Customer browser | responses for exactly one person | A hard maximum age keeps the old price alive for days |
| Search index and price portals | values read out of the page | The error travels outwards and is cached again out there |
In practice this means a single look at the detail page proves nothing. Only comparing the same value across several layers shows whether the shop has one truth or several. How closely this is tied to the rest of operational monitoring is described in the article on performance monitoring in the maintenance contract: the same measuring points, only with a different question.
Without an expiry the cache guesses
Responses without an explicit expiry are the norm, not the exception. For them a cache may estimate a lifetime, and the standard names as a typical setting a fraction of 10% (RFC 9111, Section 4.2.2) of the interval since the last modification. That sounds harmless, but it couples validity to a completely unrelated quantity: a page unchanged for thirty days receives a lifetime of roughly three days - regardless of the fact that its price changes tomorrow morning. The longer an item stays unchanged, the longer the store holds on to its copy. The stable item is the dangerous one.
The countermeasure is not artistry but discipline: every response carrying a price or a stock level gets an explicit lifetime. For shared stores there is a separate directive whose maximum age overrides both the general maximum age and the expiry date (RFC 9111, Section 5.2.2.10). That allows a short lifetime where many people see the same copy, without forcing the individual customer's browser into the same rhythm. The opposite direction matters just as much: an expired copy must not simply be passed on when an explicit directive forbids it (RFC 9111, Section 4.2.4).
The lifetime belongs on the response, not in a configuration file
One layer likes to stay unnoticed. For caches close to delivery there is a dedicated header field, and where it is set the layer must ignore Cache-Control and Expires in that response (RFC 9213, Section 2.2). A carefully chosen lifetime in the shop is then without effect, and no contradiction is reported anywhere. This layer has to be measured separately - with the same tools but from the outside, much as the article on image maintenance for short load times describes it for static files.
What must never enter a shared cache
The most expensive error in this family is not the stale price, it is the stranger's page. It appears when a personalised response ends up in a store that many visitors share. The standard is unambiguous here: if a response carries the private directive, a shared cache must not store it, because the response is intended for a single user (RFC 9111, Section 5.2.2.7). Without that directive only the cache key decides who receives the copy - and the key knows nothing about a session unless somebody handed it over.
Two more rules belong in the same check. A shared store must not reuse the response to a request carrying an Authorization header for later requests unless the response explicitly allows it (RFC 9111, Section 3.5). And where nothing at all is to be stored, there is a clear directive: the cache must then store no part of either the request or the response (RFC 9111, Section 5.2.2.5). These three rules are precisely the ones that quietly disappear after the delivery path is rebuilt.
The unqualified private response directive indicates that a shared cache MUST NOT store the response (i.e., the response is intended for a single user).
Cart and session
Anything bound to a session identifier belongs in no shared store. The damage here is not a wrong price but somebody else's order.
Signed-in views
Customer account, wish list, invoice overview. If the authorisation rule is ignored, a single cached response is enough for a reportable incident.
Prices per customer group
Tiered prices, net views and campaigns for signed-in customers need a key that contains the group - otherwise everyone sees the price of the first request.
Separation by header fields
A stored response may serve without revalidation only when the nominated header fields match (RFC 9111, Section 4.1). Each field multiplies the number of entries.
Payment and shipping
Both depend on country, group and cart value. A cached selection produces a total that no longer applies one step later.
Error and maintenance pages
A short disruption stored with a long lifetime outlives the disruption by hours. That is the case which costs revenue fastest.
Separation by header fields is a double-edged tool. Every additional field multiplies the number of entries and lowers the hit ratio; too few fields mix views that do not belong together. The workable middle road is to lift the personalised parts out of the cached page and load them separately. The page itself then stays highly cacheable, and the part that belongs to one person is never shared.
Measure price drift instead of guessing
A measurement needs three ingredients: the same value in several places, a narrow time frame and a record. For a shop that means querying the price of an item in the category listing, on the detail page, in the cart interface and in the checkout within one minute, recording the total price shown and the availability each time, and additionally noting which layer answered. Whether the response came from a store is stated in the cache status (RFC 9211, Section 2.1); the age of the copy is in the Age field. If two values differ, the drift is documented - with timestamp, address and layer.
The measurement belongs on a selection, not on the whole catalogue: the twenty items with the highest revenue, everything in a running campaign, everything with low stock. One run every fifteen minutes is enough in daily business; around price maintenance or a campaign the rhythm is shortened for a few hours. What matters is the comparison against the source: the value in the database is the truth, everything else is a copy with an age. Attaching the measurement to the existing monitoring of response times puts both pictures into one view.
#!/usr/bin/env bash
# Query the same item across several layers and report the deviation.
set -euo pipefail
BASE="https://www.example-shop.test"
ITEM="/product/SW10001"
SOURCE=$(mysql -N -B -e "SELECT price FROM price_current WHERE number='SW10001'")
measure() {
local path="$1" name="$2"
local head body price status age
head=$(curl -sS -D - -o /tmp/body.$$ --max-time 10 "${BASE}${path}")
body=$(cat /tmp/body.$$); rm -f /tmp/body.$$
price=$(printf '%s' "$body" | grep -oP '"price"\s*:\s*\K[0-9.]+' | head -1)
status=$(printf '%s' "$head" | grep -i '^cache-status:' | tr -d '\r' || true)
age=$(printf '%s' "$head" | grep -i '^age:' | tr -d '\r' || true)
printf '%-22s %-10s %-28s %s\n' "$name" "${price:-?}" "${status:-none}" "${age:-none}"
if [ -n "${price:-}" ] && [ "$price" != "$SOURCE" ]; then
printf 'DRIFT %s: %s instead of %s\n' "$name" "$price" "$SOURCE" >&2
fi
}
printf '%-22s %-10s %-28s %s\n' LAYER PRICE CACHE-STATUS AGE
measure "/category/campaigns" "Listing"
measure "$ITEM" "Detail page"
measure "/store-api/product/SW10001" "Interface"
measure "/checkout/cart" "Cart"
The check after every deployment
Cache rules rarely get lost during normal operation. They get lost during rebuilds: when the delivery path changes, when an extension that sets its own headers is updated, when the server configuration is adjusted or when an intermediate layer is introduced. That is why a short, always identical checklist belongs at the end of every maintenance window - worked through on the production system, not on the copy, because only there do all layers sit in the real order.
- Headers of a product page read: is an explicit lifetime set, and does it match how often the price is maintained?
- Cart and customer account checked: do these responses carry the private directive, and does the layer in front forward instead of reporting a hit?
- A price change applied to a test item and measured: after how many seconds does it arrive in listing, detail page and cart?
- Stock of an item set to zero and verified that orderability disappears in every layer, not only on the detail page.
- The header field for the delivery-side layer inspected, because it displaces the shop's own values in that response (RFC 9213, Section 2.2).
- Invalidation after an unsafe request reproduced: following an order, the target URI has to be invalidated (RFC 9111, Section 4.4).
- Log of the last purge reviewed - when, by whom, which scope, and whether background processing actually completed it.
- Result written into the same report that receives regression tests after updates, so that two checks share one place.
In practised hands this list costs ten minutes. It does not replace continuous monitoring, but it finds exactly the class of errors that a rebuild creates and that then stays for months. Anyone who prepares changes in separate environments anyway can work through half of the list there - the second half stays reserved for production, because the layer in front is a different one there.
When invalidation fails
A lifetime is one half of the answer, targeted invalidation is the other. A cache has to invalidate the target URI once it sees a non-error response to an unsafe request (RFC 9111, Section 4.4) - an order, a change, a deletion. In a shop a second level is added: when a product changes, every cached page that shows that product must be dropped (Shopware documentation, cache tags). That is rarely a handful of pages - one item sits in listings, search results, recommendations and home page blocks at the same time. Which is exactly why tagging those relationships is the most sensitive part of the whole setup.
Detect
The entry point is always a deviation with a timestamp: the source says 44.90, one layer says 49.90. Without that record every investigation starts from a guess. The continuous measurement supplies it as a by-product, including the age of the copy and the layer that answered.
Narrow down
Query the same address in turn with and without the layer in front, then straight against the application. The first layer that returns the correct value narrows the fault to the layer ahead of it. That takes minutes and replaces a long discussion about responsibilities.
Relieve
Only now is anything purged - for the affected address or the affected tag, not for the whole store. A full purge during business hours merely moves the problem: it creates a load spike and erases the evidence at the same time.
Remove the cause
A missing tag, an ignored directive, an extension with headers of its own or background processing that never works off its queue. The case is closed only once the cause is named and the check has been repeated.
The entry point is always a deviation with a timestamp: the source says 44.90, one layer says 49.90. Without that record every investigation starts from a guess. The continuous measurement supplies it as a by-product, including the age of the copy and the layer that answered.
Query the same address in turn with and without the layer in front, then straight against the application. The first layer that returns the correct value narrows the fault to the layer ahead of it. That takes minutes and replaces a long discussion about responsibilities.
Only now is anything purged - for the affected address or the affected tag, not for the whole store. A full purge during business hours merely moves the problem: it creates a load spike and erases the evidence at the same time.
A missing tag, an ignored directive, an extension with headers of its own or background processing that never works off its queue. The case is closed only once the cause is named and the check has been repeated.
The fourth point deserves particular attention because from the outside it looks like a cache problem and is none. If invalidation is handed to a queue and the responsible process is not running, everything stalls: the application records the change correctly, the store never hears about it. How such silent failures in background processing can be watched therefore belongs in the same rhythm of checks. If the disruption produces wrong prices during live trading it is a case for emergency support and not for the next maintenance window.
What a wrong price means in law
Technically a stale price is a copy. In law it is a statement made to consumers. Anyone offering goods or advertising with prices has to state the total price (Section 3(1) PAngV). Breaches are explicitly framed as a regulatory offence (Section 20 PAngV), and the frame for it sits in the German act on economic offences: up to twenty-five thousand euros (Section 3(2) WiStrG 1954). That figure is a ceiling, not an expectation - what matters is that a technical cause does not remove responsibility. From the point of view of the rule the cache is not a party, it is a tool of the vendor.
Next to it stands competition law. A commercial practice is misleading if it contains untrue statements or statements capable of deception about certain circumstances (Section 5(2) UWG). Named explicitly are the essential characteristics of the goods including availability (Section 5(2) no. 1 UWG) as well as the price and the manner in which it is calculated (Section 5(2) no. 2 UWG). A sold-out item that stays orderable out of an old copy therefore hits both points at once - regardless of whether anyone intended it.
Total price
The duty attaches to the statement, not to the database. Whether the correct value sat in the system is irrelevant for judging the page that was delivered.
Availability
The statement about deliverability counts among the essential characteristics. Stock from an old copy is therefore the same class of error as an old price.
Information before the order
Immediately before the order is placed the prescribed information has to be provided clearly and prominently (Section 312j(2) BGB) - from the source, never from a store.
Delivery restrictions
At the latest at the start of the ordering process it must be stated whether delivery restrictions apply and which means of payment are accepted (Section 312j(1) BGB).
Provability
Without a log there is no way to show afterwards how long a wrong statement was served. The measurement series is therefore also a file.
Follow-up costs
Beside the fine there are warning-letter costs, unwinding of orders and support effort. The technical effort for a measurement is small against that.
The conclusion that follows is not „less caching“ but „deliberate caching with a record“. Anyone who defines the rules for each page type once, writes them down and checks them after every rebuild has a defensible position. How the textual side of these duties is maintained is described in the article on maintaining legal texts and price information; the technical path by which a correct statement still arrives wrong at the customer is the complement to it.
Anchor cache rules in the maintenance contract
What is not written down is done differently after the third rebuild. So four things belong in writing: the lifetime per page type, the list of responses that must never be shared, the trigger for every invalidation and the check that runs after every deployment. Those four points fit on one page and in practice save long investigations, because they turn every deviation into a question about the intended state instead of a question about opinions. A maintenance contract with fixed response times is a good home for them, because there they are tied to responsibility and a deadline.
In operation two rhythms complement each other: the continuous measurement that reports any deviation within minutes, and the check in the maintenance window that restores the intended state after every intervention. How such windows can be placed without losing revenue is shown in the article on maintenance windows during live trading; how closely delivery and visibility are connected, the article on monitoring indexability. An overview of the scope of ongoing care is on the page about managed service offerings; anyone who would like the current state measured first will find the entry point via Shopware maintenance or directly through an enquiry with access to the shop.
Sources and studies
Related Articles
Discount codes in Black Week: detecting abuse early
Discount codes in Black Week: code length, a rate limit at the coupon field, alerts on failed attempts and new-customer discounts that resist multiple accounts.
Disk space monitoring before the shop stops
A full data partition does not cause an outage, it causes stuck orders. Which directories grow, which threshold works and what a dated cleanup cycle covers.
Monitoring indexability: when noindex reaches production
A noindex carried over from staging is often noticed only when revenue drops. Here is a five-stage check chain that reports deviations on the same day.