A consent banner that displays correctly but doesn't stop tracking scripts from running isn't a compliance tool — it's a decoration. "Prior blocking" (sometimes called consent-first blocking) is the requirement that no non-essential cookie, pixel, or script executes until the visitor has made a choice. It sounds simple. In practice it's the single hardest part of consent management to get right, and it's the part most implementations get wrong, because the failure is invisible from the front end. The banner looks fine. Underneath it, Google Analytics already set _ga, the ad pixel already fired, and the "Reject All" button the visitor is about to click does nothing to undo either.
The legal basis for this is narrower than most teams assume. Article 5(3) of the ePrivacy Directive requires consent before storing or accessing information on a user's device — not consent logged concurrently with the first tracking request, and not consent obtained retroactively once the visitor scrolls past the banner. The GDPR supplies the definition of valid consent (freely given, specific, informed, unambiguous) that this storage-and-access consent has to meet. Guidance from the CNIL and the EDPB is explicit that deposit-before-consent is a violation independent of whether the visitor later accepts — the harm, in their framing, is the access itself, not the absence of a later "yes." The ICO takes the same position for the UK. If you haven't already, the GDPR cookie compliance checklist covers the surrounding requirements (banner design, storage, withdrawal); this article is about the one item on that checklist — "block all non-essential scripts until consent is given" — that's genuinely hard to implement correctly.
1. Why This Is the Most Common Compliance Failure
Most consent banners are added by dropping a snippet into the <head> and leaving every existing tag exactly where it was. The banner renders, collects a choice, and logs it — but the Google Tag Manager container, the Meta Pixel, the A/B testing script, all continue loading in parallel because nothing told the browser not to execute them. Regulators specifically call this out because it's the most common gap between what a site's privacy policy claims and what the browser actually does. DPA guidance across the EU converges on the same requirement: non-essential storage and the network requests that accompany it must not happen until after an affirmative, informed choice, and a "Reject All" click must actually stop anything that fired before it.
The reason this failure is so persistent isn't ignorance — it's that "make sure nothing fires" is an infrastructure problem disguised as a UI problem. Adding a banner is a day of frontend work. Auditing and gating every script on a site, including the ones added later by a different team, an agency, or a plugin, is ongoing engineering work with no natural stopping point.
2. Blocking vs. Merely Not Firing a Tag
These are different guarantees, and conflating them is where most implementations quietly fail.
Not firing a tag means a developer wrapped a specific tracking call in a consent check: if (hasMarketingConsent) { fbq('track', 'Purchase') }. It works, for that one call, as long as every developer who ever touches that codebase remembers to do the same thing for every new tag they add. It has no enforcement outside the discipline of whoever writes the code next.
True blocking operates at a layer below individual tags — intercepting script insertion, DOM mutation, or network calls generically, so that a script the gating logic has never seen still gets stopped by default. The distinction matters because the tags that cause compliance incidents are rarely the ones a developer deliberately wired up. They're the ones a marketing team added through a tag manager six months later, or the ones a "necessary" vendor's own script pulls in without anyone reviewing it.
3. Mechanism: Script Tag Rewriting / type Attribute
The most common client-side technique: author tracking <script> tags with a non-executable type, such as type="text/plain" or a custom data-cookieconsent="marketing" attribute, instead of type="text/javascript". Browsers won't execute a <script> tag whose type isn't a recognized JavaScript MIME type — it sits inert in the DOM. On consent, the gating code walks the DOM, finds matching nodes, and rewrites the type attribute (or clones the node, since some browsers won't re-execute a mutated tag in place), which triggers execution.
Pros: works without a build step, requires no JavaScript API access beyond DOM manipulation, and is easy to reason about — you can see exactly which tags are gated by reading the HTML.
Cons: it only governs <script> tags that were present in the parsed HTML at page load and were correctly authored with the placeholder type. It does nothing for <img> tracking pixels, document.write()-injected scripts, inline event handlers, or — critically — scripts that a vendor's own already-loaded script creates dynamically via document.createElement('script'). It also requires perfect tagging discipline: one script added directly to a template without the placeholder type bypasses the entire system.
4. Mechanism: Proxy / Wrapper Patterns
Instead of gating individual tags, intercept the browser APIs trackers actually use: wrap document.createElement, Node.prototype.appendChild/insertBefore, XMLHttpRequest, fetch, navigator.sendBeacon, or the vendor's own loader stub (the pattern most vendor SDKs already use internally — gtag() and fbq() are themselves queue stubs that buffer calls until the real script loads). Your wrapper queues or discards calls to blocked destinations and replays them once consent is granted.
Pros: works regardless of how a script is embedded — inline, external, or dynamically injected by something else — because it operates on the underlying browser primitive rather than a specific tag shape. It's the only client-side approach that can catch a third-party script injecting further scripts, since it intercepts the DOM insertion itself rather than the original tag.
Cons: ordering is everything. The wrapper has to install before any tracking script runs, which means it must be the first script on the page, synchronously, before anything else — a single async or deferred script race ahead of it defeats the whole mechanism. It also can't intercept everything: a vendor that opens a raw WebSocket, navigates an iframe directly, or uses sendBeacon through a code path your wrapper didn't patch will still get through. Maintaining the list of hooked APIs as vendors change their loading techniques is ongoing work.
5. GTM-Based Gating
Google Tag Manager has native consent support — tags can be marked with "additional consent checks" so GTM won't fire them until the relevant consent signal is granted. This is a legitimate and widely used control, but its scope is narrower than it looks: it only governs tags authored inside GTM. It does nothing for scripts sitting directly in your page's HTML outside the container, and it does nothing for a tag inside GTM that itself injects further tags via a Custom HTML tag or a nested container — a pattern common with ad exchange creatives and some analytics vendors that load a secondary script which loads a third. If GTM itself is allowed to load before consent (it usually is, since the container script needs to load to evaluate consent state at all), you're trusting every tag inside it to respect the flag correctly, including tags configured by non-engineers. See consent signals in server-side tagging for how consent state should propagate once it leaves the browser tag manager.
6. Server-Side Approaches
The strongest structural fix: don't let the browser talk to third-party vendors directly at all. Route tracking calls to a first-party endpoint — your own domain, or a server-side tagging container — and let that server decide whether and what to forward to each vendor based on the consent state carried in the request. See what is server-side tracking for the general architecture.
Pros: this closes the "vendor script injects more scripts" hole entirely, because the vendor's client-side JavaScript never has to load in the browser to begin with — there's nothing for it to inject. It also gives you one enforcement choke point instead of dozens of client-side interception rules to maintain.
Cons: it's a real engineering project, not a snippet change. Some vendors' matching and fraud-detection quality depends on client-side execution (device fingerprinting signals, first-party cookie writes at the browser level) that a server-side proxy can't fully replicate, so you're trading data quality for compliance certainty on those integrations. And server-side tagging shifts where enforcement happens, not whether you still need consent state correctly computed and passed through — get that wrong and you've just moved the same bug to a harder-to-debug layer.
7. Why Injected Tags Defeat Naive Blocking
This is the mechanism behind most "we blocked everything and a DPA still found tracking" incidents. A site gates its known tags with type="text/plain" rewriting — one <script src="agency-loader.js"> tag, gated correctly. The visitor consents, the type gets rewritten, the script executes. That loader script is not the tracker itself; it's a bootstrapper that calls document.createElement('script') to pull in two more vendor scripts, each of which does the same again — a pattern common with ad exchanges, tag resellers, and some analytics platforms that chain through several vendors before the actual pixel fires.
Your HTML-level rewriting only ever touched the one tag that existed when the page was parsed. Every script created dynamically after that point was never mangled in the first place — it loads and executes exactly as the vendor's code intended, consent gate or not. This is why type-attribute rewriting alone is insufficient for any site using third-party tag managers, ad tech, or analytics vendors with their own sub-loaders (which is most of them): it only covers the first hop. Closing this gap requires either the proxy/wrapper approach applied to DOM insertion itself (patching createElement/appendChild so every dynamically created script is caught, not just the ones parsed from HTML) or a restrictive Content-Security-Policy script-src that denies unlisted origins outright regardless of how the script was inserted. CookieBeam's own client-side blocking layer, for example, intercepts script, iframe, and image insertion at the DOM level rather than only rewriting static tags, specifically to catch resources injected by other scripts — worth checking whether any CMP you evaluate does the same before assuming tag-level gating is sufficient.
8. Consent Mode Signals Are Not Blocking
Google Consent Mode v2 is frequently mistaken for a blocking mechanism. It isn't one — it's a signal Google's own tags read to decide how to behave, not whether they load. The gtag.js script and the GA4/Ads tags it configures still execute regardless of consent state. In Basic Consent Mode, the tags don't fire measurement pings until a signal is granted. In Advanced Consent Mode, they fire cookieless "pings" to Google even while analytics_storage and ad_storage are denied, specifically so Google can model conversions statistically — meaning a network connection to Google's servers happens before the visitor has consented to anything, even though no cookie gets set.
That's a meaningfully different guarantee than prior blocking. Article 5(3) is about consent before storage and access to information on the device; whether Advanced Consent Mode's pre-consent network request also implicates that requirement independent of cookie storage is a genuinely contested question among practitioners and not one this article will resolve — but it's exactly why Consent Mode should be treated as a complementary signal layer for vendors who support it, not a substitute for actually blocking the script from loading in the first place. If your compliance argument rests entirely on "Consent Mode is denied by default," you haven't verified the tag stopped loading — you've verified it stopped storing cookies.
How to Actually Test That Nothing Fired Pre-Consent
- 1
Start from a genuinely clean state
DevTools → Application → Storage → Clear site data, or use a fresh private window. A leftover consent cookie from a previous test run will hide a real bug.
- 2
Open the Network tab and enable "Preserve log" before the page loads, then reload
Not after the banner appears. Requests fired in the first few hundred milliseconds are the ones most likely to be missed by gating logic that initializes late.
- 3
Don't touch the banner yet
Let the page sit. Any request to a known tracker domain (google-analytics.com, googletagmanager.com/gtag/js, connect.facebook.net, doubleclick.net, or whatever vendors are in your stack) at this point is a failure, full stop — not a warning.
- 4
Check Application → Cookies and Application → Local/Session Storage for non-essential keys set before any interaction
Storage access is what Article 5(3) actually governs, independent of whether a network request also happened.
- 5
Use the Network panel's request initiator chain (click a request → Initiator tab) to trace who triggered it
If the initiator is a vendor's own dynamically-created script rather than your page's HTML, that's the nested-injection failure mode from section 7 — tag-level gating missed it.
- 6
Click "Reject All" and repeat the whole check
This is where implementations leak most often: some vendor stubs queue a call during page load and fire it as soon as any script initializes them, consent decision notwithstanding.
- 7
Hard-block the tracker domains (DevTools → Network → Block request URL, or a local proxy) and confirm the site doesn't error out
If functionality breaks when a "blocked" tracker is unreachable, the script was never actually gated — it was just failing to report, which isn't the same thing.
- 8
Automate the check
A manual pass catches today's tag stack; it won't catch the tag someone adds through the tag manager next quarter. Wire a headless-browser crawl with network capture into CI, diff the pre-consent request list against an explicit allowlist, and fail the build on any new entry.
Prior blocking isn't a feature you add once. It's a property you have to keep re-verifying every time a new vendor script enters the page — which, on most marketing-driven sites, is often.