~/toolhouse
2026.04.28·8 min read·adtech · gam · engineering · debugging

GAM's prev_scp: encoding 6 slots' targeting state in 200 characters

prev_scp packs 6+ slots of GAM targeting state into one URL parameter. Here's how it's encoded, and three production bugs only the decoder shows.

Open

[VOICE: 60-second production incident where decoding prev_scp revealed something the GAM UI did not. Lead with the symptom (CPM delta, fill-rate weirdness, mismatched dashboards) before naming the parameter. Concrete and short — under 80 words.]

The encoding density problem

Google's ad-request URL has a practical 4-8 kB ceiling. A page with 6 ad slots, each with 4-10 custom targeting keys, doesn't fit if you encode it in JSON.

prev_scp solves that by packing every slot's targeting state into one URL parameter, with two delimiters and a double-encoding pass:

  • key1=value1&key2=value2 inside one slot.
  • , between slots.
  • Double-URL-encoded when values contain reserved characters (&, =, ,).

[VOICE: numbers, not adjectives. How dense is "dense"? 6 slots × 5 keys ≈ how many chars on the wire? What's the JSON-equivalent? Why this density matters specifically for SRA.]

Anatomy of prev_scp

A two-slot SRA request:

https://securepubads.g.doubleclick.net/gampad/ads
  ?iu=/1234567/example_pub/homepage/leaderboard
  &iu_parts=1234567,example_pub,homepage,leaderboard
  &prev_iu_szs=970x250|728x90,300x250|300x600
  &prev_scp=pos%3Dtop%26section%3Dnews%26pb_bid%3D2.45,pos%3Dright%26section%3Dnews%26pb_bid%3D1.10
  &cust_params=section%3Dnews%26user%3Danon%26abtest%3DvariantB
  &correlator=4825183746201&gdfp_req=1

Decoded:

  • Slot 0 → pos=top, section=news, pb_bid=2.45.
  • Slot 1 → pos=right, section=news, pb_bid=1.10.
  • Page-level (cust_params) → section=news, user=anon, abtest=variantB.

[VOICE: walk through the example end-to-end. Where each value comes from in the GPT call (googletag.pubads().setTargeting(...) vs slot.setTargeting(...)). The mental model: cust_params is the page, prev_scp is the slots, indexed by prev_iu_szs.]

Companion params

iu_parts — comma-separated network ID + ad-unit hierarchy (1234567,homepage,leaderboard_top). The first segment is the network ID; the rest is the ad-unit path. The decoder uses it to build a deep link to the GAM admin console.

prev_iu_szs — per-slot eligible sizes. Same comma-joined slot order as prev_scp. Within a slot, sizes are |-joined (WxH|WxH). Example: 300x250|728x90,300x600 is two slots, the first eligible for 300x250 or 728x90, the second locked to 300x600.

cust_params — page-level custom targeting. Same key=value& shape as one prev_scp slot, but applies to the whole page.

correlator — random integer, identical across all GAM requests in one pageview. Resets via googletag.pubads().updateCorrelator(). Used for impression deduplication.

[VOICE: lock down the slot-ordering invariant for the reader — N-th prev_scp chunk = N-th prev_iu_szs chunk. Field-observed; assume it always holds. One-line caveat: "I have only ever seen this hold."]

3 things the decoder reveals that GAM's UI doesn't

1. Identical-on-paper slots with divergent prev_scp → state leak

Two slots configured with the same setTargeting() calls should produce identical chunks in prev_scp. If chunk N has a key chunk N+1 lacks (or vice versa), targeting was set on a stale or shared slot reference — common when slot wrappers are reused across page templates.

[VOICE: a production debugging moment. Two "identical" slots in prev_scp with a stray pos=top on the second one. One-line fix in the slot factory mutating shared state. The CPM impact in actual numbers — how much did fixing it move the needle? How long did it sit broken?]

2. iu_parts drift across adjacent requests → config flip mid-session

iu_parts is deterministic given page template + slot config. If the same slot label produces different iu_parts arrays across two requests in one session, something flipped the ad-unit path at runtime — feature flag, late A/B variant, or a CMS-injected wrapper.

[VOICE: a publisher whose iu_parts swapped from /network/site/desktop to /network/site/mobile mid-pageview because a viewport listener rebuilt slots after a resize. The decoder surfaced it; GAM UI only showed the aggregate.]

3. Different correlator on the same pageview → double counting

Every request from one page view must share a correlator. Two requests with different correlators from the same pageload are two impressions to GAM, even if the user saw one. Most common cause of "GAM impressions don't match GA pageviews" tickets.

[VOICE: a production refactor where a hot-reloading slot-refresh path called updateCorrelator() once too often, doubling impression count for a refresh-heavy publisher. Revenue/reporting impact. How prev_scp + correlator side-by-side made it obvious.]

Common debugging mistakes

[VOICE: 1-2 production examples — the cust_params vs prev_scp confusion (page-level vs slot-level; people grep one when they want the other), the comma-empty-slot pattern (a=1,,b=2 is three slots, not two), and slot-order assumptions (assuming prev_iu_szs order without verifying against prev_scp).]

Close

If you ever need to read a GAM ad request, paste it into the decoder. Runs entirely in your browser — nothing leaves your machine.

[VOICE: one-line CTA. Don't over-explain. The decoder does the work; this post is the why.]