> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tybritelabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The Caching Pipeline

> The four cache layers a read passes through, how they stay correct, and the resilience directives that keep the catalog serving during an incident.

A read passes through as many as four cache layers on its way to the database, and each answers what it
can before handing off. In practice most reads never reach Postgres at all, which is what keeps catalog
and pricing calls in the tens of milliseconds worldwide.

## The four layers

<Frame>
  ```mermaid theme={null}
  graph TD
      A["Browser cache<br/>(ETag / If-None-Match → 304)"] --> B["Edge POP cache<br/>(per-location, shared)"]
      B --> C["Distributed cache<br/>(Redis · hot business data)"]
      C --> D["Database<br/>(Postgres · system of record)"]
  ```
</Frame>

<Steps>
  <Step title="Browser cache (validation)">
    Every cacheable response carries an **ETag**. When the browser already has a copy, it sends
    `If-None-Match`; if nothing changed, the server replies **304 Not Modified** with no body — saving
    the entire payload over the wire.
  </Step>

  <Step title="Edge POP cache (first request per location)">
    The first request from a given region runs the worker and stores the response in that location's edge
    cache. Subsequent requests from the same region are served directly from the edge, with no worker,
    database, or cache round trip. A busy region therefore pays the full cost only on the first request.
  </Step>

  <Step title="Distributed cache (hot data)">
    Business data — the product catalog, pricing rules, currencies — lives in a distributed in-memory
    cache. When the edge misses, the worker reads from here, not the database.
  </Step>

  <Step title="Database (system of record)">
    Only a genuine cold read reaches Postgres, and when it does it's a **single indexed query** against
    a purpose-built read view. The result is then written back up through the cache layers.
  </Step>
</Steps>

## The hot-path budget

An authenticated read is held to a fixed budget: at most one database call and one cache round trip. Two
choices make that possible. First, authentication and tenant scoping are combined into a single database
function, so validating the key does not cost a separate lookup. Second, when many requests for the same
cold entry arrive at one location together, they are coalesced into a single database fetch and the rest
wait on its result, so a cache stampede cannot reach the database more than once.

## Write-through invalidation

Business data — the catalog, pricing rules, currencies — is kept correct by invalidation rather than by
expiry. When a merchant edits a product, changes a price, or adjusts stock, the write path refreshes the
affected entries at once, so the cache reflects the change immediately instead of waiting out a timer.
Correctness never depends on an entry expiring.

This invalidation reaches **both cache layers**. A stock change — an order placed, a fulfilment shipped,
an inventory edit — refreshes the shared in-memory cache the workers read from *and* purges the matching
response from the shared edge cache in every location, so a shopper anywhere sees the new availability
right away rather than the copy their nearest edge happened to be holding. (Product responses are tagged
so the whole set for a store can be dropped from the edge in one operation the moment its stock moves.)

Every cached entry still carries a **TTL as a backstop**, though — write-through is the primary mechanism,
not the only one. If an invalidation were ever missed, the entry still ages out on its own (the hot catalog
entries within about fifteen minutes) rather than serving stale data indefinitely. In normal operation you
never see that window, because the write already refreshed the entry; the TTL is the safety net beneath it.

<Note>
  The catalog is cached under **two separate keys** — one for the merchant's in-store/admin view (every
  product, including those not published online) and one for the public storefront view (online-enabled
  products only). Every product or media write busts **both**, so the two surfaces never drift apart.
</Note>

<Note>
  Availability is never *guaranteed* by cache freshness alone. The authoritative check on whether stock is
  actually available happens at checkout, when the order is placed against live inventory — so even in the
  brief moment before a cache refresh lands, the platform will not let two shoppers buy the last unit.
  Fresh catalog reads keep the *displayed* stock honest; the checkout is what prevents overselling.
</Note>

<Warning>
  **For a high-traffic store, or any limited or flash-sale item, reserve stock before checkout.** A cached
  catalog read shows a shopper what was in stock a moment ago; between that read and their checkout, other
  shoppers may take the last units. To guarantee the units are theirs while they pay, call
  [`client.orders.reserveStock`](/sdk/api-reference/classes/OrdersService#reservestock) first — it holds
  the stock atomically (all-or-nothing; you get `409 insufficient_stock` if a unit is already gone) for a
  short window, and the hold converts straight into the order when they complete checkout. This is the same
  mechanism that lets a single hot item survive [tens of thousands of simultaneous
  buyers](/how-gc-works/scaling-reliability) without ever going below zero. It is **optional** — a normal
  order still can't oversell, because the final stock check happens at order time — but for busy or
  scarce-inventory storefronts it is **strongly recommended**: it turns "sorry, it sold out while you were
  paying" into a held unit and a smooth checkout.
</Warning>

Short-lived external data, such as geolocation lookups and currency snapshots, leans on its TTL directly.
Business data leans on write-through first and the TTL only as a fallback — but in both cases a TTL is
present.

## Warming ahead of the shopper

Write-through keeps a busy store's cache warm on its own: every edit repopulates what it changed. The one
moment a shopper could still pay for a cold read is the first request after a quiet store's entry ages out,
or just after a deployment. To keep even that request fast, the hot storefront entries — the catalog, the
product specifications, and the store's default currency — are refreshed on a short schedule for active
stores, so the entry is already present when the next shopper arrives. Dormant stores are skipped, so the
warming does no work for catalogs no one is browsing. The effect is that the occasional cold-start cost is
absorbed ahead of time rather than landing on a real request.

## Freshness and resilience directives

Public reads carry a `Cache-Control` header that tunes freshness at each layer and adds two resilience
behaviours:

```http theme={null}
Cache-Control: public, max-age=60, s-maxage=300,
               stale-while-revalidate=600, stale-if-error=86400
```

<Columns cols={2}>
  <Card title="s-maxage — POP holds longer" icon="clock">
    The browser revalidates after `max-age`, but the shared edge cache holds the response for the
    longer `s-maxage` — so the edge keeps serving a hot response to everyone while individual browsers
    revalidate. A stock or product change doesn't wait this out: it purges the affected entries from the
    edge immediately (see write-through above), so `s-maxage` only ever governs how long an *unchanged*
    response is served.
  </Card>

  <Card title="stale-while-revalidate" icon="arrows-rotate">
    When an entry goes stale, the edge serves the stale copy **immediately** and refreshes it in the
    background — the shopper never waits for a revalidation.
  </Card>

  <Card title="stale-if-error — serve over a 500" icon="shield-halved">
    If the origin errors (a database hiccup, a worker fault), the edge keeps serving the
    last-known-good response for up to a day. For a catalog, slightly-stale data beats a failed page.
  </Card>

  <Card title="Vary — safe personalization" icon="code-branch">
    Responses that legitimately differ by currency, language, or auth are keyed on those headers, so
    variants never collide in a shared cache. Personalized responses bypass the shared cache entirely.
  </Card>
</Columns>

## What is (and isn't) cached

<Info>
  Only **public, non-personalized** reads are edge-cached: the catalog, product detail, collections,
  specifications, brands, taxonomy, the outbound product feed, semantic content, and non-personalized
  pricing. **Personalized responses** (a per-customer priced list, a customer's own cart or messages)
  and **all writes** are never edge-cached — the cache key is the URL, which doesn't carry the shopper's
  identity, so caching them would risk leaking one shopper's data to another. The platform keys these
  out of the shared cache deliberately.
</Info>

***

<CardGroup cols={2}>
  <Card title="Request Lifecycle" icon="route" href="/how-gc-works/request-lifecycle">
    See where these cache layers sit in the full path of a request.
  </Card>

  <Card title="Scaling & Reliability" icon="earth-americas" href="/how-gc-works/scaling-reliability">
    Read replicas, rate limits, and how the platform holds up under stress.
  </Card>
</CardGroup>
