Short answer: Server-side tagging moves vendor calls from the visitor's browser into a server container you operate, which means browser-level script blocking no longer covers them. The consent choice still travels with each hit: Google's Consent Mode encodes it into the
gcsandgcdparameters that arrive at your server container. Enforcement, however, is your responsibility: nothing forces a third-party tag inside the container to consult those signals before firing. A complete setup has three layers: forward consent state with every hit, gate each vendor tag in the container with default-deny conditions, and verify independently against the recorded consent state through a server-side API.
Server-side tagging is winning adopters for good reasons: fewer scripts in the page, faster load times, first-party endpoints, and full control over what data leaves your infrastructure. But the same property creates a consent blind spot. When Meta, TikTok or an affiliate network is called from your tagging server instead of the visitor's browser, browser-based consent tooling never sees the request. A banner that blocks scripts perfectly on the client can coexist with a server container that ignores the visitor's choice entirely.
This guide covers what happens after the browser: how consent reaches the server container, how to make server tags respect it, and how to prove it works. The browser-side wiring (default state, update calls, tag consent settings in web GTM) is covered in our Consent Mode v2 GTM setup guide; here we assume that part is in place.
Note: This article is technical documentation, not legal advice. Which consent rules apply to your traffic depends on the laws relevant to your audience; consult a qualified lawyer for decisions about your specific situation.
Table of contents
- Why server-side tagging changes the consent picture
- The architecture: how consent travels from browser to server container
- Step 1: Forward consent to the server container
- Step 2: Gate outgoing vendor tags in the server container
- Step 3: Verify independently with a consent state API
- When to query state server-side, and when forwarded signals are enough
- Testing enforcement end to end
- Common failure modes
- Frequently Asked Questions
- Sources
Why server-side tagging changes the consent picture
In the classic client-side model, every vendor runs in the browser, and enforcement is straightforward: the consent tool keeps non-essential scripts inert until the visitor decides, web GTM applies per-tag consent checks, and anyone can open DevTools and watch which requests fire.
Server-side tagging breaks that symmetry. The browser sends a single stream of first-party hits to your tagging server (typically a subdomain like sgtm.example.com), and the server container fans those events out to Google Analytics, Meta's Conversions API, ad platforms and data warehouses. Those outbound requests originate from your infrastructure; nothing running in the browser can observe or suppress them.
Two consequences follow:
- Browser-level script blocking no longer covers the full picture. It still matters for client-side scripts, but it cannot reach a server-to-server Conversions API call.
- The legal question does not move. Consent-first regimes such as the GDPR with the ePrivacy Directive, and KVKK in Turkey, care about whether the processing had a valid basis, not which machine emitted the request. (For how the two regimes differ on consent specifics, see our KVKK vs GDPR comparison.)
So the enforcement point has to move with the requests: into the server container itself, and into any backend process that talks to vendors on the visitor's behalf.
The architecture: how consent travels from browser to server container
The consent choice does not get lost on the way to the server. In a standard Google setup, three layers cooperate:
| Layer | Role | Who is responsible |
|---|---|---|
| Browser CMP | Collects the choice, fires Consent Mode default and update |
Your consent tool |
| The hit itself | Carries consent state as URL parameters (gcs, gcd) to the server container |
Google tag, automatically |
| Server container | Reads the signals and decides which vendor tags may fire | You |
The consent banner records the visitor's decision and translates it into Consent Mode signals (the conceptual background is in our Google Consent Mode overview). From that point on, every hit the Google tag sends carries the current consent state with it.
The compact carrier is the gcs parameter. Its format is G1 followed by two binary digits: the first for ad_storage, the second for analytics_storage, where 0 means denied and 1 means granted:
| Value | ad_storage | analytics_storage | Typical meaning |
|---|---|---|---|
G100 |
denied | denied | Visitor rejected, or no choice yet under a denied default |
G101 |
denied | granted | Analytics accepted, marketing rejected |
G110 |
granted | denied | Marketing accepted, analytics rejected |
G111 |
granted | granted | Everything accepted |
Alongside it, the gcd parameter carries a more detailed encoding that covers all four Consent Mode v2 signals, including ad_user_data and ad_personalization, and how each was set. When the Google tag on your page sends to a server container URL, these parameters arrive with each request, and the container's preview mode shows them on the incoming hit.
That is where Google's automation ends. The signals arrive; whether anything inside the container respects them is a configuration question, and it is yours.
Step 1: Forward consent to the server container
If you use the standard Google tag (gtag.js or the GA4 tag in web GTM) pointed at your server container, consent forwarding comes with it: once the Consent Mode default and update commands fire correctly in the browser, the consent parameters are included on the hits automatically. There is nothing extra to build, but there is something to verify:
- Open your server container's preview mode and load the site in another tab.
- Reject everything in the banner.
- Select an incoming request in the preview timeline and inspect its parameters: you should see
gcs=G100(and agcdvalue) on the request. - Accept everything and repeat: the new hits should show
gcs=G111.
If those parameters are present and change with the visitor's choice, forwarding works.
The picture is different with custom clients and non-standard transports. A hand-rolled fetch that whitelists known parameters, a proxy or CDN rule that rewrites query strings, or a redirect hop can silently strip gcs and gcd; events still arrive and look healthy, but every hit is now consent-anonymous. Check the raw incoming request in preview, not just the parsed event, to confirm the parameters survived the trip.
One more thing to forward: a visitor identifier. For server-side consent lookups (Step 3), the container needs to know which visitor an event belongs to in your consent records. With the cerez.io SDK, the visitor id lives in a first-party cookie named cb_vid; read it in the browser and attach it to your events as a parameter.
Step 2: Gate outgoing vendor tags in the server container
Inside the server container, adopt one rule and apply it everywhere: default deny. A vendor tag fires only on positive evidence that the relevant consent exists. Absence of a signal, an unparseable value, an unknown visitor: all of these mean the tag stays silent.
The mechanics in sGTM:
- Expose the consent state as a variable. Create a variable that reads the consent information from the parsed event: the
gcsdigits, or a custom category parameter your CMP forwards with the hit. - Add conditions per vendor tag. Use trigger conditions or blocking exceptions so that each outbound tag checks the category it depends on. Marketing endpoints (Meta Conversions API, ad platforms, retargeting partners) require the advertising signal granted. Analytics and measurement endpoints require the analytics signal granted.
- Map categories deliberately. A typical mapping: your banner's marketing category gates
ad_storage-dependent tags, the analytics category gatesanalytics_storage-dependent tags. Tags serving strictly necessary functions are the only candidates for firing without consent, and that list should be short and documented.
Two limits to keep in mind. First, gcs encodes exactly two storage signals; if your banner distinguishes more categories (functional or preference, for instance), that granularity is not in gcs, so forward the categories explicitly as event parameters or resolve them with a state lookup. Second, conditions only exist where you add them: a container is only as compliant as its newest tag, so make consent conditions part of the definition of done for every tag added later.
Step 3: Verify independently with a consent state API
Forwarded signals tell you what the browser asserted at the moment of the hit. They cannot answer a different question: what does the consent record say for this visitor, right now? For that, ask the system that stores the consent logs.
cerez.io exposes this as a server-side endpoint, built for exactly this sGTM use case. It is part of the same public API surface the banner uses, keyed by your site's API key:
GET https://cerez.io/api/v1/consent/state/{api_key}?vid={visitor_id}
vid is the visitor identifier the cookie consent banner assigned when it recorded the choice (the cb_vid first-party cookie from Step 1; the parameter is also accepted as visitor_id). A worked example:
curl "https://cerez.io/api/v1/consent/state/your_api_key?vid=7f3a2c91-4b0e-4d2a-9c1f-8e5d6a7b0c12"
{
"found": true,
"expired": false,
"action": "partial",
"categories": {
"necessary": true,
"functional": false,
"analytics": true,
"marketing": false
},
"given_at": "2026-08-09T14:32:11+03:00",
"expires_at": "2027-08-09T14:32:11+03:00"
}
The response contract:
| Field | Type | Meaning |
|---|---|---|
found |
boolean | Whether a consent record exists for this visitor on this domain |
expired |
boolean | Whether the most recent consent has passed its expiry date |
action |
string | The last recorded action: accept_all, reject_all, partial or update |
categories |
object | What the banner recorded; with the cerez.io banner, a map of necessary, functional, analytics, marketing to true or false |
given_at |
ISO 8601 | When the consent was given |
expires_at |
ISO 8601 | When it expires, following the domain's configured consent duration |
When no record exists for the visitor, the endpoint returns {"found": false, "action": null, "categories": []}. A missing vid returns a 422, and an unknown or inactive API key returns a 404. The enforcement rule your server code derives from this is one line of logic:
allow_marketing = found AND NOT expired AND categories.marketing == true
Everything else, including found: false and expired: true, means treat the visitor as unconsented. The endpoint sits behind the same rate limiting as the rest of the SDK API (500 requests per minute per API key), which is one practical reason to cache lookups briefly rather than querying on every event; the FAQ covers safe caching.
When to query state server-side, and when forwarded signals are enough
You do not need a state lookup on every event. The two mechanisms answer different situations:
| Situation | Forwarded signals (gcs/gcd) |
State API lookup |
|---|---|---|
| Live hit stream through sGTM | Yes, each hit carries fresh state | Not required |
| Webhook from a payment or booking provider | No browser context exists | Yes |
| CRM enrichment or audience sync jobs | No hits involved | Yes |
| Offline conversion uploads | Conversion happens away from the site | Yes |
| Batch or scheduled backend jobs | No browser context | Yes |
Finer categories than the two gcs signals |
Not carried | Yes, full category map |
| Consent expiry awareness | Not carried | Yes, expired and expires_at |
| Auditing that forwarding works | Source of the claim | Independent cross-check |
The pattern to remember: forwarded signals govern the synchronous path, lookups govern the asynchronous one. A live hit carries current consent state and can be gated on directly; a webhook, a nightly conversion upload or a CRM job has no browser in the loop, so the recorded consent state is the only thing you can legitimately consult. The lookup is also the cross-check that turns belief in forwarding into audited evidence.
Testing enforcement end to end
Test the whole chain in the container's preview mode with four scenarios, judging each by outgoing requests, not tag status alone:
- Reject all. Decline everything in the banner, then generate events (page views, a test conversion). In sGTM preview, the incoming hits should show
gcs=G100, marketing and analytics vendor tags should not fire, and the container's outgoing request log should contain no calls to those vendor endpoints. - Partial consent. Accept analytics only. Incoming hits show
gcs=G101; analytics endpoints receive requests, marketing endpoints stay silent. - Accept all. Everything fires; this confirms your conditions do not overblock.
- Fresh visitor. In a clean private window, generate events before touching the banner. Under a correct denied default, hits carry
gcs=G100and vendor tags stay silent. This is the scenario that catches default-grant misconfigurations.
The reason to insist on the request log: a tag can report a fired status while a template-level check suppressed the network call, and the reverse can happen with custom templates; the log is the ground truth of what actually left your server.
Re-run the four scenarios after every container change, because a newly added tag without consent conditions reintroduces the bypass silently. And check the browser side of the ledger with a cookie scanner periodically: it shows which cookies are actually set before and after consent on the page itself, which server-side testing does not cover.
Common failure modes
Five patterns account for most real-world consent bypasses in server-side setups:
- The container fires vendors unconditionally. The default state of a freshly built sGTM container: events arrive, tags fire, nothing checks consent. Symptom: vendor endpoints receive requests even in a reject-all test. Fix: audit every tag for a consent condition; treat a tag without one as a defect.
- A custom client drops the consent parameters. A parameter whitelist, a CDN rewrite or a redirect hop strips
gcs/gcdin transit. Symptom: incoming requests in preview show no consent parameters for any visitor. Fix: inspect the raw incoming request, then widen the whitelist or adjust the rewrite so the parameters survive. - Stale cached state. A backend caches a consent lookup for days; the visitor withdraws consent; the server keeps sending data on the old answer. Symptom: enforcement lags withdrawal. Fix: keep cache TTLs in minutes, and treat every cache miss as denied until refreshed.
- Missing signal treated as granted. Somewhere a condition reads "fire unless explicitly denied." Every dropped parameter then becomes implicit consent. Fix: invert the logic everywhere to default deny; absence of evidence is denial.
- Expiry ignored. The consent record exists but has passed its expiry date, and the server keeps acting on it. Symptom:
expired: truein the state response while vendor calls continue. Fix: includeNOT expiredin every enforcement rule, as in the one-line rule in Step 3.
Frequently Asked Questions
Does Consent Mode work automatically with server-side GTM?
Short answer: Partially. The Google tag forwards the consent state with every hit to your server container automatically once the browser-side default and update commands are wired, and Google's own components pass those signals along toward Google endpoints. What is not automatic is enforcement for everything else: third-party vendor tags in the server container (Conversions APIs, ad platforms, custom endpoints) do not consult the forwarded signals on their own. You have to add consent conditions to each of them, which is Step 2 of this guide.
Do I need a consent state API if I already forward gcs and gcd?
Short answer: Not for the live hit stream: events flowing browser to container carry current consent state, and gating on those parameters is sufficient there. You need a state lookup for everything that happens without a browser in the loop: webhooks, CRM enrichment, offline conversion uploads, scheduled jobs. It also gives you what the forwarded parameters cannot: the full category map beyond the two gcs signals, the expiry status, and an independent record to audit your forwarding against.
Can I cache consent state on my server?
Short answer: Yes, briefly. Caching per visitor id for a few minutes keeps you comfortably inside the API rate limit (the cerez.io endpoint shares the SDK API's 500 requests per minute per key) without meaningfully delaying enforcement. The safety rules: keep the TTL short, never serve a cached "granted" past its expires_at, and treat a cache miss or an API error as denied rather than falling back to the last known positive answer. A withdrawal must take effect within your TTL window, so choose it accordingly.
What should my server do when consent has expired?
Short answer: Treat the visitor exactly like one who never consented: suppress all non-essential vendor calls. The state endpoint surfaces this explicitly with expired: true and the expires_at timestamp, so the check is one condition in your enforcement rule. Do not delete or overwrite the historical record; the expired log remains your evidence that consent existed for the period it covered. On the visitor's next visit, the banner re-prompts and a fresh record replaces the expired state going forward.
Sources
- Google Consent Mode developer documentation
- Google Tag Manager server-side documentation
- Google Tag Manager developer documentation
- Regulation (EU) 2016/679 (GDPR), full text
- ePrivacy Directive 2002/58/EC, full text
- Turkish Personal Data Protection Authority (KVKK)
Running a server container and want the consent side handled? Start free: the cerez.io banner records consent with a visitor id out of the box, and the consent state endpoint is available for your sGTM and backend enforcement from day one, no credit card required. Plan details are on the pricing page. This article is for information purposes and is not legal advice.