Scrape Google Search in Node.js Without Puppeteer (No Headless Chrome, No Proxies)

By Serpent API Team · · 11 min read

You want Google search results inside a Node.js app. Maybe a rank tracker, a price monitor, or live grounding for an AI agent.

So you reach for the obvious tool: fetch("https://www.google.com/search?q=..."). You run it. And you get back garbage.

The "obvious" fix is Puppeteer. But Puppeteer drags in a bundled Chromium, eats memory, and forces you into a proxy pool and an endless anti-bot cat-and-mouse game. That is a lot of moving parts for some search results.

This guide shows you the lighter path: one HTTP call to a SERP API that returns clean JSON — no headless Chrome, no proxy pool. We will start with the naive attempt, prove exactly why it fails, then give you complete, copy-paste Node examples using native fetch, axios, and an async batch loop.

TL;DR: A raw fetch/axios request to google.com/search returns a JavaScript-rendered, often-blocked shell. Puppeteer and Playwright fix that but add Chromium, memory, proxies, and constant maintenance. The lightweight alternative is a SERP API: fetch("https://api.apiserpent.com/api/search?q=...") with an X-API-Key header returns structured JSON with up to 100 organic results in one call. No browser to run, no proxies to rotate.

The naive way: fetch straight at Google (and why it fails)

A direct HTTP request to Google's search page does not give you usable results. It returns an incomplete, JavaScript-rendered HTML shell, and after a handful of requests Google blocks your server's IP outright.

Here is the attempt almost everyone tries first:

// naive.js — Node 18+
const res = await fetch(
  "https://www.google.com/search?q=best+running+shoes",
  { headers: { "User-Agent": "Mozilla/5.0" } }
);
const html = await res.text();
console.log(html.length, "bytes");
// Now try to find result titles in `html`... good luck.

Run this and you hit three walls at once.

1. The HTML is a shell. Modern Google search is rendered by JavaScript in the browser. The raw HTML you download contains scripts and placeholders, not a tidy list of result links. Libraries like Cheerio parse static HTML fine — but there is no static result list in there to parse.

2. The markup is hostile. Whatever fragments you do find use obfuscated, auto-generated class names that change without warning. A selector that works today silently returns undefined next week. Selectors are why DIY scrapers break at 3am.

3. You get blocked. Google detects automated traffic by IP and behaviour. From a datacenter or cloud IP you will see CAPTCHAs, 429 responses, or consent walls within a few requests. Datacenter proxies get banned fast, which is why people graduate to residential proxy pools — another bill and another thing to manage.

The takeaway: fetch and axios are perfect HTTP clients, but Google search is not a plain HTML document you can request. Cheerio-on-axios works for blogs and docs; it does not work here.

Why Puppeteer and Playwright are heavier than they look

Puppeteer and Playwright solve the rendering problem — by running a full Chrome — which is exactly why they are expensive to operate. They drive a real browser, so the JavaScript runs and the page fills in. That works. The cost shows up everywhere else.

Chromium ships with it. Installing Puppeteer pulls down a bundled Chromium binary. On a platform like Vercel, a bundled serverless function cannot exceed 50 MB, which a headless browser blows past immediately.

Memory is not "headless light." A headless Chrome instance that starts around 200 MB can climb past 1 GB after enough navigations, because Chrome does not release memory cleanly. On a budget VPS or container, that is a crash waiting to happen.

You still need proxies. A real browser does not exempt you from Google's IP detection. To run at any volume you bolt on a residential proxy pool, plus fingerprint and stealth tweaks, and you maintain those forever as Google adapts.

It breaks on markup changes. Even with a real browser, you are still writing CSS selectors against Google's obfuscated DOM. When Google reshuffles class names, your extraction layer breaks and you debug it instead of shipping features.

Puppeteer is the right tool for many automation jobs. For "I just need Google results as data," it is a heavy answer to a small question. Here is the full API-vs-scraping trade-off.

The lightweight way: one fetch, clean JSON

A SERP API turns the whole problem into a single HTTP request that returns structured JSON. You send a query, you get back parsed organic results, ads, People Also Ask, related searches, AI Overviews, and more — with no proxy pool or headless browser to manage. The API handles access for you.

Serpent's Google SERP API is built exactly for Node backends. One endpoint, one header, JSON out. Here is the shape of a format=full response so you know what you are mapping over:

{
  "success": true,
  "query": "best running shoes",
  "engine": "google",
  "country": "us",
  "pagesScraped": 1,
  "results": {
    "organic": [
      {
        "position": 1,
        "title": "The 12 Best Running Shoes of 2026",
        "url": "https://example.com/best-running-shoes",
        "displayedUrl": "example.com › running",
        "snippet": "Our running experts tested 60+ pairs...",
        "sitelinks": []
      }
    ],
    "ads": { "top": [], "bottom": [], "totalCount": 0 },
    "peopleAlsoAsk": [],
    "relatedSearches": [],
    "featuredSnippet": null,
    "aiOverview": null
  },
  "metadata": {}
}

No HTML, no selectors, no undefined surprises. You read results.organic and move on. Let's write the code.

Example 1: native fetch (Node 18+)

On any modern Node runtime you do not need a library at all — global fetch is built in. It shipped in Node 18 and became stable in Node 21, built on the undici HTTP client. No require, no npm install.

Authenticate with the X-API-Key header (the preferred method — the old ?api_key= query param is deprecated):

// search.mjs — Node 18+, zero dependencies
const API_KEY = process.env.SERPENT_API_KEY;

async function searchGoogle(query) {
  const url = new URL("https://api.apiserpent.com/api/search");
  url.searchParams.set("q", query);
  url.searchParams.set("engine", "google");
  url.searchParams.set("country", "us");
  url.searchParams.set("num", "20"); // up to 100 in one call

  const res = await fetch(url, {
    headers: { "X-API-Key": API_KEY }
  });

  if (!res.ok) {
    throw new Error(`Serpent API returned ${res.status}`);
  }

  const data = await res.json();
  return data.results.organic;
}

const results = await searchGoogle("best running shoes");
for (const r of results) {
  console.log(`#${r.position}  ${r.title}`);
  console.log(`         ${r.url}\n`);
}

Run it with SERPENT_API_KEY=sk_live_your_key node search.mjs. You get a clean, numbered list of ranked results. That is the entire scraper.

Want to try it right now? Grab a free API key (10 free Google searches, no card) and paste the snippet above. Or experiment live in the interactive playground before you write a line of code.

Example 2: axios with params and error handling

If your project already uses axios, the pattern is the same with a cleaner params object and tidier error handling. Axios builds the query string for you and throws on non-2xx responses, so a try/catch covers both network and API errors.

// search-axios.js
const axios = require("axios");

const client = axios.create({
  baseURL: "https://api.apiserpent.com",
  headers: { "X-API-Key": process.env.SERPENT_API_KEY },
  timeout: 30000
});

async function searchGoogle(query, opts = {}) {
  try {
    const { data } = await client.get("/api/search", {
      params: {
        q: query,
        engine: opts.engine || "google",
        country: opts.country || "us",
        language: opts.language || "en",
        num: opts.num || 10
      }
    });
    return data.results.organic;
  } catch (err) {
    if (err.response) {
      // API responded with an error status
      console.error(`API error ${err.response.status}:`, err.response.data);
    } else {
      // network / timeout
      console.error("Request failed:", err.message);
    }
    throw err;
  }
}

(async () => {
  const results = await searchGoogle("mesothelioma lawyer", { num: 20 });
  console.log(results.map(r => r.title));
})();

Note the engine param. Swap google for bing, yahoo, or ddg and the exact same code scrapes Bing, Yahoo, or DuckDuckGo. One integration, four engines.

Example 3: async batch with a Promise pool

Tracking many keywords means many requests, so run them concurrently — but with a cap so you do not fire 500 calls at once. A small Promise pool keeps a fixed number of requests in flight and feeds new ones in as others finish.

// batch.mjs — Node 18+
const API_KEY = process.env.SERPENT_API_KEY;

async function fetchOne(query) {
  const url = new URL("https://api.apiserpent.com/api/search");
  url.searchParams.set("q", query);
  url.searchParams.set("num", "10");
  try {
    const res = await fetch(url, { headers: { "X-API-Key": API_KEY } });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data = await res.json();
    const top = data.results.organic[0];
    return { query, topResult: top ? top.url : null };
  } catch (err) {
    return { query, error: err.message };
  }
}

// Run `tasks` with at most `limit` in flight at once.
async function pool(items, limit, worker) {
  const results = [];
  const executing = new Set();
  for (const item of items) {
    const p = Promise.resolve().then(() => worker(item));
    results.push(p);
    executing.add(p);
    p.finally(() => executing.delete(p));
    if (executing.size >= limit) await Promise.race(executing);
  }
  return Promise.all(results);
}

const keywords = [
  "best running shoes", "wireless earbuds", "standing desk",
  "mechanical keyboard", "4k monitor", "ergonomic chair"
];

const out = await pool(keywords, 3, fetchOne); // 3 concurrent
console.table(out);

The per-item try/catch means one failed keyword never sinks the whole batch — you get a result object for every query. Tune the concurrency limit to your plan; running SERP APIs at scale covers larger workloads and rate-limit strategy.

Want to go further? Use this exact pattern to build a rank tracker or feed live search into an MCP server for Claude or Cursor.

Three approaches compared

For pulling Google results into Node, a SERP API wins on setup time, reliability, and maintenance; a headless browser wins only when you genuinely need to drive a page.

Factor Raw fetch / axios Puppeteer / Playwright SERP API (Serpent)
Gets usable resultsNo (JS shell)YesYes
Bundled ChromiumNone~Hundreds of MBNone
RAM per requestTiny200 MB → 1 GB+Tiny
Proxy pool neededYes, to avoid blocksYes, to avoid blocksNo — handled for you
Breaks on markup changeConstantlyConstantlyNo (clean JSON contract)
Results per callWhatever you parseWhatever you parseUp to 100
Setup timeHours, then fights blocksDays + infraOne fetch call

On price, Serpent is flat per call: $0.60 per 10,000 Google searches pay-as-you-go, dropping to $0.06 / 10K and then $0.03 / 10K at higher deposit tiers. Crucially, page depth does not multiply the price — a 100-result deep search costs the same as a 10-result one. You also get 10 free Google searches on signup with no subscription. See full pricing, and compare against the true cost of running your own scraper (servers, proxies, and your time).

How this differs from the Next.js guide

This is general server-side Node.js; our Next.js tutorial is framework-specific. If you searched for "scrape Google in Node," you almost certainly want what is on this page: plain functions that run anywhere Node runs.

The Next.js post builds a full product — App Router server actions, a caching layer, interactive charts, and a responsive dashboard UI. Great if you are shipping a Next.js app.

The fetch and axios snippets here have no framework assumptions. Drop them into a one-off script, an Express route, a BullMQ worker, a cron job, or an AWS Lambda. They make the same API calls — this guide just keeps them framework-free.

Either way the API contract is identical, so you can prototype with a plain script today and lift the same function into Next.js, a LangChain agent, or a real-time RAG pipeline tomorrow. Prefer another language? We also have a Python guide and a PHP guide.

Scrape Google in Node with one fetch call

Skip the headless Chrome and the proxy pool. Serpent returns clean JSON — up to 100 results per call — for any of Google, Bing, Yahoo, or DuckDuckGo. Start with 10 free Google searches, then pay as little as $0.03 / 10K. No subscription.

Get Your Free API Key

Explore: Google SERP API · Docs · Pricing

FAQ

Can I scrape Google search with just fetch or axios in Node.js?

Not reliably. A raw fetch or axios request to google.com/search returns a JavaScript-rendered shell with little usable data, and Google blocks the request by IP after a few tries. You need a rendering engine or a SERP API that returns clean JSON.

Do I need Puppeteer or Playwright to get Google results in Node?

No. Puppeteer and Playwright work but bundle Chromium, use lots of memory, and need a proxy pool to avoid blocks. A SERP API like Serpent gives you the same data with one fetch call and no headless browser to run.

Is native fetch available in Node.js?

Yes. Global fetch shipped in Node 18 and became stable in Node 21, built on the undici HTTP client. You can call fetch() directly with no import on any modern Node runtime, including serverless functions.

How do I get more than 10 Google results in one Node request?

Send num=100 (rounded up to the nearest 10) to GET /api/search on Serpent. One call returns up to 100 organic results, and page depth does not multiply the price, so a 100-result search costs the same as a 10-result one.

How much does the Serpent SERP API cost?

Google search is $0.60 per 10,000 calls pay-as-you-go, dropping to $0.06 then $0.03 per 10K at higher deposit tiers. You get 10 free Google searches on signup, there is no subscription, and the minimum deposit is $10.

How is this different from your Next.js tutorial?

The Next.js guide builds a full framework dashboard with caching and charts. This guide is plain server-side Node.js, no framework required. The fetch and axios patterns here work in any Node script, Express server, worker, cron job, or Lambda.