Rotating vs Sticky Proxies for Scraping in 2026 (When to Use Each)

By Serpent API Team · · 12 min read

Almost every proxy provider sells the same residential network under two modes — rotating, where you get a fresh exit IP on every request, and sticky, where you hold one IP for a fixed window. Picking the wrong mode is one of the quietest causes of blocked scrapers, and most tutorials never explain the difference.

The choice is not about which mode is better. It is about whether your requests are independent of each other or share a session. Get that one question right and the rest follows.

This guide defines both modes, gives you a rule of thumb you can apply in seconds, then shows the exact code to wire a proxy into Puppeteer and Playwright, rotate a session ID per request, and target a country — all with generic, runnable examples.

TL;DR: Rotate per request for stateless work like SERP queries, where each request is independent — a fresh residential IP each time stops any one address building a suspicious rate. Go sticky for 10–30 minutes for anything that carries state across pages: logins, multi-page pagination, carts, checkouts. With most providers you control the mode through the proxy username — append a random session token for sticky, change or drop it to rotate. Wire it once with --proxy-server + page.authenticate() in Puppeteer, or the proxy: option in Playwright.

Rotating vs sticky proxies, defined

Both modes route your traffic through a provider's pool of residential or datacenter IPs. The difference is how long any one exit IP stays assigned to you, and that single variable changes everything downstream.

ModeExit IP behaviorBest forRisk if misused
RotatingA new IP on (almost) every requestStateless, independent requests — SERP queries, price checks, sitemapsBreaks any flow that needs session continuity
StickyOne IP held for a window, often 10–30 minLogins, multi-page navigation, carts, checkoutsOne IP accumulates a high request rate — easy to flag if overused

Rotating spreads your requests across many addresses, so no single IP looks busy. That is exactly what you want when each request stands alone, because a site cannot tie a hundred queries to one address and conclude they came from a bot.

Sticky does the opposite on purpose: it keeps you on one address so a multi-step interaction looks like one continuous human session. The cost is that the one IP carries all of that session's traffic, so you keep sticky windows short and the per-session request count modest.

The rule of thumb

Here is the whole decision in one sentence: rotate per request when each request is independent; stay sticky when requests share a session. Everything else is a detail.

SERP scraping is the textbook case for per-request rotation. One search query is completely independent of the next — there is no cart, no login, no pagination state that the server is tracking across them. So you want a different residential exit IP for each query, which keeps any one address from racking up a query rate that looks automated. It is also the cleanest way to avoid the rate signatures that get proxy IPs banned on Google, so for search work the answer almost always lands on "rotate every request."

Logins and multi-page flows are the textbook case for sticky. The moment a server issues you a session cookie, it expects the same client — same IP included — to keep using it. If your IP changes between page one and page two, many sites treat that as a stolen session and force a re-login or a challenge. Pagination is the milder version of the same problem: deep result pages for a single query are often consistent only if they come from one IP.

WorkloadRecommended modeWhy
Single SERP queryRotate per requestEach query is independent; spread the load
Deep pagination of one queryShort stickyKeep the result set consistent across pages
Product/price snapshotsRotate per requestStateless lookups, no continuity needed
Add-to-cart / checkout flowSticky 10–30 minServer binds the session to one IP
Logged-in account scrapingSticky (whole session)An IP change looks like a hijack

The 10–30 minute sticky window is a common default because it is long enough to finish most human-scale flows yet short enough that one IP never carries hours of traffic. If your flow finishes in two minutes, let the session expire early; if it needs longer, most providers let you extend the window. For deeper context on which IP types survive Google at all, see residential vs datacenter proxies for SERP scraping.

Wiring a proxy into Puppeteer & Playwright

Before you can rotate anything you need the proxy plumbed into the browser. Both major automation libraries take a proxy at launch; the only wrinkle is that authenticated proxies need credentials supplied separately. Start with Puppeteer, which passes the proxy host as a launch flag and the username/password through page.authenticate():

const puppeteer = require('puppeteer-extra');
const Stealth = require('puppeteer-extra-plugin-stealth');
puppeteer.use(Stealth());

// Use any residential proxy provider (Bright Data, Oxylabs, Decodo,
// IPRoyal, SOAX, NetNut, DataImpulse...). Host and port come from them.
const PROXY_HOST = 'gw.example-proxy.com';
const PROXY_PORT = 7000;
const PROXY_USER = 'your-username';
const PROXY_PASS = 'your-password';

(async () => {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: [
      `--proxy-server=${PROXY_HOST}:${PROXY_PORT}`,
      '--no-sandbox',
      '--disable-blink-features=AutomationControlled',
    ],
  });

  const page = await browser.newPage();
  await page.authenticate({ username: PROXY_USER, password: PROXY_PASS });

  await page.goto('https://httpbin.org/ip', { waitUntil: 'domcontentloaded' });
  console.log(await page.evaluate(() => document.body.innerText)); // shows exit IP
  await browser.close();
})();

The httpbin.org/ip check is the fastest way to confirm the proxy is actually carrying your traffic — it echoes back the exit IP the destination sees, which should be the proxy's, not yours. Always run that probe once before trusting a new proxy config.

Playwright bakes the proxy into a single proxy: option, credentials included, which is a little cleaner:

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({
    proxy: {
      server: 'http://gw.example-proxy.com:7000',
      username: 'your-username',
      password: 'your-password',
    },
  });

  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto('https://httpbin.org/ip', { waitUntil: 'domcontentloaded' });
  console.log(await page.textContent('body')); // shows exit IP
  await browser.close();
})();

In Playwright you can also set the proxy on browser.newContext({ proxy: ... }) instead of at launch, which lets one browser run several contexts on different proxies at once. That is handy when you want some tabs sticky and others rotating in the same process. Either way the network options are documented in the Puppeteer and Playwright proxy guides.

Rotating a session ID per request

Here is the part that trips people up: with most residential providers you do not switch modes through some dashboard toggle — you control it through the proxy username. The gateway host and port stay the same; you encode the session you want by appending a token to the username.

The exact suffix syntax differs per provider, but the shape is almost universal. A username like customer-USER-session-ABC123 tells the gateway "give me whatever IP maps to session ABC123, and keep it for the sticky window." Change ABC123 and you get a different IP; drop the session token entirely and most gateways rotate on every request by default.

So rotating per request is just generating a fresh random session token each time, and going sticky is reusing the same token for the duration of a flow:

const crypto = require('crypto');

const BASE_USER = 'customer-USER';   // your account username, per provider docs
const PROXY_PASS = 'your-password';

// A fresh token = a fresh exit IP. Reuse a token = a sticky IP.
function sessionToken() {
  return crypto.randomBytes(4).toString('hex'); // e.g. "9f3a1c8e"
}

// ROTATING: new token (new IP) for every independent request.
function rotatingUser() {
  return `${BASE_USER}-session-${sessionToken()}`;
}

// STICKY: lock one token for the whole multi-page flow.
function stickyUser(flowId) {
  return `${BASE_USER}-session-${flowId}`;
}

// Per-query SERP scraping: rotate.
async function scrapeQuery(browser, query) {
  const page = await browser.newPage();
  await page.authenticate({ username: rotatingUser(), password: PROXY_PASS });
  await page.goto(`https://example.com/search?q=${encodeURIComponent(query)}`,
    { waitUntil: 'domcontentloaded' });
  // ... parse the DOM ...
  await page.close();
}

// A logged-in flow: one sticky token start to finish.
async function runStickyFlow(browser, flowId) {
  const page = await browser.newPage();
  await page.authenticate({ username: stickyUser(flowId), password: PROXY_PASS });
  await page.goto('https://example.com/login', { waitUntil: 'domcontentloaded' });
  // ... log in, then navigate page after page on the SAME IP ...
  await page.close();
}

Two practical notes. First, page.authenticate() sets credentials per page, so opening a new page with a new username is the cleanest way to swap exit IPs in Puppeteer — you do not need to relaunch the browser. Second, never hard-code real credentials in source; load them from environment variables. The token in the example is random and disposable, which is the whole point.

If your provider rotates by default with no session token, you get per-request rotation for free and only need a token when you want stickiness. Check your provider's username-suffix documentation for the exact keywords (some use sessid, session, sess, or a numeric session field).

Country and geo targeting

Most residential providers let you pin the exit IP to a country, region, or even city — again through username suffixes, on the same gateway host. This matters for SERP work because search engines localize results heavily, so a US query needs a US exit IP to return what a US searcher actually sees.

The generic pattern adds a country field to the username alongside the session token:

const BASE_USER = 'customer-USER';
const PROXY_PASS = 'your-password';

// Compose a username with both a country and a session token.
// Field names ("country", "cc", "region"...) vary by provider — check docs.
function geoUser(countryCode, sessionToken) {
  return `${BASE_USER}-country-${countryCode}-session-${sessionToken}`;
}

// Rotating US exit IPs for localized SERP scraping:
const user = geoUser('us', crypto.randomBytes(4).toString('hex'));
// -> "customer-USER-country-us-session-7b2e9a04"

Combine geo with the rotation rule and you get the full picture: a localized SERP scraper rotates a fresh in-country IP per query, while a localized logged-in flow holds one in-country sticky IP for the session. Mismatched geo is a classic silent bug — you ask for German results but the random exit IP is in Brazil, so you get the wrong localization and never notice. Verify the exit country the same way you verify the IP: with a quick probe before the real run.

Getting localization wrong is also a fast route to blocks, because an IP whose geography contradicts your locale headers looks inconsistent. If you are already seeing rate limits, the patterns in fixing Google's 429 unusual-traffic error and the reputation factors in why proxies get banned by Google go deeper than rotation mode alone.

A bypass list so static hosts skip the proxy

You pay for residential bandwidth by the gigabyte, so routing traffic through the proxy that does not need to be is wasted money. Static assets, your own telemetry endpoints, and well-known CDN hosts can go direct — only the actual target site needs the residential exit. Chromium gives you a launch flag for exactly this:

const browser = await puppeteer.launch({
  headless: 'new',
  args: [
    `--proxy-server=${PROXY_HOST}:${PROXY_PORT}`,
    // Hosts in this list bypass the proxy and connect directly.
    '--proxy-bypass-list=*.gstatic.com;*.cloudfront.net;localhost;127.0.0.1',
    '--no-sandbox',
    '--disable-blink-features=AutomationControlled',
  ],
});

The bypass list is a blunt instrument — it sends those hosts direct from the machine's real IP, so only list hosts whose traffic is not sensitive to your identity (static CDNs, fonts, your own logging). The much bigger lever, though, is not bypassing heavy resources but blocking them entirely: images are the overwhelming majority of a page's weight, and you almost never parse them. The full technique — aborting images, fonts, and media at the request layer — is in how to cut scraping bandwidth by blocking resources, and it pairs perfectly with a tight bypass list to keep your per-GB proxy bill near the floor.

The verdict

Strip away the marketing and the choice is mechanical. Ask one question of every scraping task: do these requests share a session, or is each one independent? Independent means rotate per request. Shared session means hold a sticky IP for the duration, typically 10 to 30 minutes.

For SERP scraping specifically — the most common reason people read a post like this — the answer is almost always per-request rotation with a residential exit pinned to the right country. For anything behind a login or spanning a cart and a checkout, it is sticky, start to finish, paired with a persistent cookie jar.

And the honest meta-point: rotation mode is one knob on a console with many. You still own IP reputation, fingerprinting, retries, bandwidth, and the parser that breaks every time the target ships a redesign. If the goal is search data rather than running a proxy fleet, handing the whole problem to a managed API is often the cheaper path — the same trade-off laid out in the guide to scraping Google for free, where the free routes end and an API begins.

Skip the proxy console entirely.

Serpent's SERP API returns clean JSON from Google, Bing, Yahoo & DuckDuckGo — no proxies, no CAPTCHAs, no parser maintenance. Get 10 free searches on signup, then pay-as-you-go from $0.03 per 10,000 searches at scale, no subscription.

Get Your Free API Key

Explore: SERP API · Pricing · Playground

FAQ

How often should I rotate my proxy IP?

It depends on the workload. For independent, stateless requests like SERP queries, rotate the exit IP on every request so no single address builds up a suspicious request rate. For flows that carry state across pages — a paginated result set, a checkout, anything behind a login — hold one IP for the whole session, typically 10 to 30 minutes, then rotate when the task is done. The rule is simple: rotate per request when each request is independent, stay sticky when requests share a session.

Should I use sticky or rotating proxies for Google?

For a single search query, rotating per request is usually the better default, because each query is independent and a fresh residential IP each time keeps any one address from accumulating a high query rate. If you are paginating deep into one query's results, a short sticky session keeps the pagination consistent. Either way, residential exit IPs matter more than the rotation mode for Google, since datacenter ranges are flagged quickly.

Why do free proxies fail at scraping?

Free and public proxy lists are shared by thousands of people, so their IPs are already rate-limited, blocklisted, or serving CAPTCHAs by the time you find them. They are also slow, frequently offline, and offer no session control, so you cannot reliably rotate per request or hold a sticky IP. For anything beyond a toy test, a paid residential or datacenter provider with proper session management is the only thing that works.

Rotating or sticky for logged-in scraping?

Always sticky. A login binds a session to an IP, and if your address changes mid-session most sites treat that as a hijacked session and force a re-authentication or a challenge. Hold one sticky IP for the entire logged-in flow — log in, navigate, extract — and only rotate when you start a fresh account or session. Pair the sticky IP with a persistent cookie jar so the whole fingerprint stays consistent.