Why this matters
Checkout speed is one of the few performance metrics that ties directly to revenue. The industry rule of thumb: every additional second of checkout time costs you ~3% conversion. A 4-second checkout vs an 8-second checkout is the difference between profit and a marketing budget that feels mysteriously inefficient.
The aggregate “average checkout time” hides the truth. The metric that matters is the mobile p99 — what your slowest 1% of mobile users experience. That’s where the conversion bleed happens, because mobile p99 is typically 4-8x worse than desktop p50, and it’s where impulse-buyers live.
GA4 won’t show you this without custom dimensions. Shopify Analytics gives you “store performance” which is a server-side metric, not a per-shopper timing. Agentry computes it from your existing event timestamps in one HogQL query.
What you get
- Percentile distribution (p50, p90, p99) of
order_completed_ts - checkout_started_ts, in seconds - Split by device (mobile / desktop / tablet)
- A publishable dashboard so you can watch it after every checkout optimization
Walk through it
Compute p50, p90, p99 of the time from checkout_started to purchase. Split by device.
Self-join on distinct_id, compute the duration, percentile by device.
agentry_analytics_query {
project_id: "default",
hogql: "WITH starts AS (SELECT distinct_id, properties.$device_type AS device, timestamp AS start_ts FROM events WHERE event = 'checkout_started' AND timestamp >= now() - INTERVAL 30 DAY), purchases AS (SELECT distinct_id, timestamp AS purchase_ts FROM events WHERE event = 'order_completed' AND timestamp >= now() - INTERVAL 30 DAY), durations AS (SELECT s.device AS device, dateDiff('second', s.start_ts, p.purchase_ts) AS duration_s FROM starts s INNER JOIN purchases p ON s.distinct_id = p.distinct_id AND p.purchase_ts > s.start_ts AND p.purchase_ts < s.start_ts + INTERVAL 1 HOUR) SELECT device, count() AS n, round(quantile(0.5)(duration_s)) AS p50_s, round(quantile(0.9)(duration_s)) AS p90_s, round(quantile(0.99)(duration_s)) AS p99_s FROM durations GROUP BY device ORDER BY n DESC"
}
{
"rows": [
{ "device": "Desktop", "n": 2840, "p50_s": 38, "p90_s": 96, "p99_s": 280 },
{ "device": "Mobile", "n": 1240, "p50_s": 72, "p90_s": 320, "p99_s": 1840 },
{ "device": "Tablet", "n": 120, "p50_s": 58, "p90_s": 198, "p99_s": 720 }
]
}
The numbers confirm the usual story, hard:
| Device | n | p50 | p90 | p99 |
|---|---|---|---|---|
| Desktop | 2,840 | 38s | 96s | 280s |
| Mobile | 1,240 | 72s | 320s | 1,840s |
| Tablet | 120 | 58s | 198s | 720s |
Mobile p99 is 30 minutes. The 1% slowest mobile users sit on your checkout page for half an hour before completing. Most of them don’t — they bail (that’s why this recipe pairs with cart-abandonment-funnel, which would show you the mobile-specific abandonment rate).
Desktop p90 of 96s is fine. Mobile p90 of 320s (5.3 min) is the silent killer — that’s the curve where ~10% of mobile shoppers sit waiting and many will tab away.
Two things to investigate:
- Payment-form latency on mobile — Stripe Elements is heavier than the built-in
<input>and slower on low-end Android - Network ping for the confirm POST — mobile users on cellular hit 200-500ms RTTs vs desktop’s <50ms
Want me to publish this so you can watch it as you ship fixes?
agentry_publish_query {
recipe_id: "checkout_duration_percentiles",
description: "Checkout Duration p50/p90/p99 by Device (30d)",
params: { window_days: 30 }
}
{ "publication_id": "pub_chk_1", "public_url": "https://agentry.sh/q/pub_chk_1?key=agp_…" }
The output
Checkout Duration (30d)
DEVICE N p50 p90 p99
Desktop 2,840 38s ▎ 96s ▎▎ 280s ▎▎▎▎
Mobile 1,240 72s ▎▎ 320s ▎▎▎▎ 1,840s ▎▎▎▎▎▎▎▎▎▎▎▎▎▎▎▎▎▎▎ ★ FIX
Tablet 120 58s ▎▎ 198s ▎▎▎ 720s ▎▎▎▎▎▎▎▎▎▎▎
KEY NUMBERS:
Desktop p50 (good baseline): 38s
Mobile p50: 72s (1.9x desktop)
Mobile p90: 320s (3.3x desktop)
Mobile p99: 1,840s (6.6x desktop) ← optimization target
REVENUE IMPACT (rough, using 3%-per-second heuristic):
Reducing mobile p90 from 320s → 200s would lift mobile conversion by ~12%
At your current 1,240 mobile completions / 30d, that's +150 orders / month
At avg order $69 (your current AOV), ~$10k/month uplift
Public dashboard:
https://agentry.sh/q/pub_chk_1?key=agp_xxx
SUGGESTED INVESTIGATIONS:
- Replace Stripe Elements with Payment Request / Apple Pay on mobile
- Preconnect to api.stripe.com on checkout page load
- Lazy-load the confirmation page JS bundle on /checkout
Setting it up
You need two events with timestamps and a device-type tag:
// Detect device once at page load
const deviceType = /Mobi|Android/i.test(navigator.userAgent) ? "Mobile"
: /Tablet|iPad/i.test(navigator.userAgent) ? "Tablet"
: "Desktop";
async function track(event: string, properties: Record<string, unknown> = {}) {
await fetch(`https://api.agentry.sh/v1/analytics/${process.env.AGENTRY_PROJECT_ID}/`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.AGENTRY_DSN}`,
"Content-Type": "application/json",
"User-Agent": "myshop/1.0", // REQUIRED — Cloudflare 403s default UAs
},
body: JSON.stringify({
event,
distinct_id: visitorId,
properties: { ...properties, $device_type: deviceType },
}),
});
}
// At checkout page mount:
track("checkout_started", { cart_value: cart.total });
// At order confirmation page:
track("order_completed", { order_id: order.id, total: order.total });
If you use the PostHog-shaped alias /v1/track/, $device_type is parsed from the User-Agent automatically — no manual sniff needed.
For sub-second precision, the recipe also works if you send a single checkout_completed_in_s property on order_completed (computed client-side as Date.now() - checkoutStartedAt). HogQL is happier with explicit durations than with timestamp diffs at scale.
Variations
- “Same query but split by BROWSER as well — old Safari might dominate the slow tail.”
- “Split by checkout payment method — is Stripe Link faster than card form?”
- “Compute the same percentiles for product_page_load_ms and add_to_cart_response_ms. Where else is the friction?”
- “Watch this metric: alert me if mobile p90 increases by >20% week-over-week.” (uses
agentry_create_alert)