The cart already has items. The shopper clicked "Checkout."
Then nothing happens for two seconds.
(I work on ApexCache — edge caching for GET-heavy APIs. The checkout advice stands if you use another proxy; the invalidation details are what bite you either way.)
That pause is when you lose orders that were already yours. Not because the product was wrong. Because waiting feels like something broke, and "I'll finish later" is easier than staring at a spinner.
This post is about the read side of checkout: the APIs that paint shipping, tax, and payment UI. Those are cacheable. Placing the order is not.
Checkout is a different mental mode than browsing
On a category page, slow load is annoying. On checkout, slow load reads as risk.
Shoppers ask silent questions while the UI hangs:
- Did the last step save?
- Will I get charged twice?
- Is this site legit?
Mobile makes exit cheap: back button, home screen, another tab. Intent was high a moment ago. Intent decays per second of empty UI.
Industry checkout studies (Baymard Institute and similar) keep pointing at the same operational issue: friction and delay on late funnel steps correlate with abandonment. You do not need a psychology degree to see it in session replays — watch ten checkouts on 4G and count the sighs.
The checkout waterfall nobody optimizes
Teams ship image CDN and font preloads for the storefront. Checkout often talks to a different service mesh:
GET /api/cart
GET /api/shipping-methods?cart_id=…&zip=94107
GET /api/tax-estimate?cart_id=…
GET /api/payment-config
POST /api/orders ← must hit origin, always
In traces we see the first three GETs each open a DB connection, run nearly identical joins, and return JSON that does not change until the cart changes. Under a sale, p95 on those endpoints triples while conversion on the same traffic source drops.
That is not a copy problem. It is an architecture problem.
Cache the screen painters, bypass the money movers
ApexCache only caches GET and HEAD. Everything else passes through to origin.
For checkout that is the right split:
| Endpoint | Edge cache | Why |
|---|---|---|
GET /api/cart |
Short TTL (15–60s) + cache tag per cart | Same cart id, same JSON until line items change |
GET /api/shipping-rates |
Yes, with query canonicalization | Same zip + cart weight → same rates |
GET /api/tax-estimate |
Yes, tight TTL | Recompute when address or cart changes |
GET /api/payment-methods |
Longer TTL | Changes rarely |
POST /api/orders, payment capture, webhooks |
Bypass | Non-negotiable |
If your platform uses GraphQL for checkout, the rule is the same: cache only idempotent read operations with explicit cache keys — not mutation checkoutComplete.
Example policy mindset (pseudo-config):
# Illustrative — match your dashboard or proxy rules
- path: /api/shipping-rates*
methods: [GET]
ttl: 120s
cache_tag_header: X-Cache-Tag
- path: /api/orders
methods: [POST]
action: bypass
Query strings are why shipping-rate caches miss
Two requests that mean the same thing:
GET /api/shipping-rates?zip=94107&cart_id=abc
GET /api/shipping-rates?cart_id=abc&zip=94107
A naive cache treats them as different keys. Hit rate tanks; origin still melts.
Canonicalize parameter order (and strip utm_* junk) before the cache key is computed. We do this at the edge so your application code stays dumb.
Stale shipping price is worse than slow shipping price
Speed without correctness loses trust faster than latency.
When inventory or price changes:
- Purge by tag (
product:SKU123,cart:abc) via webhook or CDC — Postgres WAL, MySQL binlog, whatever your stack already emits. - Do not rely on a 30-minute TTL for stock-sensitive reads during a flash sale.
We target sub-10ms invalidation on tagged entries in product docs; your job is to emit the tag when the row changes, not to manually purge CDN URLs.
Tradeoff in plain terms: event-driven invalidation costs engineering time up front; fixed long TTLs cost you refunds and angry tweets later.
SingleFlight when everyone checks out at once
TV ad airs. Five thousand people hit GET /api/shipping-rates?zip=10001 in the same second.
Without request coalescing, that is five thousand identical SQL queries.
With SingleFlight at the edge, one query runs; the rest wait on the in-memory result. p99 drops; the checkout step does not stutter because the database hiccuped.
This matters more at checkout than on catalog browse because every user is on the same few endpoints at the same time — shipping and tax, not long-tail SEO URLs.
Measure the right thing
Lighthouse on the marketing homepage is the wrong scoreboard.
Try this instead:
- Open checkout on throttled 4G in DevTools.
- Sort the network panel by duration for the step where users abandon (often shipping or payment).
- Note TTFB on the top three GETs.
- Replay the same GET with
curl -Itwice; look for a cache hit header on the second call.
curl -sI "https://api.yourstore.com/api/shipping-rates?cart_id=abc&zip=94107" | grep -i x-apexcache
First request: MISS (expected). Second: HIT — you removed an origin round trip from a step where hesitation is expensive.
Pair that with completion rate on the same funnel step in analytics. If median load time on shipping drops 400ms and step-three completion rises, you have evidence, not a blog claim.
What we are not saying
- Edge cache does not replace a broken payment provider.
It does remove pointless database work on reads the shopper already triggered once this session.
What I would do next on your stack
- Open ApexCache — skim how bypass vs cache policies are split for write paths.
-
Start free — point a staging API hostname, add one rule for
/api/shipping-rates*or your hottest checkout GET. - Run checkout twice; confirm the second shipping-rate request is a cache hit without changing cart contents.
Docs: getapexcache.com/docs · Questions: getapexcache.com/contact
I work on ApexCache. Session replay beats industry averages — record your own checkout before you trust any conversion math from a vendor blog, including this one.
Top comments (1)