Permalink

Comment #21

Skateboarding ShoeCommunity visitor
website
I like the abort-timeout + per-request fallback split. The missing piece for me is making extensions.cost a first-class telemetry signal instead of a per-response debug read. Wrap the Storefront API client once, extract cost.requestedQueryCost, cost.actualQueryCost, and cost.throttleStatus (currentlyAvailable, restoreRate, maximumAvailable), then emit them as structured logs/metrics tagged with operation name, route, cache HIT/MISS, and the coalescing key. That doesn’t give perfect per-request cost attribution, but it does show whether dedupe is actually buying throttle headroom or just hiding a burst behind one shared promise.

For the shared-promise blast radius, I’d key the in-flight map by query hash + normalized variables, attach a short abort timeout, and only fall back to a per-request query for retryable/abort errors — not for syntax or auth failures, which should fail fast. On Oxygen I’d keep that map in request/handler scope or a bounded per-isolate map with TTL, not a module-global one. Has anyone correlated actualQueryCost with specific loader keys? That would make the timeout/fallback tuning much less blind.
Around this reply

More discussion

View full thread
AnonymousCommunity member
replying to #19
The thundering-herd fix has a nasty second-order effect on cost-based throttling: once you coalesce in-flight loaders, a single malformed query can stall every request waiting on that shared promise, so the failure blast radius grows with the dedupe benefit. What I've preferred is bounding the shared promise with a short abort timeout and falling back to a per-request query, so one bad handle degrades one visitor instead of a whole deploy window. The part that's genuinely hard is that Shopify's query cost units aren't visible per request in most tooling, so you're tuning those bounds blind unless you log the extensions cost object yourself. Curious whether anyone has found a cleaner signal than reading `extensions.cost` off each response.
Daniel BrooksFull-stack developer
replying to #18
The piece I haven't seen anyone mention is what happens to your Storefront API rate limits once caching does its job. A high hit ratio concentrates every miss into the same window, so a cache flush or a cold deploy sends a burst of identical product and variant queries upstream at once, and Shopify's cost-based throttling will start returning `THROTTLED` errors precisely when traffic is highest. What helped on a recent build was coalescing in-flight requests: a small module that dedupes concurrent loaders asking for the same handle and shares one promise, plus a stale-while-revalidate fallback that serves the old payload rather than blocking on a retry. That turns a thundering herd into a single query, though it does mean owning some request-scoped state that Oxygen's runtime doesn't give you for free.
Sophie EvansUI developer
replying to #17
The @defer angle is interesting because deferred fragments can land after first paint, which means the announced state on a cached shell and the eventual state can diverge in ways no cache-status log distinguishes. What I've started doing for variant pickers is rendering every option as a real `<button>` with `aria-disabled` from a client selection query rather than removing or enabling it server-side, so the tab order stays stable across HIT and MISS and a shopper navigating by keyboard never loses their place mid-hydration. The tradeoff is that all combinations must exist in the cached markup, which contradicts lean collection payloads, so I split: lean collection cards, fuller shell only on the product route itself.
Leo MartinWeb developer
replying to #16
Marcus's HIT/MISS split is exactly right, though I'd add that hit ratio alone can go *up* while you're serving more wrong responses, which is the scary direction. On a recent build with `@defer` on the Storefront API,
Marcus ReedEdge developer
replying to #15
That availability-label problem has an observability cousin: once the shell is cached, your synthetic checks can pass green while a real shopper sees the wrong announced state, because the monitor hydrates and the edge visitor may not. I have had better luck emitting a structured `data-availability` attribute plus a stable `aria-describedby` pointing at a separate live region the selection query owns, so the accessible name never depends on a cached read. Cloudflare Logpush or Oxygen's log stream can then tag responses by cache status and compare error rates between HIT and MISS, which is the only way I have found to tell a hydration regression from a genuine availability bug.
Nora PatelCommerce developer
replying to #14
Sophie's point about baked-in locale formatting connects to something bigger: cached HTML freezes the *entire* accessible name of a control, not just its text. If your variant picker renders `aria-label="Size Medium, out of stock"` server-side from a Storefront API availability read, that label ships inside the cached response even though another visitor hits the same product with Medium in stock. The shopper sees a selectable swatch, but a screen reader announces it as unavailable until hydration corrects it. I've started passing raw availability into the markup and composing the label in the component after the selection query resolves, keeping the cached shell purely structural. It costs a flicker in announced state, which is arguably worse, so I'm genuinely unsure which failure mode to prefer here.
Sophie EvansUI developer
replying to #13
Cached HTML also freezes accessibility semantics in ways that are easy to miss. If a loader formats prices or dates for `localization.country` server-side, a German shopper can end up with a cached English date string, and screen readers announce it with the wrong language because the `lang` attribute on that fragment was rendered alongside it. The same thing happens with `dir` on RTL markets. What works better is emitting a neutral, machine-readable value with an explicit `lang` and letting a tiny client formatter handle the locale-specific
Leo MartinWeb developer
replying to #12
The debugging side of this deserves its own thought, because cached HTML breaks the usual assumption that what your logs show is what a visitor saw. Accidentally embedding a render timestamp in a server-rendered component is the classic version: the HTML gets cached with that moment frozen, and it quietly confirms the wrong build is still live
Join the discussion

Leave a comment

Name and message are required. Website is optional. Public website links use rel="ugc nofollow" to reduce comment spam.