On August 26, 2026, PPC Land reported a Google change that most people probably missed unless they work with rank tracking. Now, when you look at search results, the links no longer show the real site address. Instead, they all point to google.com/goto?url= with an encoded string. When you click, Google redirects you to the real page on their server side.
In the same article, Derek Perkins at Nozzle measured what this means in practice. If you want to resolve all the links for a five-page ranking, you need between 500 and 1,000 requests. You can’t decode these links yourself, and HEAD requests won’t give you the answer. The only way to know where each result points is to ask Google for every single link.
Give your AI a web data layer – Decodo’s Web Scraping API turns any site into clean, structured data your models can actually use.
Most people talked about this as if it were a typical rollout, something Google would turn on for everyone over time. That’s why Barry Schwartz checked for it on his own machine and didn’t see anything, while Nozzle saw it almost everywhere.
Both were right in their own way since this isn’t a standard rollout. It’s Google responding to something.
If you see it this way, the cost story isn’t as dramatic as it sounds. I’ll show you the numbers, but to sum up: those 500 to 1,000 requests are real, they’re cheap, and honestly, they’re not the real problem.
Two windows, one variable
Here’s an experiment you can try yourself. It only takes about four minutes.
Start by opening Chrome with a fresh profile and search for something commercial (I used “best running shoes”). In the console, count how many result links go directly to the destination and how many use the goto wrapper, using this script.
(()=>{const f=()=>{const a=[...document.querySelectorAll('#rso a[href], #search a[href]')];const h=x=>x.getAttribute('href')||'';return `goto=${a.filter(x=>h(x).includes('/goto?')).length} direct=${a.filter(x=>/^https?:\/\//.test(h(x))&&!/google\.|gstatic/.test(h(x))).length} total=${a.length}`};console.log('t0',f());setTimeout(()=>console.log('t+5s',f()),5000);setTimeout(()=>console.log('t+20s',f()),20000);})()Then, open a second Chrome window, again with a fresh profile and the same connection, but this time add the --remote-debugging-port flag. Don’t attach anything to that port and don’t automate anything. Just type the same query by hand.
window goto direct total
fresh profile, no debug port 0 213 215
fresh profile, --remote-debugging-port 96 0 106
fresh profile, --remote-debugging-port 83 0 93HTTP/3 and QUIC run on UDP, so the proxy layer has to carry it too. anyIP gives compatible clients production-ready SOCKS5 UDP ASSOCIATE across its residential and mobile network for your SERP scraping.
Everything else stays the same: same machine, same home connection, same query, just a few minutes apart. Without the flag, you see 213 destination URLs in the HTML, just like before. With the flag, you get zero direct URLs and 96 wrapped links instead.
Both results are stable. I checked each page at load, after five seconds, and after twenty seconds, and the counts never changed. So it’s not JavaScript changing the page later. The difference comes from what Google sends you right from the start.
I ran nine more tests using a stealth browser with full automation, and every time I got zero direct URLs, always wrapped. But when I browse normally, logged in or in a private window, I never see any wrappers at all.
So, from my little experiment, it seems that Google detects when I’m using an automated solution and serves me different results. The key factor for showing URLs or not is whether Google thinks it’s talking to a human or a script.
What this actually means
If you look at the reports with this in mind, it all makes sense. Schwartz didn’t see the behavior because he was just using his browser. Nozzle saw it everywhere because they’re a rank tracker, and Google had already classified their traffic. What people called a ‘bucketed rollout’ was really just two different groups seeing two different things.
This also changes what those 500 to 1,000 requests really mean. That’s not the new baseline cost for scraping Google. It’s what you pay after Google has recognized you. If you haven’t been flagged, the addresses are still in the markup, ready to grab for free.
For me, this is more interesting than just a link format change. Blocking is obvious: you’re either in or out, and you know it.
Here, Google still gives you results, the page loads, your parser can still find titles, positions, and snippets. Everything works. The only thing missing is that one field, and now you have to pay a network round trip for each row to get it.
There’s no warning when this happens. A pipeline that worked fine in June still returns HTTP 200 and well-formed HTML today, but the data quality quietly drops in the meantime.
What the wrapped page contains
Ten captures on the treatment side, across two queries, nine from a stealth browser and one read over CDP from a page a person opened by hand:
in-result anchors : 408
goto wrappers : 339
direct URLs : 0
old /url? wrappers: 0In every capture, I found zero direct destinations. The old /url?q= wrapper, which at least left the address in plain text, is gone too. Now, every organic link, every video timestamp, and every ‘translate this page’ link is wrapped. They all have a ping attribute.
The difference between the number of anchors and wrappers comes from Google’s own navigation. For example, on the CRM software pricing page, 18 out of 38 in-result anchors are related searches, and three are ad links. All 17 external destinations are wrapped.
The token is protobuf, and that doesn’t help you
One question I had was: can the token be reverted to the URL? Of course, not.
Every token starts with CAES. Base64 decoded, that’s 08 01 12: protobuf field 1 as a varint set to 1, then field 2 as a length-delimited blob. A field walk over the decoded bytes confirms it.
field 1, wire 0, value 1
field 2, wire 2, len 99..119The blob inside field 2 is where the destination should be, and it isn’t there. Every one of the 183 distinct tokens we collected opens with the same five bytes, 01 EB 3B 30 15, and everything after is high entropy noise. We measured 6.27 to 6.44 bits per byte over roughly a hundred bytes, which is what you get on random or encrypted data. Base64, base64url, percent decoding first, gzip and zlib on the result, none of them produce a single printable URL fragment.
What you see here is a constant header followed by an opaque payload, just like you’d expect from a key identifier and ciphertext. You can’t decode this yourself because you don’t have the key. Reversing an encoding is one thing, but getting a key from Google is a whole different story. So, the only way to get the destination is to let Google look it up for you, one request at a time.
HEAD is not refused, which is worse
Another idea could be to use HEAD requests, so that in cases of redirects, the Location header contains the final URL. This could be a faster way to check which URL the token corresponds to.
What we measured from our test is annoying instead. Across 50 wrapped links resolved from the home line with curl_cffi and redirects disabled:
HEAD -> 200 on all 50, Location header present on 0
GET -> 302 on all 50, Location header present on 50
median GET latency : 59 ms
mean bytes per GET : 1068 B (headers plus body, redirect not followed)HEAD requests aren’t blocked. You get a 200 response, but no redirect. If you just log the requests, you’ll see fifty successes but get nothing useful, and you’ll have to figure out why. The usual HTTP cache path doesn’t fail with an error. It just looks like it worked, which can waste a lot of time before you realize what’s going on.
GET requests work as expected: one per link, you get a 302 with a Location header, and you have the destination. I tried fifty of these from a home IP and didn’t hit any rate limits, so the throttle is set higher than that.
The mapping is not stable, so there is no lookup table
The last mitigation idea I had was to create a lookup table between tokens and final URLs. We compared the token attached to each result title across all captures. That covers three reloads inside one accepted session and several entirely separate browser sessions, hours apart, which is a harder test than reloading. Forty-six results appeared in more than one capture.
results seen in more than one capture : 46
token identical : 0
token changed : 46None of the tokens are stable. The same link to the same page on the same query gets a new token every time. The CAES and 01 EB 3B 30 15 prefix stays the same, but the rest changes completely. In one case, I saw a result with 24 different tokens across my tests.
This answers a question that came up in the original discussion. You can’t build a lookup table and reuse it, because there’s nothing stable to key it on. The payload is tied to the impression, not the destination, so you pay the full resolution cost every time. I tried to find a caching trick here, but there isn’t one.
Those reloads showed me something else. They were automated navigations using a script over CDP, but the session was started by a human. Google allowed all of them. Once Google accepts a session, it keeps working even if you automate it. The real check happens when you submit the query, not after.
Where the destination leaks anyway
In three out of ten captures, I actually got some destinations in the clear, and I doubt that’s intentional.
When Google offers to translate a result, the “translate this page” link is usually a wrapper, but sometimes it’s a plain translate.google.com/translate?u= with the target sitting in the clear:
https://www.salesforce.com/eu/crm/pricing/
https://www.zoho.com/en-us/crm/zohocrm-pricing.html
https://www.creatio.com/glossary/crm-pricingI checked if those were the same destinations you’d otherwise have to resolve. Looking at the first eight wrapped links and resolving them, six were already available for free in a translate link. The other two were Italian results, which Google didn’t offer to translate.
This happened in three out of ten captures, using the same queries and the same browser. I couldn’t figure out what triggers it. I think of it as a discount that sometimes shows up, so always check for it before you spend a request.
What’s the cost of this modification?
On a wrapped page, I saw between 59 and 113 wrapper anchors, depending on how many results there were. After deduplication, that’s 59 unique tokens on my reference page. For a five-page ranking, that’s 565 requests if you resolve every anchor, or 295 after deduplication. The 500-1,000 range makes sense if you just resolve everything, which is probably what most people do.
The good news is that solving these tokens actually takes almost nothing. I tried resolving the same links four different ways: using curl_cffi with full Chrome TLS impersonation, and with plain requests, both with and without a Referer header. No cookies, no session, no proxy involved.
requests, no referer 12/12 resolved, 302 + Location, median 124 ms, 1269 B
requests, with referer 12/12 resolved, 302 + Location, median 137 ms, 1268 B
curl_cffi, no referer 12/12 resolved, 302 + Location, median 115 ms, 1479 B
curl_cffi, with referer 12/12 resolved, 302 + Location, median 123 ms, 1479 BPlain requests work just fine. The stealth setup you needed to get the page isn’t needed to resolve the links. You don’t need a residential proxy, cookies, or even a referer, at least at this small scale.
Tokens also stay valid. I captured links one day and resolved all 12 of them the next morning, so you don’t have to resolve them right away. You can collect now and resolve later, even in bulk.
Now, about throughput. This is where people expected to hit a wall. I ran 1,200 resolutions from a regular IP using a plain client, increasing concurrency as I went:
concurrency 1: 60/60 resolved, 7.8 req/s
concurrency 5: 60/60 resolved, 35.1 req/s
concurrency 10: 60/60 resolved, 56.9 req/s
concurrency 20: 200/200 per chunk, six chunks, 110 req/s sustainedA tenth of a second, maybe two. In a multi-second answer, that’s real but manageable.
So, if the costs for solving these tokens are minimal, why did Google bother to put this feature in place?
The answer is that Google is now a synchronous dependency inside a competitor’s live product. Every user question that reaches Google’s index now needs a set of real-time callbacks to google.com before the answering system can read a single page.
That gives Google three things it didn’t have before.
First, a throttle it controls, where the visible symptom is someone else’s product getting slow.
Second, a live view of how many results each consumer dereferences and when, which is a decent proxy for their query volume.
Third, the machinery to apply both selectively, since the classifier that decides who gets wrapped markup is the same one that could decide who gets rate-limited.
This means more Google control over a certain type of third-party services, but not its direct competitors in the LLM space.
Claude’s web search appears to run on Brave Search. Anthropic has never said so publicly, but Brave Search has been on its published subprocessor list since March 2025 and is still there in July 2026, the search function carries a BraveSearchParams parameter, and the citations match what Brave returns for the same query. ChatGPT is more mixed. It launched on Bing, then built its own crawler and index, and now runs a blended stack with Bing still contributing alongside third-party providers.
Neither of them reads a Google results page, so they’re not touched by this deploy.
That narrows the target a lot. The companies with real exposure are the ones whose whole product is Google’s index resold with low latency, the SERP API vendors that sit between Google and everyone building on top of search. They can’t route around this like an assistant with its own index, because Google’s index is what they sell. Their customers want results in a second or two, which puts the resolution stage inside a latency budget set by someone else.
The most prominent vendor in the reachable group is the one Google currently has in court.
As a Google spokesperson said to Seroundtable.com: “We have a long history of deploying technical measures against evolving forms of abuse, and we regularly take steps to protect our services and users.” No target, no timetable, no documentation, and no acknowledgment that two clients asking the same question get different pages.
None of this is a rule anyone has to enforce, and that’s what makes it durable. A block can be litigated, and Google already lost once, on the grounds that reading a page counts as circumvention. Serving valid HTML with one field removed to clients you have classified isn’t a refusal of service, and there’s nothing to appeal against.
The cost for this particular field is actually small, and that’s important to keep in mind. What matters isn’t the 295 requests. It’s that Google can now separate the two groups well enough to send them different markup, quietly, with no announcement and no error in your logs. The field it removed this time is cheap to buy back with a GET. Nothing about the mechanism says the next one will be.



