A page can't ask if you run an ad blocker - it checks the outcome: a hidden bait ad, a blocked request, or a missing ad-script global.
There is no browser API that answers "is an ad blocker installed?" A page can't query it the way it can query your timezone or your screen size. So sites that want to know don't ask — they set a small trap and watch what happens to it. If the trap survives, nothing is blocking. If it doesn't, something is.
Key Takeaways
- A page infers an ad blocker from the outcome, not a direct signal. The three common checks are a bait element that filter lists are trained to hide, a network request to an ad-like URL that fails, and a global variable an ad script was supposed to define but never got the chance to.
- The same checks work whether the blocker is a classic extension or a declarative rule engine. Script-based extensions and declarative engines like WebKit Content Blockers on iOS or Chrome's
declarativeNetRequestenforce the same filter-list patterns, so they leave the same observable trace on a page. - Detection returns one bit — "something interfered" — but the pattern of which bait elements got hit can narrow that further. Different filter lists target different class names and URL patterns, so which bait survives and which doesn't is itself a small fingerprinting signal.
- The request-based checks can misfire with no ad blocker installed. DNS-level filtering, a browser's built-in tracking protection, or a corporate proxy can all produce the identical "the ad didn't load" result — even though none of them can hide a bait element.
- This is about detection, not evasion. Nothing here is instructions for hiding a blocker from a site or defeating an anti-adblock wall — that's a different, adversarial project this post isn't attempting.
The Three Checks Behind Every Ad Blocker Detection
Strip away the variations and almost every ad-blocker check on the web reduces to one of three tests, run moments after the page loads.
A bait element. The page creates a <div> (or similar) with a class name and structure that looks exactly like a real ad slot — something like ad-banner, pub_300x250, or text-ad, sized and positioned the way a real ad container would be. It drops that element into the page and waits a beat. Ad blockers work primarily from filter lists (EasyList and its relatives) that match elements by exactly these kinds of selectors, so a genuine blocker hides or deletes the bait along with any real ads matching the same pattern. The page then checks whether the bait is still there: is its computed display set to none, is offsetHeight zero, was the node removed from the DOM entirely. Any of those means something intercepted it. BrowserInsight's own plugin and extension check runs exactly this test: it inserts an off-screen div carrying classes such as pub_300x250, text-ad and ad-banner, waits about 100 milliseconds, then reads its computed style and layout size.
Note that this is purely a cosmetic test. The bait is created locally by the page's own script, so no network request is involved — only something that applies element-hiding rules inside the page can make it disappear.
A blocked network request. Instead of watching an element, the page fires a request to a URL that looks like an ad or tracking call — a path containing /ads/, a hostname matching a known ad-serving domain, a script named like a known analytics tag. A blocker with network-filtering rules (not just cosmetic hiding) stops the request before it leaves the browser, or the browser reports the response as failed. The page's own script sees that failure — a rejected fetch, an image onerror firing instead of onload — and treats it as the signal.
A missing global. Real ad-serving scripts, once they load successfully, typically define something in global scope — a function, a config object, a flag the publisher's own code checks before proceeding. If the ad script itself was blocked from loading, that global never gets defined. The page checks for its existence a moment after the script tag would have executed; undefined means the script never ran.
All three share the same shape: define an expectation that only holds if nothing interfered, then check whether it held. None of them ask the browser a direct question, because there isn't one to ask.
Why It Works the Same Across Every Blocker Type
It would be reasonable to assume a browser extension and a built-in, no-extension blocker would look completely different to a detector. In practice they don't, and the reason is that they're implementing the same filter-list rules through different mechanisms.
A traditional ad-blocking extension injects content scripts and stylesheets that hide or remove matching elements, and it can intercept network requests directly. A declarative blocking engine works differently under the hood but converges on the same outcome: the browser is handed a compiled rule list ahead of time and enforces it itself, without the blocker's own code inspecting every page. WebKit's Content Blockers, introduced for Safari and used by every content-blocking app on iOS, work this way — an app supplies a JSON rule list, and WebKit applies it at the engine level to every page loaded afterward. Those rules can both block a resource and hide elements by CSS selector (the css-display-none action), so they cover the network checks and the bait check alike. Chrome's extension platform has moved in a similar direction: declarativeNetRequest lets an extension hand the browser a static or dynamic rule set instead of inspecting every request in JavaScript, and under Manifest V3 it is the model Chrome expects network-blocking extensions to use. (Element hiding in an MV3 blocker still happens through injected CSS, but the filter lists behind it are the same.)
The mechanism is different — a compiled rule list enforced by the engine versus a script watching the page — but the result a page can observe is the same either way: a bait element matching a filter-list pattern gets hidden or removed, and a request matching a filter-list pattern fails to load. A detector built around outcome, not mechanism, doesn't need to know or care which kind of blocker it's up against. That's also why a mobile ad blocker that ships as "just" a Safari content blocker, with no ability to run scripts or read page content, still trips the exact same bait-element check a desktop extension would.
What It Gives Away
On its own, a positive result from this kind of check is close to one bit: yes, something is filtering this page, or no, nothing is. That single bit is already useful to the site — it's the trigger for an anti-adblock wall or a "please whitelist us" banner — but it isn't much of a fingerprinting signal by itself, because so many visitors share the same answer.
What sharpens it is running more than one bait element at once, each styled to match a different filter list's conventions. A detector that drops five bait divs — one shaped like a generic EasyList pattern, one like a regional list's convention, one like a rule only a stricter annoyance list carries — and checks each independently doesn't just learn "blocked: yes." It learns which patterns got hit, and filter-list subscriptions vary enough between users that the specific combination narrows the crowd you blend into. That's the same entropy math behind any fingerprinting signal: a rare combination of results is more identifying than a common one, exactly as our guide to fingerprint entropy and anonymity sets lays out for signals in general. An ad-blocker check is a small, low-entropy example of the same idea — worth a few bits, not an identifier on its own.
It's worth being precise about what this technique is not. It isn't the same as detecting which specific extensions you have installed, which works by probing an extension's own exposed resource files or its distinctive side effects on the page — a much more targeted technique aimed at a named extension, not a generic "is anything filtering ads" outcome check. Ad-blocker detection doesn't need to know which product is responsible; it only needs to know that something interfered with its bait.
False Positives: When the Check Fires With Nothing Installed
Because the check watches an outcome rather than asking a direct question, anything else that produces the same outcome trips it too — and several ordinary, non-extension setups do exactly that. Which check they trip matters, though: anything that works on the network can make the blocked-request and missing-global checks fire, but it can't touch a bait element the page built locally.
- Network-level ad blocking. A Pi-hole on the home network, a filtering DNS resolver, or a router-level ad blocker stops ad requests before they ever reach an ad server — no extension installed on the device at all, yet the ad script fails to load and its global never appears, exactly as if an extension had blocked it.
- Browser-native tracking protection. Firefox's Enhanced Tracking Protection blocks known tracking content in private windows by default and in every window in Strict mode, with no add-on involved. If the "ad" a detector loads sits on a domain from the tracker lists it uses, that request fails too.
- Browsers with a built-in ad blocker. Brave Shields, and the built-in blockers in browsers such as Opera and Vivaldi, apply filter lists themselves. Depending on settings, that can include element-hiding rules — so these can trip even the bait-element check with nothing installed.
- A corporate or school proxy. Many managed networks route traffic through a filtering proxy that strips ad and tracking domains as a side effect of a broader content policy, unrelated to anything the user chose to install.
- DNS resolvers that uncloak disguised trackers. Some filtering DNS services go further and follow CNAME records to spot CNAME-cloaked trackers — a third-party tracker hidden behind a first-party-looking subdomain. That can block an ad or tracking call even when its URL looks harmlessly first-party.
None of these involve an installed extension, yet each can make a page conclude "ad blocker detected." A site that treats this signal as certain proof of an installed extension is making an inference the data doesn't actually support — it knows only that something, somewhere between the page and the ad server, interfered.
What Detection Does Not Tell a Site
It's worth stating the boundary plainly, because it's easy to overstate what a passing or failing bait check proves. A page running this test does not learn which extension you have, if any — that requires the separate, more targeted extension-probing techniques covered in our extension privacy guide. It does not learn your identity or browsing history. And from a failed request alone it cannot distinguish an extension from network-level filtering, a strict browser privacy setting, or a managed-network proxy, as the false-positive list above makes clear. What it learns, at most, is a small number of bits about which observable patterns got interfered with on this one page load.
This article is about how that inference is built and what it can and can't support — not a guide to defeating the check. Filter-list authors and anti-adblock vendors run an active back-and-forth that's out of scope here; nothing above is a recipe for hiding a blocker from a site or slipping past an anti-adblock wall.
See What Your Own Setup Reveals
BrowserInsight's plugin and extension check runs a bait-element test alongside its extension-detection checks, so you can see for yourself what a site sees when it runs this test against your browser. Because that test is cosmetic, a network-only blocker such as Pi-hole won't show up in it. If the result surprises you — blocked with no extension installed, or not blocked when you have one — check for a browser with a built-in blocker, or an extension whose element hiding is switched off or allowlisted for the site. For the bigger picture of how a check like this fits into a full browser fingerprint, the fingerprint check shows the rest of what a page can observe about your setup.
Frequently Asked Questions
Can a website tell exactly which ad blocker I'm using?
No, not from an outcome-based check like a bait element or a failed request. Those tests only establish that something interfered with ad-shaped content or requests — they can't identify the specific extension or blocking mechanism responsible. Naming a specific extension requires a different, more targeted technique aimed at that extension's own footprint.
Does this work the same way on mobile as on desktop?
Yes, for declarative blockers. A content-blocking app on iOS built on WebKit's Content Blocker API produces the same observable outcome — a hidden bait element, a failed ad request — as a desktop browser extension, even though it has no ability to run scripts on the page. The detector doesn't need to know which mechanism is responsible.
Can I have an ad blocker detected when I don't have one installed?
Yes. Network-level filtering (a home DNS blocker, a corporate proxy) and a browser's built-in tracking protection can make a blocked-request or missing-global check fire, and a browser with a built-in ad blocker such as Brave can trip the bait-element check too — all with no extension installed.
Is having an ad blocker detected a big privacy risk?
On its own, it's a small signal — closer to one bit than a fingerprint. It becomes more identifying only when a site runs several differently-styled bait elements at once and records which specific ones got caught, since that pattern varies more between users than a single yes/no answer does.

