Is checkout too slow?

Compute p50/p90/p99 of checkout duration split by mobile and desktop. Mobile p99 is usually 4-8x worse than desktop p50.

Published

difficulty intermediate · time to value 2 minutes · tools used 2

Just say this

Compute p50, p90, p99 of the time from checkout_started to purchase. Split by device (mobile vs desktop).

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

You

Compute p50, p90, p99 of the time from checkout_started to purchase. Split by device.

Agent

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 }
  ]
}
Agent

The numbers confirm the usual story, hard:

Devicenp50p90p99
Desktop2,84038s96s280s
Mobile1,24072s320s1,840s
Tablet12058s198s720s

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:

  1. Payment-form latency on mobile — Stripe Elements is heavier than the built-in <input> and slower on low-end Android
  2. 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)

Try this recipe in your own agent.

Paste the prompt above into your agent. It'll set up Agentry against your data, then run the recipe and show you the real output.

Install https://agentry.sh/agentry.md and set it up