---
title: "How to Reliably Detect an Ad Blocker"
canonical_url: https://ot.webclat.com/qa/detect-ad-blocker-reliably
description: "Ad blockers work by matching filter-list patterns, not by disabling a feature - here's the bait-element and bait-request method for detecting one, with timing pitfalls and verification steps."
source: Webclat | OneTrust Solutions (OneTrust partner, independent consultancy)
---

# What's a reliable, current method to detect that a visitor is running an ad blocker?

**Short answer:** Ad blockers work by pattern-matching request URLs and DOM elements against filter lists like EasyList, so the reliable detection method is a "bait" element or request built from the same patterns those lists target, checked after a short delay to see whether it was hidden or blocked - not user-agent sniffing or feature detection, since blockers don't touch either. No single check is 100% reliable across every blocker and browser combination, so combine a cosmetic-filter bait with a network-request bait rather than relying on one.

## Why this happens

Ad blockers use two independent mechanisms, and a detection script has to account for both. Cosmetic filtering hides DOM elements whose class, id, or attributes match a crowd-maintained selector list (EasyList and similar), purely by string pattern - the element is still in the DOM, just given `display:none` by an injected stylesheet rule. Network filtering blocks the request itself before it leaves the browser, based on the URL matching a known ad/tracker pattern - the request never gets a response at all.

This is why testing right after inserting a bait element often gives a false negative: most extensions apply their cosmetic rules on a short delay (a mutation observer batch, or a scheduled pass), not synchronously the instant the element is added to the DOM. A check that runs in the same tick as the insertion will see the element as still visible even when a blocker is active and about to hide it.

It's also why no method reaches 100% reliability. Filter lists differ by blocker, by list subscription, and by how aggressively the user has configured it, so a bait pattern that one blocker's default list flags might not be on another's. Treat detection as a probabilistic signal you can measure and react to, not a certainty you can branch application logic on.

## Fix it

1. **Build a cosmetic-filter bait element** - Create an off-screen element with class names that commonly appear in filter-list selectors (ad, ads, ad-banner, adsbox), append it to the DOM, then check its computed visibility after a delay - not immediately.

   ```
const bait = document.createElement("div");
bait.className = "ad-banner ads adsbox ad-placement";
bait.style.cssText = "height:1px;width:1px;position:absolute;left:-9999px;top:-9999px;";
document.body.appendChild(bait);

setTimeout(() => {
  const blockedByCosmeticFilter =
    bait.offsetParent === null ||
    bait.offsetHeight === 0 ||
    getComputedStyle(bait).display === "none";
  document.body.removeChild(bait);
  // blockedByCosmeticFilter === true means an element-hiding rule fired
}, 300);
   ```

2. **Build a network-request bait** - Request a resource whose path matches a known ad-network pattern (or a filename like ads.js) and treat a caught failure or a zero-length response as a network-level block, distinct from a real 404 or CORS error.

   ```
fetch("https://pagead2.googlesyndication.com/pagead/id", { mode: "no-cors" })
  .then(() => { /* request reached the network - not blocked here */ })
  .catch(() => { /* request never left the browser - network filter fired */ });
   ```

3. **Combine both signals before treating the visitor as blocked** - Require the cosmetic check and the network check to agree, or accept either alone only if you understand the false-positive/false-negative tradeoff each carries on its own - a single method flags fewer true blockers but with more confidence, two methods catch more but can disagree.

4. **Always wait for the blocker to act before reading state** - Give any bait at least 100-300ms before checking it. A same-tick check after DOM insertion will misreport blockers that apply cosmetic rules on a batched pass rather than synchronously.

5. **Decide what the detection result changes, deliberately** - Detection alone is neutral - what you do with a positive result (show a message, log the block rate, adjust something) is a separate design decision. Don't let the detection code silently start altering page behavior as a side effect.

## How to verify it worked

- Test with a real blocker installed (uBlock Origin and AdBlock Plus behave differently) and confirm the boolean flips to true; then disable the blocker (or use a clean profile) and confirm it flips to false on the same page.
- Open DevTools > Elements while the blocker is active and inspect the bait element's computed style panel - a passing result shows an injected `display: none !important` rule attributed to the extension, not your own stylesheet.
- Open DevTools > Network while the blocker is active and confirm the bait request shows a blocked status (Chrome labels it `(blocked:other)` or similar) rather than a normal response code.

## Related

- [How do I gracefully handle (or measure the impact of) ERR_BLOCKED_BY_CLIENT on my tracking requests?](https://ot.webclat.com/qa/err-blocked-by-client-tracking-requests)
- [Why does an ad blocker hide a legitimate page element just because its class/id contains "ad" or a tracking-like pattern?](https://ot.webclat.com/qa/ad-blocker-hiding-elements-named-ad)
- [If a privacy extension only partially blocks our tracking, should we detect that and route around it, or respect the intent?](https://ot.webclat.com/qa/detect-and-respond-to-partial-tracking-blocks)
- [Why do Snowplow tracker requests get silently blocked in the browser, and how do I detect it?](https://ot.webclat.com/qa/snowplow-requests-blocked-by-browser)

Contact: ot@webclat.com | (813) 694-4451 | https://ot.webclat.com/#contact
