Web scraping at scale means dealing with IP blocks. Modern anti-bot systems detect datacenter IPs instantly, throttle residential proxies, and identify patterns in request behavior. The most reliable solution in 2026 is rotating mobile proxies — real 4G/5G IPs that blend in with legitimate mobile traffic.
This guide covers setup, rotation strategies, code examples for Python-based scrapers, and the most common failure modes you'll hit at production scale.
Mobile Proxies for Scraping
Real 4G/5G carrier IPs — Ukraine, Romania, Latvia. Instant rotation via API.
Why Mobile Proxies Beat Datacenter for Scraping
Datacenter proxies are fast and cheap but get blocked easily because:
- Their ASNs (Amazon, DigitalOcean, Hetzner, OVH) are well-known and blocklisted
- IP ranges are small — ban one IP and the whole /24 subnet is suspect
- No CGNAT — each IP maps to one client, easy to identify and rate-limit
- Reverse DNS often resolves to provider hostnames that scream "this is a server"
Mobile carrier IPs work better because:
- They belong to legitimate mobile ASNs (Kyivstar, Orange, T-Mobile, LMT)
- CGNAT means thousands of real users share the same IP pool
- Banning a mobile carrier IP affects real users — sites are reluctant to do it
- Carrier ranges change constantly as IPs rotate between SIM cards
- Reverse DNS resolves to carrier-generic patterns (
mobile.kyivstar.net,dynamic-host.orange.ro) consistent with real subscriber traffic
How IP Rotation Works
ProxyGrow mobile proxies support two rotation methods:
Timer-based rotation: The proxy automatically changes the exit IP every N minutes (minimum 3 minutes on Shared plans, configurable on Premium including no-rotation for stable sessions).
API-based rotation: You call a unique URL to trigger an instant IP change. The IP changes in 3–5 seconds. The proxy credentials (host:port:user:pass) stay the same — only the exit IP changes.
GET https://your-rotation-url.proxygrow.com/rotate/TOKEN
Response: {"ok":true,"new_ip":"176.36.124.42","carrier":"Kyivstar","took_ms":3200}
This is ideal for scrapers: rotate between requests or after a block is detected. The credentials staying constant means you don't have to reconfigure your scraper — only the exit IP changes from the target site's perspective.
When to rotate
Three common patterns:
- Every N requests — simplest, works for most workloads. Tune N based on per-site rate limit observation (50–500 typical).
- On block detection — rotate immediately when you see a 403, 429, or CAPTCHA. Adaptive, doesn't waste rotations when not needed.
- Time-based — rotate every 5–10 minutes regardless of request count. Useful for long-running crawls where request rate varies.
Combine patterns: "every 100 requests OR on block detection, whichever comes first" is the most robust default.
Python: Basic Proxy Setup
requests library
import requests
proxy = {
'http': 'socks5h://user:[email protected]:5000',
'https': 'socks5h://user:[email protected]:5000',
}
response = requests.get('https://example.com', proxies=proxy, timeout=15)
print(response.status_code)
For SOCKS5, install requests[socks]:
pip install requests[socks]
Note the socks5h:// (with h) — this tells the SOCKS client to resolve DNS through the proxy as well. Without it, your local DNS resolver gets every hostname you scrape, leaking your activity and adding latency.
Rotating IP between requests
import requests
import time
PROXY = 'socks5h://user:[email protected]:5000'
ROTATION_URL = 'https://your-rotation-url'
def rotate_ip():
requests.get(ROTATION_URL, timeout=10)
time.sleep(5) # wait for IP change to propagate
def scrape(url):
proxies = {'http': PROXY, 'https': PROXY}
return requests.get(url, proxies=proxies, timeout=15)
urls = ['https://example.com/page/1', 'https://example.com/page/2']
for i, url in enumerate(urls):
if i % 5 == 0 and i > 0:
rotate_ip()
response = scrape(url)
print(response.status_code)
Adaptive rotation on block detection
import requests, time, random
PROXY = {'http': 'socks5h://user:pass@host:port', 'https': 'socks5h://user:pass@host:port'}
ROTATION_URL = 'https://your-rotation-url'
def is_blocked(response):
return (
response.status_code in (403, 429, 503)
or 'captcha' in response.text.lower()
or 'access denied' in response.text.lower()
)
def fetch_with_retry(url, max_retries=3):
for attempt in range(max_retries):
r = requests.get(url, proxies=PROXY, timeout=20)
if not is_blocked(r):
return r
print(f'[blocked on attempt {attempt+1}] rotating IP...')
requests.get(ROTATION_URL, timeout=10)
time.sleep(5 + random.uniform(0, 3))
raise RuntimeError(f'Failed after {max_retries} attempts: {url}')
This pattern doesn't waste rotations on healthy requests but recovers quickly when a block hits.
Playwright: Browser-Based Scraping
Playwright scrapes JavaScript-heavy sites. Combine it with mobile proxies for maximum stealth.
import asyncio
from playwright.async_api import async_playwright
async def scrape_with_playwright():
async with async_playwright() as p:
browser = await p.chromium.launch(
proxy={
'server': 'socks5://proxy.proxygrow.com:5000',
'username': 'your_user',
'password': 'your_pass',
}
)
page = await browser.new_page()
# Set mobile viewport for better fingerprint match
await page.set_viewport_size({'width': 390, 'height': 844})
await page.goto('https://example.com')
content = await page.content()
print(content[:500])
await browser.close()
asyncio.run(scrape_with_playwright())
Tip: Use mobile viewport (390×844 for iPhone 14, 360×800 for typical Android) to match the mobile proxy fingerprint. A desktop viewport with a mobile IP looks inconsistent. For maximum realism, pair the mobile viewport with a mobile User-Agent:
page = await browser.new_page(
user_agent='Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
viewport={'width': 412, 'height': 915},
device_scale_factor=2.625,
is_mobile=True,
has_touch=True,
)
Scrapy: Large-Scale Crawling
For high-volume crawls, Scrapy with a rotating proxy middleware is the standard approach.
Install the middleware:
pip install scrapy-rotating-proxies
settings.py:
ROTATING_PROXY_LIST = [
'socks5://user:[email protected]:5000',
]
DOWNLOADER_MIDDLEWARES = {
'rotating_proxies.middlewares.RotatingProxyMiddleware': 610,
'rotating_proxies.middlewares.BanDetectionMiddleware': 620,
}
ROTATING_PROXY_PAGE_RETRY_TIMES = 3
For a single mobile proxy with API rotation, create a custom middleware:
# middlewares.py
import requests
import time
class MobileProxyRotateMiddleware:
def __init__(self, rotation_url, rotate_every=10):
self.rotation_url = rotation_url
self.rotate_every = rotate_every
self.request_count = 0
def process_request(self, request, spider):
self.request_count += 1
if self.request_count % self.rotate_every == 0:
requests.get(self.rotation_url, timeout=10)
time.sleep(5)
request.meta['proxy'] = 'socks5://user:[email protected]:5000'
def process_response(self, request, response, spider):
if response.status in (403, 429, 503):
requests.get(self.rotation_url, timeout=10)
time.sleep(5)
# Mark request for retry
return request.replace(dont_filter=True)
return response
Anti-Bot Evasion Tips
Respect crawl delays
DOWNLOAD_DELAY = 2 # 2 seconds between requests
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1
AUTOTHROTTLE_MAX_DELAY = 10
AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0
Random delays are better than fixed ones:
import random
time.sleep(random.uniform(1, 4))
A fixed 2.0-second delay produces a perfect cadence that's trivially detectable by frequency analysis. A uniform-random 1–4 second delay looks like a real user.
Set realistic headers
headers = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'uk-UA,uk;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-User': '?1',
'Sec-Fetch-Dest': 'document',
}
Match the User-Agent to your proxy country. An Android Ukrainian UA with a Kyivstar IP is a believable combination. A Mac Safari UA with a Kyivstar 4G IP is not.
Handle blocks gracefully
Detect blocks (403, CAPTCHA page, redirect to anti-bot challenge) and rotate IP:
def is_blocked(response):
if response.status_code in (403, 429):
return True
if 'captcha' in response.text.lower():
return True
if response.url.startswith('https://challenges.cloudflare.com'):
return True
return False
if is_blocked(response):
rotate_ip()
response = scrape(url)
Maintain cookie hygiene
For logged-in scraping, keep cookies bound to the proxy session. Rotating the IP while keeping the same session cookies looks like account hijacking and triggers re-authentication or 2FA on most platforms.
Pattern: one requests.Session() per proxy session, dispose of both on rotation:
session = requests.Session()
session.proxies = PROXY
# ... do scraping ...
if rotation_needed:
rotate_ip()
session = requests.Session() # fresh cookies for the new IP
session.proxies = PROXY
Common Production Failures
Modem went offline mid-crawl — your scraper hangs on TCP connect. Add aggressive connection timeouts (5s) and retry-on-fail. Premium plans include health webhooks so you can detect modem issues out-of-band.
Rotation succeeded but new IP has same /24 prefix — CGNAT pool didn't roll over to a different subnet. Rotate again or wait. Not a problem for most use cases but matters for sites that block at the subnet level.
Cloudflare 1020 error after 100+ successful requests — IP-level reputation drift. Rotate, slow request rate by 50%, add random delays.
Random ECONNRESET errors — mobile carrier NAT timeout (usually 5–15 min idle). Don't keep TCP connections open between requests; close after each.
Speed drops to 1 Mbps after a few hours — modem signal degraded or carrier throttled. Rotate IP to get a fresh CGNAT session; if persistent, request a different modem assignment.
Shared vs Premium for Scraping
| Shared | Premium | |
|---|---|---|
| Rotation speed | 3 min minimum | Instant |
| Simultaneous connections | Shared with others | Unlimited |
| Speed | 10–50 Mbps | up to 150 Mbps |
| pOSfp spoofing | No | Yes |
| Best for | Light scraping, testing | Heavy scraping, production |
For production scrapers making hundreds of requests per hour, Premium plans with instant rotation and unlimited connections are the right choice.
Sample End-to-End Production Pattern
import requests, time, random, logging
from contextlib import contextmanager
PROXY_HOST = 'proxy.proxygrow.com'
PROXY_PORT = 5000
PROXY_USER = 'user'
PROXY_PASS = 'pass'
ROTATION_URL = 'https://your-rotation-url'
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
log = logging.getLogger('scraper')
def proxy_dict():
return {
'http': f'socks5h://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}',
'https': f'socks5h://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}',
}
def rotate_ip():
r = requests.get(ROTATION_URL, timeout=10)
new_ip = r.json().get('new_ip', 'unknown')
log.info(f'rotated to {new_ip}')
time.sleep(4)
def fetch(url, session, max_retries=3):
for attempt in range(max_retries):
try:
r = session.get(url, timeout=20)
if r.status_code == 200 and 'captcha' not in r.text.lower():
return r
log.warning(f'block on {url} (status {r.status_code}), rotating')
except requests.RequestException as e:
log.warning(f'request error on {url}: {e}, rotating')
rotate_ip()
time.sleep(2 + random.random() * 3)
return None
def main(urls):
session = requests.Session()
session.proxies = proxy_dict()
session.headers.update({
'User-Agent': 'Mozilla/5.0 (Linux; Android 14; Pixel 8) Mobile Safari/537.36',
'Accept-Language': 'uk-UA,uk;q=0.9,en;q=0.8',
})
for i, url in enumerate(urls):
r = fetch(url, session)
log.info(f'{i+1}/{len(urls)} {url} -> {r.status_code if r else "FAILED"}')
time.sleep(random.uniform(1, 3))
if i and i % 50 == 0:
rotate_ip()
session = requests.Session()
session.proxies = proxy_dict()
if __name__ == '__main__':
main(['https://example.com/p/' + str(i) for i in range(1, 500)])
This pattern handles 500 URLs at ~1500 requests/hour effective throughput with sub-5% block rate on most mid-protection targets.
Get Mobile Proxies for Scraping
Instant API rotation - Unlimited connections on Premium - Real carrier IPs.
Further Reading
- Mobile Proxies for Web Scraping — overview of why mobile beats datacenter for scraping
- How to Check Proxy Quality — verify your rotation actually changes the exit IP
- SOCKS5 vs IKEv2 vs VLESS — protocol selection for scraping workloads