Whenever you click on a Google ad you land on a URL with a
gclid GET parameter - the Google Click ID. It's what ties a click to a conversion (a purchase, a booking etc.) later, so an advertiser can tell which click (and thus which exact advertisement) led to a sale, and thus which ads to invest more money into. The value is a 50 to 90-ish character string that looks random, and comes in two visible shapes:Cj0KCQjw...D_BwE ("long format", ~91 chars), e.g.: CjwKCAjwsaqzBhAqEiwAmj8B5_Ca0Evik_S8hpmohpjv0MrrT67o5zcscI74ID0TFgCMjoYC5qhh-RoCfE4QAvD_BwE EAIa...D_BwE ("short format", ~55 chars), e.g.: EAIaIQobChMI9-Kyq8fNlgMVO6VQBh10jhEoEAQYASABEgIcivD_BwE
For a project I needed to know what I can extract from that value and if it can tell me a little more about the actual ad itself.
Step one: it's a protobuf
I base64-decoded one and looked at the bytes. It's URL-safe base64 (
- and _ instead of + and /). A long token decodes to something starting:0a 3c 0a 08 08 f0 b1 aa b3 06 10 2a 12 2c 00 ...
That opening is recognizable: In the protobuf wire format every field begins with a tag byte: the low three bits are the wire type, the rest is the field number.
0a means "field 1, wire type 2 (length-delimited)", and the 3c after it is the length. Inside that comes 0a 08 - field 1 again, 8 bytes - then 08, "field 1, wire type 0 (varint)". Exactly the way protobuf serializes. You don't need the .proto definition to take it apart; the wire format carries enough structure to walk the tree, you just get field numbers instead of names.So a
gclid is a base64url-encoded protobuf. Decoding more of them showed two distinct layouts - a long one (Cj0K/CjwK) and a short one (EAIa) - as already mentioned in the intro. Both formats share a field #2 set to
2, and a field numbered #16382 - probably just a deliberately high field number used as a trailer/sentinel - set to 1. Most gclid (in both formats) seem to end in ...D_BwE due to that last field. For both of these fields I couldn’t really find out what they mean, but they are static across most IDs.The actual payload lives in field 1 for the long format and field 3 for the short one.
The parser is short enough to include in full - the one wrinkle is that random bytes sometimes parse as "valid" protobuf, so it has to treat known leaf fields as opaque instead of recursing into them:
import base64 def b64url(s): return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)) def read_varint(b, i): v = shift = 0 while True: byte = b[i]; i += 1 v |= (byte & 0x7f) << shift if not byte & 0x80: return v, i shift += 7 OPAQUE = {(1, 2), (1, 3), (3, 2)} # leaves: don't recurse into these def walk(b, base=()): i, n = 0, len(b) while i < n: tag, i = read_varint(b, i) field, wt = tag >> 3, tag & 7 path = base + (field,) if wt == 0: val, i = read_varint(b, i); yield path, val elif wt == 5: yield path, int.from_bytes(b[i:i+4], "little"); i += 4 elif wt == 1: yield path, int.from_bytes(b[i:i+8], "little"); i += 8 elif wt == 2: ln, i = read_varint(b, i) chunk = b[i:i+ln]; i += ln if path in OPAQUE: yield path, chunk.hex() else: yield from walk(chunk, path) else: break
I'll take the short format first, since more of it is readable.
The short format
Here's one I generated by clicking an ad, run through the parser.
decode("EAIaIQobChMI9-Kyq8fNlgMVO6VQBh10jhEoEAQYASABEgIcivD_BwE") = (2) = 2 (3,1,1,1) = 1788271733289335 (3,1,1,2) = 105948475 (3,1,1,3) = 672239220 (3,1,2) = 4 (3,1,3) = 1 (3,1,4) = 1 (3,2) = 1c8a (16382) = 1
The left side are the protobuf field paths - the address of each value in the message's nested structure. Think of them like a multi-dimensional array, e.g.
arr[3][1][1][1] .(3,1,1,1) contains a 16-digit number - which definitely looks like a microsecond Unix timestamp. Divided by a million it reads 2026-09-01 14:08:53 UTC, which was the moment I clicked. I checked it the obvious way: generate a token, decode it, compare to the clock. The short format records the exact click time in cleartext.(3,1,1,2) and (3,1,1,3) contain a 28-bit and a 32-bit integer, together some kind of click identifier so I named them “idA” and “idB”:(3,1,1,3) behaves like a plain random nonce, I couldn’t find any regularity in the numbers.(3,1,1,2) is odd. The low 14 bits are random, but the high bits only ever take about fifteen distinct values. The count of unique values I saw is exactly 15 from bit 14 upward, then explodes below it - so the field is really B · 2¹⁴ + noise, with B one of ~15 values.My first guess was that B encoded a device or browser type - "iPhone Safari" versus "Windows Chrome." It doesn't; the distribution is nearly identical on every OS. What B seems to track is the user or campaign info: it stays constant across ~98% of a given person's clicks and almost never changes. Since it has only 15 possible values, its probably more of a cohort than a real identifier.
It's independent of OS, browser, geography, consent, and ad type, which rules out a plain hash of the device (too few values, far too skewed) and points instead to a stable per-user segment carried in the clear - a cohort or partition id of some kind, though I can't tell which from the outside. As a stable cleartext per-user value, it also works as a weak cross-site fingerprint: two clicks sharing a rare B are probably the same person. However most values of B are very often seen.
Then the three small integers,
(3,1,2), (3,1,3), (3,1,4). These took the longest to decrypt. I’m still not 100% sure, but generally this seems plausible:They correlate with the page each click landed on. Clicks that ended up on a specific product page almost always had
(3,1,2)=4 and (3,1,4)=1; clicks that landed on a category or home page had 0 and 0; and (3,1,3) was a small number, usually 1 on the generic pages and larger - up to about 35 - on the specific ones. So it seems this generally tells Google if we landed on some general landing page or a more specific product page, feature page etc., maybe with a “main category” and “subcategory” enum.A lot of my tested clicks came from Google's newer AI-driven search campaigns (”AI Max”), which generate ads pointing straight at specific product pages. It seems
(3,1,4)=1 could mark an AI-generated ad, and the index measured how deep into the catalog the match went.I also saw
(3,1,2)=4 which appears on clicks from 2024 - before those AI campaigns existed. When I matched the values against campaign types where I could identify it, the correlation ran the other way: the AI campaigns were mostly 0, and the 4s came from ordinary ones. However, that can be a false positive since a lot of other things also changed in the campaigns in that time.So in general: these three fields are an ad-level code that tracks the kind of landing URL - a specific product page versus a category or home page. The values have been stable since at least 2024, and a couple of rare ones appear only recently, which suggests the field gains new values over time as Google adds ad formats. I can't assign each number a precise meaning.
The long format
The long format is stingier - it gives up a date and little else.
(1,1,1) = 1718262000 # a Unix timestamp (1,1,2) = 42 # a small integer (1,2) = 00 9a3f01e7 f09ad04b...f91a # 44 bytes (1,3) = 7c4e # 2 bytes (2) = 2 (16382) = 1
(1,1,1) is again a Unix timestamp, however it always lands on exactly 00:00:00. So the time of day had been dropped and only the date kept.The offset was
07:00 on some tokens and 08:00 on others, and sorting by date showed the switch landing on the exact days US daylight saving time begins and ends. Seven or eight hours behind UTC, following US DST, is America/Los_Angeles - Google's headquarters. The long format stores the click date at midnight Pacific time. Whoever designed it coarsened the timestamp to a single day, in Google's own timezone.That left a question: the date of what - the click, or a campaign start, or a key rotation? Actually, that way pretty easy to answer: it's the click date, rounded to the day. I compared a few gclids from the last few years and they all line up without issues.
Everything that identifies the click for Google sits in
(1,2) as 44 bytes.- The first byte is always
00.
- The next four vary day to day but are identical for every click on the same day, even across different pages (let’s call it the key identifier).
- The remaining 39 are, by every test I ran, indistinguishable from random. Fixed length, high entropy, with a small varying header in front - that is encrypted data, and the four repeating bytes are a key identifier saying which day's key was used. 39 splits neatly into 23 bytes of content and a 16-byte authentication tag, which is what AES-GCM or ChaCha20-Poly1305 produce. I can't read it, and that's probably the design: the identity is encrypted under a key only Google holds.
Notably there's no visible nonce anywhere in the 44 bytes, which any standard AEAD needs, so the layout can't be a plain
[ciphertext][tag]. It could equally be CTR mode with a truncated MAC, a deterministic/SIV-style mode, or something bespoke. The nonce/IV seems to be properly unique per click
The long format only really reveals the date, plus that key ID. I tried to find some regularity in the key identifier to see if we can predict the next days’ key ID but they also seem to be random.
The prefix
As a small technical detour: At the start I thought there are two different “long formats”, since I’ve seen
Cj0K and CjwK as two different prefixes indicating different versions. However they aren't. Decoded, the only difference is one byte - field 1's length is 61 in one case and 60 in the other - and that byte comes from (1,1,2), the per-day key index. It's a varint, so it takes one byte for values under 128 and two bytes from 128 up. On days when the index is 128 or higher the message is a byte longer, and the base64 prefix flips from CjwK to Cj0K. The prefix tells you nothing about the click; it's a side effect of how one internal counter serializes.The day key is global
The four-byte key identifier inside the blob changes once a day. I assumed it was scoped per advertiser or per account. However turns out it isn't: long tokens from completely unrelated sites, collected on the same day, carry the same four bytes and the same key index, and a different day gives a different value that is again shared by everyone. The key rotates once per Pacific day, across all of Google Ads at once.
That has one practical use. From whatever tokens you have, you can build a table mapping each date to its key identifier, then check any long
gclid against it: if the date and the key bytes don't agree for that day, the token didn't come out of Google's pipeline. You still can't decrypt anything - but you can spot one that's been forged or mangled.Aside: gbraid
Sometimes the URL carries
gbraid (or wbraid) instead of a gclid. Google uses these where a per-user click ID isn't allowed - mostly iOS and no-consent traffic. Decoded, a gbraid is a short fixed header followed by a token that repeats across many clicks in the same campaign: it identifies the campaign, not the individual click. For attribution it tells you the campaign and nothing about the person.Summary
The parts I'm confident about are the ones reproducible from tokens alone:
- A
gclidis base64url-encoded protobuf, in two formats that share two constant fields (#2 = 2,#16382 = 1).
- Short format:
(3,1,1,1)is the click time in microseconds, cleartext.
- Long format:
(1,1,1)is the click date at midnight America/Los_Angeles.- The
Cj0K/CjwKprefix is a varint length artifact, not a separate format. - The long-format day key is shared across advertisers and rotates daily.
Reasonably sure, but not proven: the 44-byte block is a per-day-encrypted identity (
scheme byte, key ID, then AEAD ciphertext and tag); the short format's (3,1,1,2) is a coarse bucket plus a nonce; the three enums track landing-URL type; gbraid is a per-campaign bucket.Still open: what's inside the encrypted block (sealed - not something more analysis fixes), what the two-byte tag is, whether the daily key is predictable or merely logged, and the exact meaning of the enum values.
A
gclid, in the end, is a small protobuf carrying a timestamp or a date, a few flags, and - in the long format - an encrypted core that keeps the identity with Google. You can classify one, date it, read the click time out of the short format, and check a long one against the daily key. You can't read who it belongs to, extract some Google User ID or something it seems.Try for yourself: https://gclid-lens.vercel.app/
What surprised my a little was how old ProtoBuf really is. Maybe its just me, but it came into my view only a few years ago. I thought, surely the gclid, in use since at least 2010, is a very early use of it. However, it seems to predate to 2001 and was almost a decade old by the time gclid uses it.