A frozen User-Agent string can lie about your browser version. Here's how feature-detection probes bracket your real engine milestone instead.
Your User-Agent string says Chrome/143.0.0.0. Is that true? Two separate things make it unverifiable from the string alone. First, User-Agent Reduction froze everything after the major version to a hardcoded .0.0.0, so the one piece of version detail that used to move with every update is simply gone. Second — and this is the part that matters — the whole string is something the browser says about itself, and an extension or an automation framework can rewrite it in one line. A site that actually needs to know which engine milestone your browser is running has to ask a different question entirely: not "what does the browser claim," but "which features does it actually have." That's feature-detection version inference, and it's the same technique behind BrowserInsight's own kernel check.
Key Takeaways
- Feature-detection version inference tests for the presence of a capability that shipped in one engine milestone and the absence of one that shipped in the next, bracketing the real engine to a narrow range.
- The result is always a range, not an exact patch number — features ship in batches, some are flag-gated or backported, so the technique can pin down a milestone band but never the four-part build string
chrome://versionshows. - Every such detector has a built-in ceiling: it only knows about features that existed when its test table was written, so it reports "at least this milestone" once your browser passes every test it has.
- The two readings disagree asymmetrically. A detected version below what the UA claims is usually just a stale test table; a detected version above the claim is the direction that actually indicates spoofing, because a browser can hide a version but can't demonstrate features it doesn't have.
- The same probes separate Blink, Gecko, and WebKit from each other before version ever comes into it, because their feature sets diverge independently of any version number.
The mechanism: bracketing a version with feature probes
Every browser engine release ships a set of new, observable web-platform capabilities — a CSS property, a JavaScript method, a DOM interface. Most of these land in a way a page can test for directly, with no permission prompt and no user interaction: check whether a property exists, whether a constructor is defined, whether a CSS value is accepted.
A minimal existence probe looks like this:
// `Intl.Locale.prototype.variants` shipped in Chromium 149.
const has149 = 'variants' in Intl.Locale.prototype;
// The CSS `text-fit` property shipped in Chromium 150.
const has150 = CSS.supports('text-fit', 'initial');
// has149 && !has150 -> this engine is Chromium 149.
Neither line downloads anything or touches the network — each just asks the engine a yes/no question about its own capabilities. The trick is choosing the probes in pairs: one feature known to have shipped in milestone N, and another known to still be missing in N but present by N+1. If the first passes and the second fails, the browser is bracketed to "N, and not yet N+1." Run enough of these pairs across a table of known release milestones and you can walk up the ladder until you find the highest milestone the browser fully satisfies — that's your lower-bound estimate of the real engine version, entirely independent of anything the UA string claims.
This is exactly the approach behind BrowserInsight's own kernel check: it walks one table of Chromium milestones (starting at Chromium 79) and a separate one for Firefox, testing each milestone's known feature deltas, and reports the version implied by where the "pass" streak stops — alongside the version the User-Agent claims, so you can see directly whether the two agree. The two probes above are lifted straight out of that table.
Why the answer is a range, not a number
A feature-detection matrix can never resolve an exact build the way chrome://version does — it can only bracket a milestone, and even that comes with caveats worth being upfront about:
- Features ship in batches, not one per release, so a single passing test rarely isolates a single milestone on its own — it takes several confirming and disconfirming tests together to narrow the range.
- Some features are flag-gated behind experimental flags for one or more releases before shipping to everyone by default, so a small slice of users on a given milestone may fail a test that "should" pass for their version.
- Backports happen: a security-relevant feature occasionally lands in an older stable channel outside its normal release cadence, which can make an older build look newer on that one test.
None of that breaks the technique — it just means the honest output is "this browser is at least milestone N," not "this browser is exactly version N.N.N.N." Any tool, this site's kernel check included, that presents feature detection as resolving an exact patch version is overstating what the method can actually do.
Why every detector has a ceiling
A feature-detection table is a snapshot: it can only test for capabilities that existed and were known at the time the table was written. Once a browser passes every test in the table — because it's genuinely newer than the newest milestone the table covers — there's nothing left to fail it, and the honest report becomes "this milestone or newer," not a precise number. That's not a bug specific to any one implementation; it's a structural limit of the method itself, and it applies equally to BrowserInsight's own kernel check. The table needs periodic updates as engines ship new milestones and new distinguishing features become available to test — Chrome's own feature roadmap and its shipped-features list are the primary public record of what landed and when, which is exactly the kind of source such a table has to track over time. A useful check on any single feature's support timeline across engines is caniuse.com, which is why maintainers cross-reference it rather than relying on one browser vendor's changelog alone.
Reading a mismatch in the right direction
Once you have two readings, the obvious move is to compare them. But the two possible mismatches are not equally interesting, and treating them as symmetric is how a version checker starts flagging ordinary people as spoofers.
- Detected version lower than the UA claims is usually not a lie at all. It's the ceiling from the previous section showing up in practice: the browser is newer than the table, passes every test in it, and gets reported at the table's top rung. BrowserInsight's kernel check deliberately does not return a spoofing verdict for this case — on any table that isn't updated the same week a milestone ships, it would fire for every fully up-to-date visitor.
- Detected version higher than the UA claims is the genuinely suspicious direction. A browser can decline to advertise its version, but it cannot demonstrate capabilities it doesn't have. Features that only exist in a later milestone, running behind a UA that claims an earlier one, mean the string was rewritten while the engine underneath kept telling the truth.
There's a third pattern that doesn't involve the UA at all: whether the pass/fail results form a clean staircase. A genuine engine passes every milestone up to its own and fails everything above it, with no gaps in between. Passing a higher milestone while failing a lower one isn't a version — it's a shape no real build produces, and it points at a patched or emulated environment rather than an outdated one. Because that check compares the probes only against each other, it still works when the UA string is missing, generic, or nonsense.
Cross-engine: the same technique, before version even comes up
Feature detection doesn't just bracket a version number — it's also how a page tells engines apart in the first place, before version ever enters the picture. Blink, Gecko, and WebKit have been diverging in which experimental and newly-standardized features they ship, and in what order, since long before any of them reached their current milestone. A feature that exists in Blink but not Gecko separates the two engines regardless of which specific Chromium or Firefox release is running. For the deeper background on why the three engines diverge the way they do, see Browser Engines Explained; for the distinction between the rendering engine this technique probes and the separate JavaScript engine bundled alongside it, see Rendering Engine vs. JavaScript Engine.
It's also worth being precise about what gets detected. Chrome, Edge, Brave, Opera, and every other Chromium-based browser share the same underlying Blink engine, so a feature-detection probe reports the shared engine version — "Chromium 143" — not the branded product wrapped around it. Chromium vs. Chrome covers exactly why that distinction exists and why it isn't a bug, and Why Browsers Use Chromium covers why so many unrelated browser vendors ended up sharing that one engine at all.
Why this got more useful, not less
Feature detection isn't a workaround that stopped mattering once better version signals came along — it got more valuable, not less, as the alternatives changed:
- The User-Agent string used to move with real updates. Before User-Agent Reduction, a browser's UA reported something close to its true patch version, so inferring a version from features was mostly a curiosity. Now the UA's version digits are frozen at
.0.0.0by design, so it no longer moves at all between patch releases — it's supposed to look static, and it does, for every up-to-date install. Feature detection is the one version signal left that isn't zeroed out. - User-Agent Client Hints, specifically the
Sec-CH-UA-Full-Version-Listheader and the matchingnavigator.userAgentDataAPI, restored a precise version number — but it's a third reading of the same underlying claim the browser makes about itself, gated behind an opt-in request rather than broadcast by default. It's a genuinely useful, higher-detail channel, and it's also just as spoofable by the same tooling that would spoof a UA string, because both come from the browser self-reporting. - A disagreement between readings is now a real signal. With three independent-ish sources — the frozen UA, the client-hints claim, and the feature-detected engine version — a browser that claims one thing on two self-reported channels while behaving like a different engine milestone entirely is exhibiting exactly the kind of contradiction covered in detecting User-Agent spoofing. Feature detection is the one reading in that trio that the browser doesn't get to simply declare.
See it on your own browser
BrowserInsight's kernel check runs this exact probe against your browser right now: it shows the feature-detected engine version next to the version your User-Agent claims, a verdict on whether they agree, and the full milestone-by-milestone pass/fail list so you can see where your staircase stops.
If you want to see the technique with your own eyes first, open DevTools and run CSS.supports('text-fit', 'initial'). A true means you're on Chromium 150 or newer; a false means you're either below it or on a different engine entirely — either way, that's one rung of the ladder read directly off the engine, with no User-Agent involved.
Frequently Asked Questions
Can feature detection reveal my exact Chrome build number?
No. It can bracket your engine to a milestone — "at least Chromium 149, but not yet 150" — while the trailing build and patch digits that chrome://version shows stay out of reach. Features ship per milestone, not per patch release, so patch-level precision isn't something the technique can offer even in principle.
Does this work the same way for Firefox and Safari?
The bracketing logic is engine-agnostic, but the feature tables are engine-specific and have to be built and maintained separately for each one. BrowserInsight's kernel check runs a Chromium (Blink) matrix and a Firefox (Gecko) matrix; WebKit's version numbering is noisier and doesn't currently have an equivalent matrix on this site, though the same engine-identification step still applies to it.
If a browser fails every test in the table, does that mean it's an old browser?
Not necessarily — it could also mean the browser is genuinely newer than the table's newest known milestone, so nothing in the table distinguishes it from that ceiling. Passing every available test and failing every available test look identical to an outdated table; only the specific pattern of passes and failures (versus a blanket pass or blanket fail) tells you which side of the table's ceiling you're actually on.
Why not just trust the User-Agent Client Hints version instead?
Client Hints give you a precise version number, but it's still the browser stating a fact about itself through a different channel — not independently verified behavior. Feature detection is comparatively harder to spoof convincingly, because faking it means the browser would have to actually implement (or convincingly emulate) every feature in the table up to the claimed milestone, not just return a different string.
Conclusion
A feature-detection probe answers a question the frozen User-Agent string can no longer answer honestly: which engine milestone is this browser actually running, based on what it can do rather than what it claims. The answer comes back as a range bounded by a ceiling, not an exact build number — and that's an honest limitation of the method, not a flaw specific to any one implementation. What makes it valuable isn't precision; it's independence. It's the one signal in the mix that the browser doesn't get to simply declare, which is exactly why it's worth checking against everything else the browser tells you about itself.
Recommended Reading:


