HomeBilby — store monitorFree store scanFree toolsLive status: is it down?AI appsNavaal SEO appBlogServicesStudioContactSign in

// Bilby · the data report

The State of Broken Online Stores.

Of 1,483 public Shopify stores Bilby visited in a real browser, 38% had a broken or degraded buying path on the night we visited them.

Measured: 1,483 visits between 1 September 2026 and 2 September 2026 (UTC), one night per store, computed live from the visit records when this page was served (9 November 2026). Our own demo store and test hosts are excluded from every figure.

Across every kind of visit, not just this study’s crawl: 2,599 completed visits of 1,797 real stores, 27 August to 9 November 2026, of which 38.1% had at least one serious issue (P1 or P2). One definition of a visit and of an issue, in lib/store-visits.ts, read by this page, /about and /scoreboard — the three used to disagree.

US830 stores · 821 visitedAU362 stores · 362 visitedEU176 stores · 175 visitedUK67 stores · 67 visitedCA65 stores · 65 visited
The sample: public Shopify storefronts from the latest Common Crawl index, verified as live Shopify and placed in a market by their currency. Grey is the sample; green is visited so far. Live from the table of stores visited.

Of 1,483 public Shopify storefronts visited in a real browser through to the payment step, 37.7% had a broken or degraded buying path on the night they were visited.

1,483 visits of 1,483 distinct stores, one night each, between 1 September 2026 and 2 September 2026 (UTC). Computed from the visit records when this page was served (9 November 2026). Our own demo store and test hosts are excluded from every figure.

The sample

public Shopify storefronts, found from public sources and reachable without a login — so the study is about Shopify stores and says so rather than generalising to every platform. Each store was visited once, at night in its own time zone, from the region closest to its market. A store is in the sample if its walk completed and was scored; a walk that fell over measured nothing and is not counted as a healthy store.

Markets are the store’s own currency, recorded when the sample was built — not the region the browser ran from. A cut by region once put 212 US-market stores in a Canadian row, which is the kind of error that makes a study worthless, so the two are kept separate. A market’s row is only shown once it passes 50 stores.

The method, and what it cannot tell you

A real Chromium browser at 1440×900 and 390×844 walks home → collection → product → add-to-cart → cart → checkout, stopping at the payment step. Deterministic checks run first; a language model judges each page from its screenshot under a fixed set of rules, and is never asked about load time — that is measured twice in code, so a judged finding about a slow page cannot be invented. robots.txt is respected. Nothing is ever ordered.

Each store, once, at night in its own time zone1440×900 and 390×844, a real Chromium browser, robots.txt respectedHomeloads, rendersCollectionproducts listedProductprice, variantsAdd to cartdoes the cart moveCartsubtotal, shippingCheckoutstops hereMeasured in CODE: load time, layout shift, blocking time — so the model cannot invent a slow page.Judged from the screenshot under fixed rules: everything else. Nothing is ever ordered.
The payment step is where it stops, in every market, on every night. That is the line between a study of whether a store can be bought from and a study of whether its homepage answers.
  • One night each. A store that was broken only on the night we visited looks broken; one that broke the next day looks fine. This is a cross-section, not a monitoring record.
  • We never complete a purchase, so a purchase event cannot be verified and no finding here claims it was.
  • A bot-challenged store is not counted as broken. Being walled is a fact about automated browsers, not about a shopper, so those walks are excluded rather than scored.
  • Shopify only. Nothing here says anything about WooCommerce, BigCommerce or a custom build.
  • Aggregates only. No store is named, no URL is published and no screenshot of a third-party store appears anywhere in this report. That is the condition of visiting other people’s stores at all.

The findings

Six, each with the number it is a share of and the query that produced it. If you think a number is wrong, the query is there to argue with.

Finding 1

One store in four had a broken or degraded buying path on the night it was visited

37.7%n = 1,483 visits

A P1 or P2 finding means a step of home → collection → product → add-to-cart → cart → checkout failed, or something on it was broken. Not a slow page: a broken one.

US44.9% of 815AU20.8% of 361EU36% of 175UK32.8% of 67CA50.8% of 65
The query this number came from
select coalesce(t.market, case s.region when 'syd' then 'AU' when 'iad' then 'US' when 'lax' then 'US' when 'yyz' then 'CA' when 'lhr' then 'UK' when 'fra' then 'EU' else 'other' end) as market, count(*)::int as n,
    count(*) filter (where exists (select 1 from findings f where f.scan_id = s.id and f.severity in ('P1','P2')))::int as broken
  from scans s left join corpus_targets t on t.domain = s.domain
  where s.status = 'complete' and s.score is not null and not (coalesce(s.domain, '') ~* '(^|\.)(navaal\.ai|fly\.dev|localhost)$' or coalesce(s.domain, '') ~* '^(www\.)?(fernwick|.*bilby-testsite)') and s.ip = 'corpus' group by 1 having count(*) >= 50 order by 2 desc

Finding 2

The median mobile homepage took longer to show its main content than Google's threshold allows

3.2 sn = 1,183 visits with a mobile homepage measurement

Google's good threshold for largest-contentful-paint is 2.5 s and its poor threshold is 4 s. This is a real phone viewport in a real browser, not a simulation.

median (p50)3.20 s75th percentile5.13 sGoogle's good line2.50 sGoogle's poor line4.00 s
The query this number came from
select
    count(*)::int as n,
    percentile_cont(0.5) within group (order by lcp)::int as p50,
    percentile_cont(0.75) within group (order by lcp)::int as p75,
    count(*) filter (where lcp > 2500)::int as over_2500,
    count(*) filter (where lcp > 4000)::int as over_4000
  from (select (select (p->>'lcp')::numeric from jsonb_array_elements(coalesce(s.summary->'speed'->'pages','[]'::jsonb)) p where p->>'viewport' = 'mobile' and p->>'page' = 'home' and (p->>'lcp') is not null limit 1) as lcp from scans s where s.status = 'complete' and s.score is not null and not (coalesce(s.domain, '') ~* '(^|\.)(navaal\.ai|fly\.dev|localhost)$' or coalesce(s.domain, '') ~* '^(www\.)?(fernwick|.*bilby-testsite)') and s.ip = 'corpus') x where lcp is not null and lcp > 0

Finding 3

Purchase-path tracking was missing or broken on more than one store in ten

11.8%n = 1,483 visits

A Money finding means an event that should have fired on add-to-cart or at the start of checkout did not reach the pixel. Purchase itself cannot be verified without placing an order, and we never place one.

any tracking finding11.8% of 1,483add-to-cart not recorded10.7% of 1,483checkout start not recorded5.6% of 1,483
The query this number came from
select
    count(distinct s.id)::int as visits,
    count(distinct s.id) filter (where exists (select 1 from findings f where f.scan_id = s.id and f.lens = 'money'))::int as any_money,
    count(distinct s.id) filter (where exists (select 1 from findings f where f.scan_id = s.id and f.lens = 'money' and lower(f.title) like '%add-to-cart%'))::int as atc,
    count(distinct s.id) filter (where exists (select 1 from findings f where f.scan_id = s.id and f.lens = 'money' and lower(f.title) like '%checkout%'))::int as checkout
  from scans s where s.status = 'complete' and s.score is not null and not (coalesce(s.domain, '') ~* '(^|\.)(navaal\.ai|fly\.dev|localhost)$' or coalesce(s.domain, '') ~* '^(www\.)?(fernwick|.*bilby-testsite)') and s.ip = 'corpus'

Finding 4

The same eight faults account for most of what we found

6.5%n = 1,483 visits

The most common fault is an analytics event that never fires — invisible on the storefront, and it quietly misprices every ad campaign the store runs.

meta pixel didn't record the add-to-cart (P2)6.5% of 1,483google analytics 4 didn't record the add-to-…5.8% of 1,483google analytics 4 didn't record the start o…3.9% of 1,483site search returns nothing for a product yo…3.2% of 1,483meta pixel didn't record the start of checko…2.2% of 1,483your collection page shows an error instead …1.6% of 1,483a product page shows an error instead of the…1.5% of 1,483the mobile homepage scrolls sideways (P3)1.2% of 1,483
The query this number came from
select lower(f.title) as title, f.lens, f.severity, count(distinct f.scan_id)::int as scans
  from findings f join scans s on s.id = f.scan_id
  where s.status = 'complete' and s.score is not null and not (coalesce(s.domain, '') ~* '(^|\.)(navaal\.ai|fly\.dev|localhost)$' or coalesce(s.domain, '') ~* '^(www\.)?(fernwick|.*bilby-testsite)') and s.ip = 'corpus' group by 1,2,3 order by 4 desc limit 8

Finding 5

Most stores score well; the tail is where the money is

83 / 100n = 1,483 visits

The score is P1 35 · P2 20 · P3 10 · P4 5 off a hundred. A store under 50 generally has a failed buy-path step and a tracking fault at once.

90–10054.8% of 1,48370–8927.4% of 1,48350–6912.9% of 1,483under 504.9% of 1,483
The query this number came from
select
    count(*) filter (where s.score >= 90)::int as b90,
    count(*) filter (where s.score >= 70 and s.score < 90)::int as b70,
    count(*) filter (where s.score >= 50 and s.score < 70)::int as b50,
    count(*) filter (where s.score < 50)::int as b0,
    count(*)::int as n
  from scans s where s.status = 'complete' and s.score is not null and not (coalesce(s.domain, '') ~* '(^|\.)(navaal\.ai|fly\.dev|localhost)$' or coalesce(s.domain, '') ~* '^(www\.)?(fernwick|.*bilby-testsite)') and s.ip = 'corpus'

Finding 6

Severity, as the product itself grades it

562 visitsn = 1,483 visits

P1 stops a sale, P2 damages one, P3 and P4 are worth fixing and will not cost you tonight. Only P1 and P2 count towards the headline.

P10.6% of 1,483P237.3% of 1,483P341.2% of 1,483P42.6% of 1,483
The query this number came from
select f.severity, count(distinct f.scan_id)::int as scans
  from findings f join scans s on s.id = f.scan_id
  where s.status = 'complete' and s.score is not null and not (coalesce(s.domain, '') ~* '(^|\.)(navaal\.ai|fly\.dev|localhost)$' or coalesce(s.domain, '') ~* '^(www\.)?(fernwick|.*bilby-testsite)') and s.ip = 'corpus' group by 1 order by 1

A second study · September 2026

Product data on 409 live Shopify storefronts

A different method on a different sample, which is why it is a section rather than a row above: a public-web audit of product data on 409 storefronts, with no browser walk and no logins. Published 14 September 2026, updated 24 September 2026. 71.9% had at least one actionable product-data finding — and that is the wrong number to plan around.

How you define “actionable finding”Share of storesOf
All nine checks71.9%294/409
Excluding missing product_type and thin descriptions36.2%148/409
Only attributes AI shopping feeds require, plus policy pages22.7%93/409
Only structured data (missing or duplicated)15.9%65/409
Only crawler access0.5%2/409
  • Median findings per store: 1 (mean 1.28, maximum 6).
  • Stores with zero findings: 28.1%.
  • 35.7% had no finding except a missing product_type and/or a short description.

The sensitivity table is the point: the headline is carried by two content-quality checks, and the figure that matters for AI shopping feeds is 22.7%. The full study has the nine checks, their denominators and the method.

Cite this

Published under CC BY 4.0: use the figures, with attribution. The aggregates are downloadable as CSV so you do not have to retype them.

Navaal AI. The State of Broken Online Stores. 1 September 2026–2 September 2026. https://navaal.ai/bilby/data

Frozen editions

This page recomputes on every request, which is right for a live report and wrong for a citation. Each edition below is a permanent URL holding exactly what the study said when it was taken — cite one of those and the figure will still read the same next year.

By market

MARKETSTORES VISITEDBUY PATH BROKEN (P1/P2)MEDIAN MOBILE HOME LCPMOBILE HOME LCP > 4 SPURCHASE-PATH TRACKING FINDINGSAVG SCORE
US81545% · n=8153.1 s · n=65631%14%80
AU36121% · n=3613.9 s · n=26033%11%92
EU17536% · n=1753.1 s · n=15233%2%82
UK6733% · n=672.0 s · n=6312%10%86
CA6551% · n=653.9 s · n=5240%14%79
STORES VISITED
stores whose visit completed, by the store’s market; a store that refused the browser or errored on our side is not counted as visited anywhere on this page.
BUY PATH BROKEN
share of visited stores with at least one P1 or P2 finding — a step of home → collection → product → add-to-cart → cart → checkout that failed or a broken feature on it — on the night visited, visits to 2 September 2026.
MEDIAN MOBILE LCP
the median, across stores, of the home page’s Largest Contentful Paint at 390×844 in a real Chromium from the store’s market region (PerformanceObserver, one load per store); n is the stores with a mobile measurement.
LCP > 4 S
share of those stores whose mobile home LCP exceeded 4 s on mobile (Google’s “poor” threshold).
TRACKING FINDINGS
share of stores where an add-to-cart or begin-checkout event failed to reach an installed analytics platform’s own collection endpoint during the visit (Money check).
AVG SCORE
mean Bilby health score, 100 minus the severity points of every finding, floored at zero.

Every cell prints from the visits themselves; a market's row is only meaningful once it passes 50 stores. The 26 August 2026 checkout-scripts sunset is the reason the tracking column exists: a store whose add-to-cart or begin-checkout event no longer reaches GA4 / Meta shows up here as a Money check finding on a single night’s visit (purchase itself needs a real order — we never place one).

Methodology

  1. Sample. Public Shopify storefronts with at least one product page in the latest Common Crawl monthly index (*.myshopify.com/products/*), de-duplicated by host, password-protected and development stores skipped, the store’s active currency read once to place it in a market. Majority US by construction of the web; every cut prints its n.
  2. Visit. The same free scan every merchant can run: a real Chromium browser, desktop 1440×900 and mobile 390×844, from the nearest of six regions, home → collection → product → add-to-cart → cart → checkout, stopping at the payment step. robots.txt is respected; a store that refuses is not visited and not counted.
  3. Findings. Deterministic checks first (HTTP, cart endpoint, tracking requests, Web Vitals); a frozen Claude judge only where a human eye is needed, confirmed by a second signal before it becomes P1. Rate limits (429/403) are never findings. Rules: /bilby/methodology.
  4. Cuts. Market = the store’s active currency, read once when the sample was built (USD → US, GBP → UK, CAD → CA, AUD → AU, EUR → EU); the visit itself ran from the nearest of six regions. Speed rows count a mobile homepage LCP above 4 s, measured on our machine class and discounted when our own browser was starved.
  5. Limits, printed. One night per store; no purchase events (no orders placed); stores that wall automated browsers are absent; the sample is what Common Crawl saw, not the whole platform. Bilby scores its own store publicly under the same rules, and a separate study checked the product data on 409 live Shopify stores.

Press kit

  • Cite as: The State of Broken Online Stores, Navaal (Bilby), 2026, https://navaal.ai/bilby/data — CC BY 4.0.
  • Live figures update nightly; the table above is the source of truth, and the measurement line under the headline dates every figure. Quote the numbers with that date.
  • Marks: bilby mark · navaal wordmark (BRAND.md rules). Contact: support@navaal.ai — Waqas Ahmad, Navaal, Sydney.
  • The demo store used in every screenshot and the film is our own labelled store, Fernwick & Co.; no merchant is named without consent.

Not sure if your store is okay right now? Bilby walks it like a customer and shows you.

Scan my store free →Pricing →