Residential vs Datacenter
Deep dive into proxy types. Proxy Comparison →
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.
Look, proxies aren’t niche anymore. This is a $1 billion+ industry growing fast.
| Metric | Value | Source |
|---|---|---|
| Proxy services market ( 2026) | $1 billion → $1.8B by 2033 | Market Growth Reports |
| Data center proxy market | $2.8B (2026) → $5.53B by 2033 | Business Research Insights |
| Residential proxy market | $440M ( 2026) → $1.11B by 2033 | Business Research Insights |
| Active residential IPs | 5.5+ million | bestproxyreviews.com |
| Active datacenter IPs | 4+ million | bestproxyreviews.com |
| Pages scraped monthly | 2.5+ billion | Market 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.
When you scrape from a single IP:
Request 1 → 200 OKRequest 2 → 200 OK...Request 100 → 200 OKRequest 101 → 429 Too Many RequestsRequest 102 → 403 BlockedSites track:
| Without Rotation | With Rotation |
|---|---|
| ~100 requests before block | Thousands of requests |
| Single geographic location | Multiple locations |
| Easy to blacklist | Hard to block |
| Session tracking | Session isolation |
Not all rotation is equal. Here are the main approaches:
New IP for every request. Simple but often problematic.
// Per-request rotationfor (const url of urls) { const proxy = getNextProxy(); // Different proxy each time await scrapeWithProxy(url, proxy);}Best for:
Problems:
Keep the same IP for a “session” of related requests.
// Session-based rotationconst 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:
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:
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 pricesawait scrapeByCountry('https://amazon.com/product', 'us');
// Scrape UK pricesawait scrapeByCountry('https://amazon.co.uk/product', 'uk');Best for:
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); }}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); } }}
// Usageconst 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); } }}GoLogin profiles can use any proxy. Here’s how to combine rotation with fingerprint management:
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;}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); }}// Rotating residentialconst 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 targetingconst brightDataGeo = { host: 'brd.superproxy.io', port: 22225, username: 'brd-customer-CUSTOMER-zone-ZONE-country-us-state-ny-city-new_york', password: 'PASSWORD',};// Rotating residentialconst smartproxyRotating = { host: 'gate.smartproxy.com', port: 10000, username: 'USER', password: 'PASS',};
// Sticky sessionconst smartproxySticky = { host: 'gate.smartproxy.com', port: 10001, // Different port for sticky username: 'USER-session-SESSION123', password: 'PASS',};
// Country targetingconst smartproxyUS = { host: 'us.smartproxy.com', port: 10000, username: 'USER', password: 'PASS',};// Rotating residentialconst oxylabsRotating = { host: 'pr.oxylabs.io', port: 7777, username: 'USER', password: 'PASS',};
// Country targetingconst oxylabsUS = { host: 'pr.oxylabs.io', port: 7777, username: 'USER-country-us', password: 'PASS',};
// City targetingconst oxylabsNYC = { host: 'pr.oxylabs.io', port: 7777, username: 'USER-country-us-city-new_york', password: 'PASS',};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;}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(''); } }}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 } }}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 useasync 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);}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; }}// Block unnecessary resources to save bandwidthconst 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(); });}Depends on your scale and rotation strategy:
Small scale (< 10,000 pages/day):
Medium scale (10,000-100,000 pages/day):
Large scale (100,000+ pages/day):
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.
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:
Session-based rotation (50-100 requests/IP):
Exception: If you’re hitting pure REST APIs with no state tracking, per-request rotation is fine.
Rotating proxies: You get a new IP with each request (or on a schedule).
// Bright Data rotatingusername: 'brd-customer-CUSTOMER-zone-ZONE'// Every request = different IPSticky sessions: Same IP for a duration (10-30 minutes).
// Bright Data stickyusername: 'brd-customer-CUSTOMER-zone-ZONE-session-ABC123'// Same IP for all requests with sessionkey "ABC123"When to use sticky:
When to use rotating:
Critical for avoiding detection. A New York IP with Beijing timezone screams “bot.”
// Get proxy location firstconst proxyLocation = await getProxyIPInfo(proxy.host);// Returns: { city: 'New York', timezone: 'America/New_York', locale: 'en-US' }
// Then match fingerprintconst 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:
Pro tip: Most residential proxy providers include location in their control panel. Use that data to set fingerprints.
Common mistakes:
Using datacenter IPs on protected sites
Rotating too fast
Not rate limiting per proxy
Timezone mismatch
Same session ID across rotations
Burning IPs with failures
Debug checklist:
// 1. Check proxy healthconst test = await fetch('https://ipinfo.io', { proxy });// Should return proxy's IP, not yours
// 2. Check for proxy flagsconst info = await fetch('http://ip-api.com/json', { proxy });// info.proxy should be false// info.hosting should be false
// 3. Test fingerprintawait page.goto('https://bot.sannysoft.com');// Should pass all checks2024 pricing (per GB or per IP):
Datacenter proxies:
Residential proxies:
ISP (Static Residential):
Mobile proxies:
Cost optimization:
// Use datacenter for low-riskif (site === 'wikipedia' || site === 'public-api') { return datacenterProxy; // $0.5/GB}
// Use residential for medium-riskif (site === 'amazon' || site === 'ebay') { return residentialProxy; // $7/GB}
// Use ISP for high-riskif (site === 'bank' || site === 'betting') { return ispProxy; // $50/month/IP}Session-based rotation beats per-request — Maintain IPs for 50-100 requests like real users.
Match fingerprint to proxy — Timezone, locale, and geolocation must align.
Monitor proxy health — Track success rates and remove failing proxies.
Use the right proxy type — Datacenter for volume, residential for detection, ISP for accounts.
Rate limit per proxy — Don’t burn through IPs with excessive requests.
Implement retry logic — Rotate on failure with exponential backoff.
Optimize for cost — Use cheaper proxies for less protected pages.
Residential vs Datacenter
Deep dive into proxy types. Proxy Comparison →
Bypass Cloudflare
Combine rotation with anti-detection. Cloudflare Guide →
Multi-Account Setup
Use rotation for account management. Multi-Account →
Proxy Tester Tool
Test your proxies before deploying. Proxy Tester →