Skip to content

Proxy Rotation Guide: Scale Your Scraping Without Getting Blocked

Here’s a fact: 90% of scraping blocks are IP-related. Rate limits, blacklists, geographic restrictions — they all target your IP address.

Proxy rotation is the solution. But here’s what nobody tells you: random rotation often makes things worse.

Real humans don’t change IP addresses every request. Sites detect unnatural patterns. The key is rotating intelligently — knowing when to rotate, how often, and which proxies to use.

This guide covers everything from basic setup to production-grade rotation systems.

The Proxy Market in 2026

Look, proxies aren’t niche anymore. This is a $1 billion+ industry growing fast.

MetricValueSource
Proxy services market ( 2026)$1 billion → $1.8B by 2033Market Growth Reports
Data center proxy market$2.8B (2026) → $5.53B by 2033Business Research Insights
Residential proxy market$440M ( 2026) → $1.11B by 2033Business Research Insights
Active residential IPs5.5+ millionbestproxyreviews.com
Active datacenter IPs4+ millionbestproxyreviews.com
Pages scraped monthly2.5+ billionMarket Analysis

Market distribution: Residential proxies (44% of traffic), Datacenter (39%), Mobile (17%). But datacenter proxies dominate by revenue (~60% market share) because of volume pricing.

Why this matters: If you’re scraping at scale without proxies, you’re doing it wrong. The infrastructure exists. The costs have dropped. It’s now cheaper to rotate proxies than to deal with blocks.

Why Rotate Proxies?

The Blocking Problem

When you scrape from a single IP:

Request 1 → 200 OK
Request 2 → 200 OK
...
Request 100 → 200 OK
Request 101 → 429 Too Many Requests
Request 102 → 403 Blocked

Sites track:

  • Request volume per IP
  • Request patterns and timing
  • Geographic impossibilities
  • Known proxy/datacenter IPs

Rotation Benefits

Without RotationWith Rotation
~100 requests before blockThousands of requests
Single geographic locationMultiple locations
Easy to blacklistHard to block
Session trackingSession isolation

Rotation Strategies

Not all rotation is equal. Here are the main approaches:

Strategy 1: Per-Request Rotation

New IP for every request. Simple but often problematic.

// Per-request rotation
for (const url of urls) {
const proxy = getNextProxy(); // Different proxy each time
await scrapeWithProxy(url, proxy);
}

Best for:

  • High-volume, stateless scraping
  • Sites without session requirements
  • API endpoints

Problems:

  • Breaks session cookies
  • Looks unnatural
  • Higher costs

Strategy 2: Session-Based Rotation

Keep the same IP for a “session” of related requests.

// Session-based rotation
const SESSION_SIZE = 50; // Requests per session
let requestCount = 0;
let currentProxy = getNextProxy();
for (const url of urls) {
if (requestCount >= SESSION_SIZE) {
currentProxy = getNextProxy();
requestCount = 0;
}
await scrapeWithProxy(url, currentProxy);
requestCount++;
}

Best for:

  • Most web scraping
  • Sites with cookies/sessions
  • Natural browsing simulation

Strategy 3: Sticky Sessions

Same IP for extended periods (hours or days).

// Sticky session (provider-side)
const proxy = {
host: 'gate.smartproxy.com',
port: 10000,
username: 'user-session-abc123', // Session ID in username
password: 'password',
};
// All requests through this proxy get the same IP
// for the session duration (usually 10-30 minutes)

Best for:

  • Account management
  • Multi-page workflows
  • Login/checkout flows

Strategy 4: Geographic Rotation

Rotate through specific locations.

const geoProxies = {
us: [/* US proxies */],
uk: [/* UK proxies */],
de: [/* German proxies */],
};
async function scrapeByCountry(url: string, country: string) {
const proxies = geoProxies[country];
const proxy = proxies[Math.floor(Math.random() * proxies.length)];
return scrapeWithProxy(url, proxy);
}
// Scrape US prices
await scrapeByCountry('https://amazon.com/product', 'us');
// Scrape UK prices
await scrapeByCountry('https://amazon.co.uk/product', 'uk');

Best for:

  • Price comparison
  • Geo-restricted content
  • Localized search results

Building a Proxy Rotation System

Basic Proxy Pool

interface Proxy {
host: string;
port: number;
username?: string;
password?: string;
protocol: 'http' | 'https' | 'socks5';
}
class ProxyPool {
private proxies: Proxy[] = [];
private currentIndex = 0;
private failureCounts = new Map<string, number>();
constructor(proxies: Proxy[]) {
this.proxies = proxies;
}
getProxyKey(proxy: Proxy): string {
return `${proxy.host}:${proxy.port}`;
}
getNext(): Proxy {
// Skip proxies with too many failures
let attempts = 0;
while (attempts < this.proxies.length) {
const proxy = this.proxies[this.currentIndex];
this.currentIndex = (this.currentIndex + 1) % this.proxies.length;
const failures = this.failureCounts.get(this.getProxyKey(proxy)) || 0;
if (failures < 3) {
return proxy;
}
attempts++;
}
// All proxies have failures, reset and return any
this.failureCounts.clear();
return this.proxies[0];
}
reportSuccess(proxy: Proxy): void {
const key = this.getProxyKey(proxy);
this.failureCounts.delete(key);
}
reportFailure(proxy: Proxy): void {
const key = this.getProxyKey(proxy);
const current = this.failureCounts.get(key) || 0;
this.failureCounts.set(key, current + 1);
}
}

Session-Aware Rotation

class SessionProxyManager {
private pool: ProxyPool;
private sessions = new Map<string, { proxy: Proxy; requestCount: number }>();
private sessionLimit: number;
constructor(proxies: Proxy[], sessionLimit: number = 50) {
this.pool = new ProxyPool(proxies);
this.sessionLimit = sessionLimit;
}
getProxyForSession(sessionId: string): Proxy {
let session = this.sessions.get(sessionId);
// Create new session if needed
if (!session || session.requestCount >= this.sessionLimit) {
session = {
proxy: this.pool.getNext(),
requestCount: 0,
};
this.sessions.set(sessionId, session);
}
session.requestCount++;
return session.proxy;
}
endSession(sessionId: string): void {
this.sessions.delete(sessionId);
}
reportResult(proxy: Proxy, success: boolean): void {
if (success) {
this.pool.reportSuccess(proxy);
} else {
this.pool.reportFailure(proxy);
}
}
}
// Usage
const manager = new SessionProxyManager(proxies, 50);
async function scrapeWithSession(urls: string[], sessionId: string) {
for (const url of urls) {
const proxy = manager.getProxyForSession(sessionId);
try {
await scrapeWithProxy(url, proxy);
manager.reportResult(proxy, true);
} catch (error) {
manager.reportResult(proxy, false);
// Retry with new session
manager.endSession(sessionId);
}
}
}

Integrating with GoLogin

GoLogin profiles can use any proxy. Here’s how to combine rotation with fingerprint management:

Per-Profile Proxy Assignment

import { GoLogin } from '@gologin/core';
async function createRotatingProfiles(
count: number,
proxyPool: ProxyPool
): Promise<GoLogin[]> {
const profiles: GoLogin[] = [];
for (let i = 0; i < count; i++) {
const proxy = proxyPool.getNext();
const gologin = new GoLogin({
profileName: `scraper-${i}`,
proxy: {
protocol: proxy.protocol,
host: proxy.host,
port: proxy.port,
username: proxy.username,
password: proxy.password,
},
// Match fingerprint to proxy location
fingerprintOptions: {
platform: 'windows',
locale: 'en-US',
timezone: await getTimezoneForIP(proxy.host),
},
});
profiles.push(gologin);
}
return profiles;
}

Dynamic Proxy Rotation

class RotatingGoLogin {
private gologin: GoLogin;
private proxyManager: SessionProxyManager;
private sessionId: string;
constructor(profileName: string, proxyManager: SessionProxyManager) {
this.proxyManager = proxyManager;
this.sessionId = `${profileName}-${Date.now()}`;
const proxy = proxyManager.getProxyForSession(this.sessionId);
this.gologin = new GoLogin({
profileName,
proxy: {
protocol: 'http',
host: proxy.host,
port: proxy.port,
username: proxy.username,
password: proxy.password,
},
});
}
async start() {
return this.gologin.start();
}
async rotateProxy(): Promise<void> {
// End current session and get new proxy
this.proxyManager.endSession(this.sessionId);
this.sessionId = `${this.gologin.getProfile()?.name}-${Date.now()}`;
const newProxy = this.proxyManager.getProxyForSession(this.sessionId);
// Stop current browser
await this.gologin.stop();
// Recreate with new proxy
this.gologin = new GoLogin({
profileName: this.gologin.getProfile()?.name || 'default',
proxy: {
protocol: 'http',
host: newProxy.host,
port: newProxy.port,
username: newProxy.username,
password: newProxy.password,
},
});
}
async stop() {
await this.gologin.stop();
this.proxyManager.endSession(this.sessionId);
}
}

Provider-Specific Integration

Bright Data (Luminati)

// Rotating residential
const brightDataRotating = {
host: 'brd.superproxy.io',
port: 22225,
username: 'brd-customer-CUSTOMER-zone-ZONE',
password: 'PASSWORD',
};
// Sticky session (10 minute)
const brightDataSticky = {
host: 'brd.superproxy.io',
port: 22225,
username: 'brd-customer-CUSTOMER-zone-ZONE-session-SESSION123',
password: 'PASSWORD',
};
// Geographic targeting
const brightDataGeo = {
host: 'brd.superproxy.io',
port: 22225,
username: 'brd-customer-CUSTOMER-zone-ZONE-country-us-state-ny-city-new_york',
password: 'PASSWORD',
};

Smartproxy

// Rotating residential
const smartproxyRotating = {
host: 'gate.smartproxy.com',
port: 10000,
username: 'USER',
password: 'PASS',
};
// Sticky session
const smartproxySticky = {
host: 'gate.smartproxy.com',
port: 10001, // Different port for sticky
username: 'USER-session-SESSION123',
password: 'PASS',
};
// Country targeting
const smartproxyUS = {
host: 'us.smartproxy.com',
port: 10000,
username: 'USER',
password: 'PASS',
};

Oxylabs

// Rotating residential
const oxylabsRotating = {
host: 'pr.oxylabs.io',
port: 7777,
username: 'USER',
password: 'PASS',
};
// Country targeting
const oxylabsUS = {
host: 'pr.oxylabs.io',
port: 7777,
username: 'USER-country-us',
password: 'PASS',
};
// City targeting
const oxylabsNYC = {
host: 'pr.oxylabs.io',
port: 7777,
username: 'USER-country-us-city-new_york',
password: 'PASS',
};

Error Handling and Recovery

Retry Logic with Rotation

async function scrapeWithRetry(
url: string,
manager: SessionProxyManager,
maxRetries: number = 3
): Promise<any> {
let lastError: Error | null = null;
let sessionId = `scrape-${Date.now()}`;
for (let attempt = 0; attempt < maxRetries; attempt++) {
const proxy = manager.getProxyForSession(sessionId);
try {
const result = await scrapeWithProxy(url, proxy);
manager.reportResult(proxy, true);
return result;
} catch (error) {
lastError = error;
manager.reportResult(proxy, false);
// Force new session on failure
manager.endSession(sessionId);
sessionId = `scrape-${Date.now()}-retry-${attempt}`;
// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
await new Promise(r => setTimeout(r, delay));
}
}
throw lastError;
}

Health Monitoring

class ProxyHealthMonitor {
private stats = new Map<string, {
successes: number;
failures: number;
lastCheck: Date;
avgLatency: number;
}>();
recordResult(proxy: Proxy, success: boolean, latency: number): void {
const key = `${proxy.host}:${proxy.port}`;
const current = this.stats.get(key) || {
successes: 0,
failures: 0,
lastCheck: new Date(),
avgLatency: 0,
};
if (success) {
current.successes++;
current.avgLatency = (current.avgLatency * (current.successes - 1) + latency) / current.successes;
} else {
current.failures++;
}
current.lastCheck = new Date();
this.stats.set(key, current);
}
getHealthScore(proxy: Proxy): number {
const key = `${proxy.host}:${proxy.port}`;
const stats = this.stats.get(key);
if (!stats) return 1; // Unknown proxy, assume healthy
const total = stats.successes + stats.failures;
if (total === 0) return 1;
return stats.successes / total;
}
getHealthyProxies(proxies: Proxy[], threshold: number = 0.7): Proxy[] {
return proxies.filter(p => this.getHealthScore(p) >= threshold);
}
generateReport(): void {
console.log('\n=== Proxy Health Report ===\n');
for (const [key, stats] of this.stats) {
const total = stats.successes + stats.failures;
const successRate = total > 0 ? (stats.successes / total * 100).toFixed(1) : 'N/A';
console.log(`${key}:`);
console.log(` Success Rate: ${successRate}%`);
console.log(` Total Requests: ${total}`);
console.log(` Avg Latency: ${stats.avgLatency.toFixed(0)}ms`);
console.log(` Last Check: ${stats.lastCheck.toISOString()}`);
console.log('');
}
}
}

Production Best Practices

Rate Limiting Per Proxy

class RateLimitedProxyPool extends ProxyPool {
private rateLimits = new Map<string, { count: number; resetTime: number }>();
private maxRequestsPerMinute: number;
constructor(proxies: Proxy[], maxRequestsPerMinute: number = 30) {
super(proxies);
this.maxRequestsPerMinute = maxRequestsPerMinute;
}
async getNextWithRateLimit(): Promise<Proxy> {
const now = Date.now();
while (true) {
const proxy = this.getNext();
const key = this.getProxyKey(proxy);
const limit = this.rateLimits.get(key);
// Reset if minute has passed
if (!limit || now > limit.resetTime) {
this.rateLimits.set(key, {
count: 1,
resetTime: now + 60000,
});
return proxy;
}
// Check if under limit
if (limit.count < this.maxRequestsPerMinute) {
limit.count++;
return proxy;
}
// Wait for reset or try another proxy
const waitTime = limit.resetTime - now;
if (waitTime < 1000) {
await new Promise(r => setTimeout(r, waitTime));
limit.count = 1;
limit.resetTime = Date.now() + 60000;
return proxy;
}
// Try next proxy in pool
}
}
}

Proxy Warm-Up

async function warmUpProxy(proxy: Proxy): Promise<boolean> {
const gologin = new GoLogin({
profileName: `warmup-${Date.now()}`,
proxy: {
protocol: 'http',
...proxy,
},
});
try {
const { browserWSEndpoint } = await gologin.start();
const browser = await puppeteer.connect({ browserWSEndpoint });
const page = await browser.newPage();
// Visit some normal sites
await page.goto('https://www.google.com');
await page.waitForTimeout(2000);
await page.goto('https://www.youtube.com');
await page.waitForTimeout(3000);
await browser.close();
await gologin.stop();
return true;
} catch (error) {
console.error(`Warm-up failed for ${proxy.host}:`, error);
return false;
}
}
// Warm up all proxies before production use
async function warmUpPool(proxies: Proxy[]): Promise<Proxy[]> {
const results = await Promise.all(
proxies.map(async proxy => ({
proxy,
success: await warmUpProxy(proxy),
}))
);
return results
.filter(r => r.success)
.map(r => r.proxy);
}

Cost Optimization

Proxy Selection by Task

type TaskType = 'search' | 'product' | 'checkout' | 'account';
interface ProxyPools {
datacenter: ProxyPool; // Cheapest
residential: ProxyPool; // Medium
isp: ProxyPool; // Premium
}
function getPoolForTask(pools: ProxyPools, task: TaskType): ProxyPool {
switch (task) {
case 'search':
// Search results are less protected
return pools.datacenter;
case 'product':
// Product pages need residential
return pools.residential;
case 'checkout':
case 'account':
// High-value actions need ISP
return pools.isp;
default:
return pools.residential;
}
}

Bandwidth Optimization

// Block unnecessary resources to save bandwidth
const blockedResourceTypes = [
'image',
'stylesheet',
'font',
'media',
];
const blockedDomains = [
'google-analytics.com',
'facebook.com',
'twitter.com',
'doubleclick.net',
];
async function setupBandwidthOptimization(page: Page): Promise<void> {
await page.setRequestInterception(true);
page.on('request', (request) => {
const resourceType = request.resourceType();
const url = request.url();
if (blockedResourceTypes.includes(resourceType)) {
request.abort();
return;
}
if (blockedDomains.some(domain => url.includes(domain))) {
request.abort();
return;
}
request.continue();
});
}

Frequently Asked Questions

How many proxies do I need for scraping?

Depends on your scale and rotation strategy:

Small scale (< 10,000 pages/day):

  • 5-10 proxies with session-based rotation
  • Cost: ~$50-100/month
  • Good for: Single-site scraping, product monitoring

Medium scale (10,000-100,000 pages/day):

  • 50-100 proxies rotating every 50-100 requests
  • Cost: ~$500-1,000/month
  • Good for: Multi-site scraping, market research

Large scale (100,000+ pages/day):

  • Rotating proxy service (unlimited pool)
  • Cost: $1,000-5,000/month
  • Good for: Enterprise scraping, price comparison

The math: If you scrape 100,000 pages/day and rotate every 100 requests, you need 1,000 unique IPs rotating throughout the day. Use a residential proxy service with automatic rotation rather than managing individual proxies.

Should I rotate every request or use session-based rotation?

Session-based wins 99% of the time. Here’s why:

Real humans don’t change IP addresses between page loads. A person browsing Amazon doesn’t magically relocate from New York to California between clicking a product and viewing it.

Per-request rotation:

  • Breaks cookie sessions
  • Triggers bot detection (unnatural behavior)
  • More expensive (more bandwidth)
  • Works for: Stateless API scraping only

Session-based rotation (50-100 requests/IP):

  • Maintains cookies and sessions
  • Mimics real user behavior
  • More cost-effective
  • Works for: 99% of web scraping

Exception: If you’re hitting pure REST APIs with no state tracking, per-request rotation is fine.

What’s the difference between rotating and sticky sessions?

Rotating proxies: You get a new IP with each request (or on a schedule).

// Bright Data rotating
username: 'brd-customer-CUSTOMER-zone-ZONE'
// Every request = different IP

Sticky sessions: Same IP for a duration (10-30 minutes).

// Bright Data sticky
username: 'brd-customer-CUSTOMER-zone-ZONE-session-ABC123'
// Same IP for all requests with sessionkey "ABC123"

When to use sticky:

  • Multi-page workflows (search → product → cart)
  • Login/account management
  • Forms and checkouts
  • Any time you need cookies to persist

When to use rotating:

  • High-volume scraping
  • Maximum IP diversity
  • When you don’t care about cookies

How do I match proxy location with browser fingerprint?

Critical for avoiding detection. A New York IP with Beijing timezone screams “bot.”

// Get proxy location first
const proxyLocation = await getProxyIPInfo(proxy.host);
// Returns: { city: 'New York', timezone: 'America/New_York', locale: 'en-US' }
// Then match fingerprint
const gologin = new GoLogin({
profileName: 'matched-profile',
proxy: proxy,
fingerprintOptions: {
timezone: proxyLocation.timezone, // ← Must match
locale: proxyLocation.locale, // ← Must match
latitude: proxyLocation.latitude, // ← Must match
longitude: proxyLocation.longitude, // ← Must match
},
});

IP geolocation APIs:

  • ip-api.com (free, 45 req/min)
  • ipinfo.io (free 50k/month)
  • ipapi.co (free 1k/day)

Pro tip: Most residential proxy providers include location in their control panel. Use that data to set fingerprints.

My proxies keep getting blocked. What am I doing wrong?

Common mistakes:

  1. Using datacenter IPs on protected sites

    • Fix: Switch to residential proxies for Amazon, Cloudflare-protected sites
  2. Rotating too fast

    • Fix: Slow down to 50-100 requests per IP
  3. Not rate limiting per proxy

    • Fix: Limit each IP to 10-30 requests/minute
  4. Timezone mismatch

    • Fix: Match fingerprint timezone to proxy location
  5. Same session ID across rotations

    • Fix: Generate new session ID when rotating
  6. Burning IPs with failures

    • Fix: Implement health monitoring, remove bad IPs

Debug checklist:

// 1. Check proxy health
const test = await fetch('https://ipinfo.io', { proxy });
// Should return proxy's IP, not yours
// 2. Check for proxy flags
const info = await fetch('http://ip-api.com/json', { proxy });
// info.proxy should be false
// info.hosting should be false
// 3. Test fingerprint
await page.goto('https://bot.sannysoft.com');
// Should pass all checks

How much do rotating proxies cost?

2024 pricing (per GB or per IP):

Datacenter proxies:

  • Shared: $1-3/IP/month or $0.5-1/GB
  • Dedicated: $5-10/IP/month
  • Best for: High volume, less protected sites

Residential proxies:

  • Traffic-based: $5-15/GB
  • IP-based: $200-500/month for 10 IPs
  • Best for: Protected sites, realistic behavior

ISP (Static Residential):

  • $25-75/IP/month
  • Best for: Account management, long sessions

Mobile proxies:

  • $50-150/IP/month or $10-25/GB
  • Best for: Mobile-specific scraping, highest trust

Cost optimization:

// Use datacenter for low-risk
if (site === 'wikipedia' || site === 'public-api') {
return datacenterProxy; // $0.5/GB
}
// Use residential for medium-risk
if (site === 'amazon' || site === 'ebay') {
return residentialProxy; // $7/GB
}
// Use ISP for high-risk
if (site === 'bank' || site === 'betting') {
return ispProxy; // $50/month/IP
}

Key Takeaways

  1. Session-based rotation beats per-request — Maintain IPs for 50-100 requests like real users.

  2. Match fingerprint to proxy — Timezone, locale, and geolocation must align.

  3. Monitor proxy health — Track success rates and remove failing proxies.

  4. Use the right proxy type — Datacenter for volume, residential for detection, ISP for accounts.

  5. Rate limit per proxy — Don’t burn through IPs with excessive requests.

  6. Implement retry logic — Rotate on failure with exponential backoff.

  7. Optimize for cost — Use cheaper proxies for less protected pages.

Next Steps