Server-Side vs Client-Side Cloaking: A Technical Deep Dive
If you are an engineer or tech lead inheriting a "traffic differentiation" feature, you are probably staring at one of three things: an Nginx config branching on User-Agent, a JavaScript snippet swapping innerHTML after page load, or a Cloudflare Worker doing both. The team calls it cloaking. Ad platforms call it a policy violation. This article is a calm, engineering-level walk through what is actually happening on the wire and in the DOM, why modern detection is far ahead of these implementations, and what an auditable replacement looks like.
For the broader business and policy context, start with our pillar on website cloaking. This piece focuses on the systems-level details.
What "cloaking" actually means at the protocol level
In a non-cloaked site, the response a crawler receives is materially equivalent to the response a human receives: same HTML skeleton, same critical content, same canonical URL. In a cloaked site, the response diverges based on attributes of the requester, and the divergence is designed to deceive a reviewer rather than to serve a legitimate UX need.
The two classical implementation families are:
Server-side cloaking: the divergence happens before the HTML leaves the origin (or edge). The bot and the human get different bytes.
Client-side cloaking: the same HTML leaves the origin, but JavaScript rewrites the page after load based on signals only available in the browser.
Modern stacks blur the line with edge workers and partial hydration, but the detection problem reduces to the same question: can a reviewer reproduce what a real user saw?
Server-side cloaking: implementation paths
HTTP response branching
The simplest variant is a reverse proxy or application middleware that inspects the request and returns one of two HTML documents. Branching signals typically include:
User-Agentsubstring matching (Googlebot,AdsBot-Google,facebookexternalhit)Source IP against known datacenter ranges or published crawler IP lists
Accept-LanguageandAcceptheadersReferrer (
google.com,facebook.com, ad network domains)Geo-IP and ASN lookups
In Nginx-style pseudo-config:
```
if ($http_user_agent ~* "googlebot|adsbot") {
rewrite ^ /safe.html last;
}
proxy_pass http://money-page-upstream;
```
Edge worker branching
A Cloudflare Worker, Lambda@Edge, or Fastly VCL script does the same logic closer to the user, often with richer signals (TLS fingerprint, HTTP/2 frame ordering, JA3/JA4, Bot Management score). The output is still two different response bodies for two different audiences.
DNS-level splitting
Less common but still seen: split-horizon DNS or GeoDNS sends suspected reviewers to a clean origin and everyone else to the money origin. The two origins serve different content from different IPs, so a header-only inspection cannot connect them.
Why server-side cloaking gets caught
Detection is no longer just "did you send a Googlebot UA and compare HTML." Modern review pipelines include:
Residential and mobile proxy fetches. Reviewers request the page from IPs that look like real users on real carriers, defeating ASN- and datacenter-based gating.
Multi-pass fetching with rotated fingerprints. The same URL is fetched with varied UA strings, headers, TLS fingerprints, and referrers, and the resulting bodies are diffed. A divergence that correlates with bot signals is itself the signal.
Click-from-ad replay. Ad platforms replay the actual click URL including all tracking parameters, from clean infrastructure that mimics a real device. This is the killer for referrer-based branching.
TLS and HTTP/2 fingerprinting on the inbound side. The reviewer is no longer obviously a bot at the network layer.
Continuous re-review. Even if you pass on day one, the URL is sampled again over time. A delayed flip from "safe page" to "money page" trips the diff.
None of these techniques require the reviewer to know your branching rule. They only require that the rule produces an observable difference correlated with reviewer-shaped traffic.
Client-side cloaking: implementation paths
DOM swap after load
The origin returns a benign HTML document. After DOMContentLoaded, JavaScript inspects navigator, screen, timing, or a remote decision endpoint, and rewrites document.body.innerHTML, or toggles display: none on a "safe" block and reveals a hidden "real" block.
JS-based content selection
A variant: the page ships both variants in the DOM (or fetches them via XHR) and a small selector script chooses which to render. Often dressed up as "A/B testing" or "personalization," but with a binary "reviewer vs not" decision rather than a multivariate experiment.
Lazy hydration and deferred fetch
The page server-renders a safe shell, then hydrates a React or Vue tree that fetches "real" content from a JSON endpoint, gated by client-side checks. The reasoning is that automated fetchers will not execute the JS, will not hydrate, and will not see the real payload.
Why client-side cloaking gets caught
Headless Chromium and the modern review crawler stack handle all of this:
Full page rendering by default. Reviewers run Chromium with JavaScript, fetch all subresources, and capture the post-hydration DOM, not the initial HTML.
Long settle windows. The crawler waits for network idle,
requestIdleCallback, and additional timers before snapshotting. Delaying the swap by a few seconds does not help.DOM mutation observers in the crawler. The reviewer can record every mutation between initial paint and final state, then diff against a clean run.
Anti-fingerprint hardening of the reviewer browser.
navigator.webdriver, missing plugins, automation flags, headless markers, and timing tells are systematically masked. The page cannot reliably detect "this is a reviewer."XHR and fetch interception. All outbound requests, including the JSON decision endpoint, are logged. A request that returns different payloads based on detector heuristics is itself evidence.
The DOM is observable. If the rendered page differs from what an ad reviewer was shown, the diff is the violation, regardless of how clever the JavaScript was.
Hybrid cloaking: why combining them does not buy you anything durable
A common evolution: server-side rules to filter out obvious bots, then client-side rules to handle the harder cases, plus a "trust score" computed from both layers. The implementation gets more complex, the maintenance cost grows, but the underlying problem remains. Detection is not looking for any one trick. It is looking for any observable divergence between "what we showed the reviewer" and "what we showed real traffic." Stacking layers multiplies the surface where that divergence can leak. Common leaks:
Decision endpoints whose response shape differs across cohorts
Asset URLs (images, fonts, scripts) that appear in only one variant
DOM size, render timing, or Lighthouse metrics that cluster by branch
Third-party tags loaded conditionally
<link rel="canonical">and OpenGraph tags drifting between variants
Engineering effort spent stacking obfuscation is effort not spent on the legitimate version of the same goal: serving the right user the right experience in a way you can defend.
The legitimate engineering pattern: auditable differentiation
Differentiating user experience by context is not the problem. Doing it in a way you cannot show a reviewer is. The technique we recommend, and detail in cloaking vs smart landing pages, is to invert the design: make the differentiation logic, the inputs, and the outputs all auditable by default, then design variants that are individually compliant and only differ on legitimate axes (language, region, device class, consent state, marketing campaign).
The contract is simple:
Every variant exists in a versioned catalog and is individually reviewable.
Every routing decision is logged with the inputs that produced it.
Any reviewer with the decision log can replay any past response.
Differentiation never includes a "bot vs human" branch. The decision inputs are user-meaningful (locale, plan tier, A/B bucket), not detector-evasion-meaningful.
Reference architecture: audit-friendly traffic differentiation
Below is a minimal reference design. It is small enough to implement in a weekend and complete enough to survive a platform audit.
```
+-----------------------------+
request --> | 1. Context Resolver |
|
- locale, geo, device |
|---|
|
- campaign / referrer |
|
- consent / auth state |
+--------------+--------------+
|
v
+-----------------------------+
|
2. Variant Selector |
|---|
|
- reads variant catalog |
|
- applies routing rules |
|
- returns variant_id |
+--------------+--------------+
|
v
+-----------------------------+
|
3. Renderer |
|---|
|
- serves variant_id |
|
- identical for any client |
|
given same context |
+--------------+--------------+
|
v
+-----------------------------+
|
4. Decision Logger |
|---|
|
- context hash |
|
- variant_id, version |
|
- timestamp, request_id |
+--------------+--------------+
|
v
+-----------------------------+
|
5. Audit Endpoint |
|---|
|
/audit?request_id=... |
|
-> inputs + variant + html |
+-----------------------------+
```
Component contracts
1. Context Resolver. A pure function from request to a structured context object. Inputs are explicit and finite: locale, country, device_class, campaign_id, consent_state, is_authenticated. Crucially, it does not consume "is this a bot" as an input.
2. Variant Selector. Reads from a versioned variant catalog (variants/v3/checkout-IN-mobile.html, etc.) and applies declarative routing rules expressed as data, not code branches. Rules are reviewable in a pull request and reproducible from the context object alone.
3. Renderer. Returns the chosen variant. Given the same context, any client gets the same bytes. There is no per-request divergence beyond what the context declares.
4. Decision Logger. Writes one row per request: hashed context, variant_id, variant_version, timestamp, and a stable request_id. This is the evidence pile.
5. Audit Endpoint. Given a request_id, returns the inputs and the exact rendered HTML. Reviewers, internal QA, and your future self can all answer "why did this user see this page" in seconds.
If a reviewer flags a URL, you do not need to argue. You return the decision log entries for that URL, the variant catalog, and the routing rules. The dispute resolves on evidence.
Comparison: server-side cloaking vs client-side cloaking vs smart landing page architecture
|
Dimension |
Server-side cloaking |
Client-side cloaking |
Smart landing page architecture |
|---|---|---|---|
|
Implementation surface |
Edge / proxy / origin middleware |
JS bundle and decision endpoint |
Variant catalog + selector + logger |
|
Primary detection method |
Multi-pass fetch with rotated identity |
Headless render + DOM diff |
None needed; differentiation is disclosed |
|
Resistance to ad-platform review |
Low; review keeps improving |
Low; Chromium replay is standard |
High by design; passes by being auditable |
|
Maintenance cost over time |
Rising; rule churn per platform |
Rising; fingerprint cat-and-mouse |
Flat; rules are declarative data |
|
Failure mode |
Account ban, domain blacklist |
Account ban, domain blacklist |
Variant rollback, no policy risk |
|
Compliance posture |
Violates major ad policies |
Violates major ad policies |
Aligned with platform expectations |
|
Engineering ROI |
Negative after first ban |
Negative after first ban |
Positive; reused for personalization, i18n, experiments |
Practical migration notes
If you are currently maintaining a cloaked deployment and want to move to the architecture above:
Inventory the divergences first. What does the "bot" version actually omit, and why? Often the answer is "claims a reviewer would question." That is a product and copy problem, not an engineering problem.
Split the variants by user-meaningful axes. Locale, device, consent, campaign. Each variant must independently survive a manual review.
Make the decision deterministic from the context object. If you cannot replay the decision from logs, you cannot defend it.
Remove all detector inputs from the resolver. No User-Agent classification, no JA3, no "is this an automation tool." The resolver should not care.
Run a shadow window. Log decisions under the new system while still serving the old one, diff the outputs, and migrate variant by variant.
For platform-specific notes on TikTok and Meta review behavior, see cloaking for TikTok and Facebook ads. For tooling comparisons including which vendors have moved toward auditable architectures, see cloaking tools compared 2026. For cases where even smart landing pages are the wrong answer, see when not to use cloaking.
FAQ
Q1. Is server-side cloaking harder to detect than client-side cloaking?
Marginally and temporarily. Server-side cloaking forces detection to invest in residential proxies and fingerprint rotation, while client-side cloaking is defeated by any headless renderer. In practice, large ad platforms run both classes of probe routinely, so the gap is not durable enough to plan around.
Q2. Can I use TLS fingerprint randomization to bypass review?
You can complicate one specific class of probe, but the platform reviewer is not the only adversary. Continuous re-review, click-from-ad replay, and user reports all bypass network-layer obfuscation. Engineering effort here has a short half-life.
Q3. Is server-side rendering the same as server-side cloaking?
No. SSR is a performance and SEO technique where the same canonical content is rendered on the server for all clients. Server-side cloaking deliberately renders different canonical content based on who is asking. The mechanics overlap, the intent does not.
Q4. We use feature flags. Is that cloaking?
Feature flags become cloaking when the flag value is derived from detector signals (UA, ASN, headless markers) and the resulting experience is one a reviewer would reject. Feature flags driven by user attributes (plan, region, opt-in) and exposed in an audit log are not cloaking; they are normal product engineering.
Q5. How is the smart landing page architecture different from a CDN with edge rules?
Edge rules are an implementation detail. The architecture's defining feature is the decision log and audit endpoint, not where the routing runs. You can implement the variant selector at the edge, in your app server, or in a serverless function; what matters is that every decision is reproducible and every variant is individually compliant.
Q6. What is the single biggest engineering signal that a cloaking system is about to fail review?
Branching code that consumes detector inputs. If your routing layer has a function called isBot or isReviewer, your system is one detection upgrade away from failing. Strip those inputs, redesign the differentiation around user-meaningful axes, and you remove the entire class of risk.
Closing
The honest summary for engineers: both server-side and client-side cloaking are losing arms races against detection stacks that have far more budget than the average growth team. The work of stacking obfuscation is rarely amortizable, and the failure mode is account- or domain-level. The same engineering hours, redirected into a small, auditable differentiation system, produce a platform you can reuse for localization, personalization, and experimentation, and that you can defend in writing when a reviewer asks.
Build the audit endpoint first. Everything else follows from that.

