Google's search tracking links are just HTTP 302 redirects wearing a trench coat
Developers needlessly deploy bloated headless browsers to extract Google search links when a raw HTTP request intercepts the 302 redirect in milliseconds.
By Felix Hart
Sparked by In less than a year, Google made scraping 10× harder · discussion

I was reading a Hacker News thread this morning where people were lamenting the sheer amount of computational overhead now required to parse modern search results. The discussion was catalyzed by a recent Growtika study detailing how Google handles outbound routing to monitor clicks and combat automated scraping.
The prevailing consensus in the forum seemed to be that extracting a clean destination URL from a search page now mandates booting up a full Chromium instance via Puppeteer or Playwright. Developers are deploying fleets of gigabyte-heavy containers, chewing through cloud budgets and CPU cycles, just to figure out where a basic hyperlink actually points.
I see a lot of developers assume that because a URL changes dynamically on a page, they have no choice but to spin up a headless browser to untangle it. What's actually happening here is much dumber. Google is just using a standard 1996 HTTP 302 Found redirect, tucked behind a JavaScript event listener.
If you inspect the page behavior in a standard browser, the underlying mechanism is incredibly mundane. The search results initially render with clean, direct URLs in the HTML markup—which is why right-clicking and copying the link often works perfectly.
However, Google attaches an aggressive event listener to the element. Upon mousedown, a quick JavaScript function rapidly swaps out the clean href attribute for a localized tracking endpoint. Clicking the link sends the user to a convoluted /url?q=... path, which logs the interaction on their servers and immediately bounces the client to the final destination.
The industry's default reaction to this minor client-side friction is an escalating anti-pattern. Because the URL changes dynamically upon interaction, developers decide they need to simulate the human interaction—moving a virtual mouse, triggering the event, and waiting for the heavy page to navigate.
The bottom line: underneath that JavaScript event, it's just a standard HTTP 302 redirect. The browser hits that rewritten URL and obediently follows the Location header provided by the server. We absolutely do not need to render cascading stylesheets, execute V8 JavaScript engines, or allocate immense blocks of memory to read a simple network header.
You can prove this without opening a browser at all. If you open your terminal and run a raw curl -I command against one of those Google tracking URLs, you can see exactly what the server is doing:
curl -I "https://google.com/url?q=https://felixhart.net"
It instantly spits out something like this:
HTTP/1.1 302 Found
Location: https://felixhart.net
Cache-Control: private
Content-Type: text/html; charset=UTF-8
The -I flag tells the utility to only fetch the HTTP headers. The command connects to the server and immediately returns that clean Location: string in the terminal output, bypassing the JavaScript entirely. The server has no idea whether the request originated from a heavily orchestrated Puppeteer script or a twenty-year-old command-line utility. It just sees a request for a resource and responds with the redirect sequence.
The napkin math here heavily favors raw network requests. A headless browser takes upwards of a full second just to initialize its environment, establish a WebSocket connection to the debugging port, and prepare the rendering engine. It requires dedicated CPU cores and hundreds of megabytes of RAM per instance. By contrast, a raw HTTP request resolves in tens of milliseconds, consuming kilobytes of memory. You can run thousands of these sequential requests on a cheap virtual private server in the time it takes a single Selenium instance to successfully boot.
Instead of treating URL resolution as a visual rendering problem, we can bypass the overhead by dropping directly into a basic script. Using a lightweight HTTP library, we can intercept that redirect explicitly and extract the final destination without ever downloading a single byte of HTML:
import httpx
def resolve_google_link(tracking_url: str) -> str:
# We explicitly prevent the client from following the redirect
with httpx.Client(follow_redirects=False) as client:
response = client.head(tracking_url)
# The raw HTTP 302 response contains the clean destination
if response.status_code == 302:
return response.headers.get("Location", tracking_url)
return tracking_url
print(resolve_google_link("https://google.com/url?q=https://example.com"))
This fourteen-line snippet takes advantage of the fact that protocol-level requests are significantly faster than Selenium or any equivalent browser automation framework. By issuing a HEAD request rather than a GET request, we ask the server exclusively for the routing metadata. The follow_redirects=False parameter is the essential component here—it forces the client to stop at the exact moment the server hands over the 302 status code and the new destination URL. We catch the bounce before it happens, pulling the underlying URL right out of the network header.
The pervasive instinct to reach for a fully automated web browser the moment data gets slightly tricky is a deeply concerning industry habit. We have normalized deploying massive, memory-hogging abstractions to solve data extraction problems that were elegantly solved at the protocol layer three decades ago.
When you look at the concise httpx snippet above, the absurdity of spinning up a multi-gigabyte container architecture for the exact same string extraction becomes impossible to ignore. We have to actively push back against this kind of architectural bloat ourselves! Before you reach for a gigabyte of RAM and a headless Chromium instance to scrape a link, take a second to look at the network tab in your developer tools. Sometimes you just need to drop down to the protocol layer and curl it yourself.