← Back to Blog

How to Check If Your Visitors Are Using Ad Blockers

There are three common ways to detect ad blockers in the browser: bait elements, request interception, and script load checks. Each has real trade-offs. This post covers all three with working code, explains where each breaks down, and is honest about when the DIY approach stops being worth the e...

There are three common ways to detect ad blockers in the browser: bait elements, request interception, and script load checks. Each has real trade-offs. This post covers all three with working code, explains where each breaks down, and is honest about when the DIY approach stops being worth the effort.

Approach 1: Bait Element Detection

This is the most widely used method. You create a tiny invisible DOM element with class names that cosmetic filter rules target, then check after a short delay whether the element has been hidden or removed.

Ad blockers that apply CSS-based hiding rules will catch class names like .ad, .adsbox, .adsbygoogle, and .ad-banner. If your element has one of these classes and an ad blocker is active, the element will typically have offsetHeight === 0 or display: none after the filter rules fire.

```javascript function detectAdBlockBaitElement(callback) { const bait = document.createElement(‘div’); bait.className = ‘ad ads adsbox adsbygoogle ad-banner’; bait.style.cssText = ‘position:absolute;top:-999px;left:-999px;width:1px;height:1px;’; document.body.appendChild(bait);

setTimeout(() => { const blocked = bait.offsetHeight === 0 || bait.offsetParent === null || window.getComputedStyle(bait).display === ‘none’;

document.body.removeChild(bait);
callback(blocked);   }, 100); }

detectAdBlockBaitElement((hasAdBlock) => { console.log(‘Ad blocker detected:’, hasAdBlock); }); ```

The limitation is significant: this only catches blockers that use element-hiding rules. Blockers that work primarily by intercepting network requests (without applying cosmetic filters) will walk right past this check. Some strict blockers also disable JavaScript in ways that interfere with the timeout, producing false negatives. You can reduce them by extending the timeout to 200ms, but you won’t eliminate them.

Approach 2: Request-Based Detection

Instead of inspecting the DOM, you make a fetch() request to a URL that matches patterns ad blockers filter. If the request fails or is intercepted, the visitor has a blocker active.

The common misconception here is that you can just fetch a known blocked URL like https://doubleclick.net/something. You can’t. CORS will block cross-origin requests before the ad blocker even gets involved, so you’d get a false positive regardless.

You need to host a decoy endpoint on your own domain with a path that matches blocked patterns. Something like /ads/track.js or /analytics/ad-event will match broad EasyList and EasyPrivacy rules.

```javascript async function detectAdBlockByRequest(callback) { try { const response = await fetch(‘/ads/track.js’, { method: ‘GET’, cache: ‘no-store’, });

// If the request succeeds, no blocker caught it
callback(!response.ok);   } catch (error) {
// Fetch threw: the request was blocked
callback(true);   } }

detectAdBlockByRequest((hasAdBlock) => { console.log(‘Ad blocker detected:’, hasAdBlock); }); ```

Your server needs to handle GET /ads/track.js and return a 200 with any response body. The file contents don’t matter. What matters is that the path matches filter list rules.

This method catches request-blocking ad blockers that bait elements miss. The two approaches are complementary, not interchangeable. Running both in parallel improves your detection rate meaningfully. The downside: you need server-side infrastructure for the decoy endpoint, and you have to keep the endpoint path current as filter lists evolve.

Approach 3: Script Load Detection

The oldest approach. You include a <script> tag pointing to a file with a name ad blockers target. Inside that script, you set a global flag. After page load, you check whether the flag was set.

html <!-- In your HTML head --> <script>window.__adsLoaded = false;</script> <script src="/js/ads.js"></script>

javascript // /js/ads.js window.__adsLoaded = true;

javascript // After page load window.addEventListener('load', () => { if (!window.__adsLoaded) { console.log('Ad blocker detected: script was blocked'); } });

This works when an ad blocker intercepts the script request entirely. The problem is that it misses blockers using only cosmetic rules, gives you no information about timing or accuracy, and the detection gap between when the script should have loaded and when you check is inherently fuzzy.

It also gets outdated fast. If /js/ads.js stops matching active filter list rules because the maintainers updated their patterns, you’ll see false negatives without realising it. It’s the easiest method to implement and the least reliable in production.

Where DIY Detection Falls Short

Each method works in isolation, but each has a coverage gap. Bait elements miss request blockers. Request checks require your own decoy infrastructure. Script tags miss cosmetic-only filters. The only way to approach reliable coverage is to combine all three, which means more code, more maintenance, and more ways for the detection to drift out of sync with what ad blockers are actually doing.

The specific problems that compound over time:

  • Filter lists update daily. A path that matched EasyList six months ago may have been dropped, and a new pattern you haven’t covered may now be catching 20% of your blocked visitors.
  • Detection accuracy varies by browser. Safari’s Intelligent Tracking Prevention and Firefox’s Enhanced Tracking Protection add their own layers that interact with your detection in unpredictable ways.
  • Blocker version updates matter. uBlock Origin’s cosmetic filtering engine and its request-blocking engine behave differently across versions, and each major release shifts what gets caught.
  • DIY gives you a true/false flag per page load. It doesn’t give you historical trends, rates by traffic source, or breakdowns by page. To turn raw detection into useful data, you have to build that aggregation layer yourself.

If you’re detecting ad blockers to show a single polite message to visitors, a basic bait element check is probably enough. If you want to understand what percentage of your audience is blocking ads, how that’s trending over time, and whether a specific traffic source has unusually high blocker rates, you’re building analytics infrastructure, not just a detection script.

When a Purpose-Built Tool Is Worth It

Adblockmonitoring.com is a two-line snippet that handles detection using multiple methods simultaneously, including both bait elements and request-blocking checks, calibrated against current filter lists. Accuracy sits above 97% across blocker types and versions. The detection stays current automatically.

The dashboard shows blocker rates over time, broken down by page and traffic source. No cookies, cookieless daily fingerprints that auto-reset, GDPR compliant. You get the trends and historical data without building the aggregation layer.

If you want to run detection yourself, the code above gives you a working start. If you want accurate numbers without the maintenance overhead, the free trial is two lines of JavaScript and a dashboard in about five minutes.

The Short Version

Bait element detection catches cosmetic-filter blockers. Request-based detection catches network-blocking ones. Script load detection is the oldest method and the least reliable. Use at least the first two together if you want coverage above 70-80%. All three still require maintenance as filter lists change.

If accuracy and trends matter more than control over the implementation, a dedicated tool handles the detection, the calibration, and the analytics for you.