How it is measured, and what it misses.
A census is only worth the method. This one is small enough to describe in full, so here it is — including the parts that would make a figure wrong if you read it as more than it is.
Who is counted
One census of 1,224 products, counted together. The names came from two places, and that is worth knowing because it is the part you can check — but it is where a name came from, not two things reported separately.
51 were picked by hand: Plausible, Linear, Cal.com, Resend and the rest of the commercial tools an indie founder already reads about. That is a choice I made, and it is not a sample of anything. It is a named list, and it is on the population page in full.
The other 1,173 are open-source tools taken from two public lists, awesome-selfhosted and awesome-sysadmin. They were chosen because they are curated by other people, they are public, and every row here can be traced back to a line in somebody else’s repository. I did not assemble this list, which is the point: a list of a thousand entries that I typed out myself would be unauditable.
Entries pointing at GitHub, GitLab, an app store or Read the Docs are dropped before the survey runs — 348 of the 1,565 the two lists hold. Fingerprinting a code host tells you what the code host runs, and nothing about the product.
The population is fixed between surveys. It changes only when the source lists change and the list is rebuilt, and a product that joins is never reported as having adopted anything: the change feed only compares products that answered in both surveys.
What actually happens
Every morning at 06:00 UTC, each of the 1,224 domains gets one HTTPS GET of its home page, with a normal browser user-agent and redirects followed. From the response we keep the headers, the cookies, the HTML, the script tags, the meta tags and up to two of the stylesheets it links to. We also ask DNS for the domain’s MX records. That is the whole input: no login, no crawl, no second page.
Those inputs are matched against the fingerprint rules from enthec/webappanalyzer, which carries on the ruleset Wappalyzer closed in August 2023. The rules are GPL-3.0. They are fetched at the start of every run and never stored here, so this repository redistributes none of them — and the census always matches against current rules rather than a copy going stale in a data folder.
The four things it cannot see
Anything that needs a browser. Most of the public ruleset’s modern front-end detection is written as js and dom rules, which only exist after a page has executed. A serverless function has no browser, so those rules are skipped rather than half-run. That is why the supplement below exists.
Anything behind a login. This is the marketing page. The application a company actually ships may be built on something else entirely, and often is.
Anything the CDN hides. A site fronted by Cloudflare tells you about Cloudflare. What answers behind it is frequently invisible from the outside.
Anything about the company. No revenue, no headcount, no funding, no ranking. This census counts what a response contained. It does not make claims about the people behind it.
Reading the figures
A count is “how many of the sites that answered had this in the response”. It is not market share, and it is not a recommendation. Two detections are kept only when the rules put confidence at 50 or above, which is the ruleset’s own threshold for a weak signal.
Some rows are inferred rather than seen: the ruleset says Next.js implies React, so React is counted wherever Next.js was found. Those lines say implied in their evidence, and the census prefers a directly observed example when it has one.
TXT records are not read, though the rules would happily match them. The first production run found 689 things where the same code on a laptop had found 443, and 244 of the 246 extra were domain verification records — every openai-domain-verification and slack-domain-verification line a company has ever been asked to add. A verification record proves somebody once proved they owned the domain. It is not the page running anything, one of them resolved to “Apple iCloud Mail” on nineteen sites that do not use it, and different resolvers return different amounts of it — which would have filled the change feed with hundreds of changes that never happened. MX is read, because it is where the mail actually goes.
The green and red figures are how far a count moved since the previous survey. They are worked out only on the products that answered in BOTH surveys. Without that restriction a morning where forty sites time out would read as forty products dropping their framework overnight, and the arrows would be reporting the weather rather than the web.
Two signals are kept out of the change feed, and carry no movement figure either. HTTP/3 and HSTS flap on their own: a server advertises alt-svc inconsistently, and which edge answers decides whether HSTS appears. Both stay in the tally, where they are true. Neither belongs in a feed that is read as news.
The ruleset ships a few products under two spellings — Sanity and Sanity.io are the same thing, matching the same evidence. Those are merged before counting, or the tally would show one product twice.
The 51 rules this census adds
On the public ruleset alone, Next.js was found on 11 of the first 51 sites while /_next/static/ sat in the HTML of twice as many — the rest of its rules need a browser. So the census adds rules of its own. Every one matches a string that is present in the response, and every one is printed here, because a fingerprint you cannot read is one you cannot argue with.
17 of them matched nothing at all in this survey, which is itself a result: not one home page in the whole census carried a trace of Remix, Umami, Vercel Speed Insights, or of 14 other things looked for. They are marked below.
Next.js
- script src /_next/static/
- in the html <script[^>]+id="__NEXT_DATA__"
- in the html self\.__next_f
SvelteKit
- script src /_app/immutable/
- in the html __sveltekit_
Astro
- in the html <astro-island
- in the html /_astro/[^"']+\.(?:css|js)
Remixfound nothing
- in the html window\.__remixContext
Gatsby
- in the html id="___gatsby"
- in the html /page-data/(?:sq/)?
Nuxt.js
- script src /_nuxt/
Radix UI
- in the html data-radix-[a-z-]+
- in the html data-radix-scroll-area-viewport
shadcn/ui
- in the html data-slot="(?:button|card|badge|input|dialog|tabs)";confidence:50
Framer Sites
- in the html framerusercontent\.com
- <meta generator> Framer
Plausible
- script src plausible\.io/js/
- script src /js/plausible[^"']*\.js
Fathom
- script src cdn\.usefathom\.com/
Simple Analytics
- script src simpleanalyticscdn\.com
- script src sa\.simpleanalytics
Umamifound nothing
- in the html data-website-id="[0-9a-f-]{36}";confidence:50
PostHog
- script src (?:app|eu|us)(?:-assets)?\.(?:i\.)?posthog\.com
- script src posthog-js
- in the html posthog\.init\(
Vercel Analytics
- script src /_vercel/insights/script\.js
Vercel Speed Insightsfound nothing
- script src /_vercel/speed-insights/script\.js
Amplitude
- script src cdn\.amplitude\.com
- script src amplitude-js
Mixpanelfound nothing
- script src cdn\.mxpnl\.com
Statsigfound nothing
- script src cdn\.statsig\.com
Koalafound nothing
- script src cdn\.getkoala\.com
Common Room
- script src cdn\.cr-relay\.com
Segment
Intercom
- in the html widget\.intercom\.io
- in the html intercom-frame
Crisp Live Chatfound nothing
- script src client\.crisp\.chat
Plainfound nothing
- script src chat\.cdn-plain\.com
Front Chatfound nothing
- script src chat-assets\.frontapp\.com
Help Scout
- script src beacon-v2\.helpscout\.net
Stripe
- script src js\.stripe\.com
- in the html (?:checkout|billing)\.stripe\.com
Paddle
- script src cdn\.paddle\.com
- script src sandbox-cdn\.paddle\.com
Lemon Squeezy
- script src lemonsqueezy\.com/js
- script src assets\.lemonsqueezy\.com
Cal.com
- script src cal\.com/embed
Calendly
- script src assets\.calendly\.com
Typeformfound nothing
- script src embed\.typeform\.com
ConvertKitfound nothing
- script src f\.convertkit\.com
- script src ck\.page
- in the html app\.convertkit\.com/(?:landing_pages|forms)
Klaviyo
- script src static\.klaviyo\.com
Customer.iofound nothing
- script src assets\.customer\.io
Loopsfound nothing
- script src app\.loops\.so
Sentry
- script src browser\.sentry-cdn\.com
- in the html Sentry\.init\(
- in the html sentry-trace
LogRocketfound nothing
- script src cdn\.log(?:rocket|r)\.io
- script src cdn\.logrocket\.com
Highlight.iofound nothing
- script src highlight\.run
Cloudinary
- in the html res\.cloudinary\.com
Imgix
- in the html \.imgix\.net
Muxfound nothing
- in the html (?:stream|image)\.mux\.com
Vercel Blob
- in the html \.public\.blob\.vercel-storage\.com
Cloudflare R2
- in the html r2\.cloudflarestorage\.com
- in the html \.r2\.dev
Sanity
- in the html cdn\.sanity\.io
Contentful
- in the html images\.ctfassets\.net
Storyblokfound nothing
- in the html a\.storyblok\.com
Mintlify
- in the html mintcdn\.com
- in the html mintlify\.(?:com|s3)
Algolia
- script src @docsearch
- in the html algolia(?:net)?\.(?:com|net)
Cloudflare Turnstile
- script src challenges\.cloudflare\.com/turnstile
Corrections
If a line is wrong about your site, it is wrong in public and I would rather fix it: the evidence string on each row says exactly what was matched, so a correction takes one message. @antionmil.