Rate limiting — одна из самых частых причин, по которой парсеры падают, API-интеграции рвутся, а автоматизированные скрипты получают статус 429 Too Many Requests. Сервер видит слишком много запросов с одного IP — и просто перестаёт отвечать. В этой статье разберём, как правильно выстроить инфраструктуру на прокси, чтобы обходить лимиты запросов без банов и сбоев — с реальными примерами кода на Python и Node.js.
Что такое rate limiting и почему обычные задержки не помогают
Rate limiting (ограничение частоты запросов) — это механизм защиты сервера, который ограничивает количество запросов от одного источника за определённый промежуток времени. Источником чаще всего выступает IP-адрес, но продвинутые системы также учитывают токены авторизации, User-Agent, cookies и даже поведенческие паттерны.
Когда ваш скрипт превышает лимит, сервер возвращает один из следующих ответов:
429 Too Many Requests— стандартный HTTP-статус для rate limiting503 Service Unavailable— иногда используется вместо 429403 Forbidden— если IP уже занесён в блок-лист- Пустой ответ или таймаут — при агрессивной блокировке
Первая мысль большинства разработчиков — добавить time.sleep(1) между запросами. Это работает только при очень мягких лимитах (например, 60 запросов в минуту). Но реальные сценарии сложнее:
Реальные лимиты популярных платформ:
- Twitter/X API (бесплатный): 500 000 твитов в месяц, но не более 15 запросов каждые 15 минут
- Google Search: ~100 запросов в день с одного IP без авторизации
- Wildberries, Ozon: агрессивный rate limiting — блок после 30–50 запросов в минуту
- GitHub API: 60 запросов/час без токена, 5000/час с токеном
- Cloudflare-защищённые сайты: могут блокировать уже после 10–20 запросов в минуту
Если вам нужно собрать 100 000 карточек товаров с маркетплейса или мониторить цены в реальном времени — задержки просто не помогут. Нужна другая архитектура. И именно здесь прокси становятся не опцией, а необходимостью.
Важно понимать: rate limiting привязан к IP-адресу. Если у вас 100 разных IP — у вас фактически 100 независимых «квот». Это и есть ключевой принцип обхода ограничений через прокси.
Как прокси решают проблему ограничений по запросам
Механизм прост: каждый запрос к целевому серверу уходит с разного IP-адреса. С точки зрения сервера — это разные пользователи. Квота каждого из них практически не расходуется, поэтому блокировка не наступает.
Рассмотрим разницу между работой без прокси и с пулом прокси на конкретном примере. Допустим, сервер разрешает 10 запросов в минуту с одного IP:
| Сценарий | Запросов в минуту | Блокировка | Время на 10 000 запросов |
|---|---|---|---|
| Один IP, без прокси | 10 | Да, после 10 запросов | ~16 часов |
| 10 прокси, ротация | 100 | Нет | ~1.7 часа |
| 100 прокси, ротация | 1000 | Нет | ~10 минут |
Кроме масштабирования пропускной способности, прокси дают ещё несколько преимуществ при работе с rate limiting:
- Изоляция сессий — если один IP попал в бан, остальные продолжают работать
- Географическое распределение — запросы идут из разных регионов, что снижает подозрительность
- Sticky sessions — возможность «прилипнуть» к одному IP для многошаговых сценариев (авторизация + действие)
- Контроль нагрузки — можно точно дозировать запросы на каждый IP, не превышая лимит
Какой тип прокси выбрать под вашу задачу
Не все прокси одинаково эффективны против rate limiting. Выбор типа зависит от целевого сайта, объёма запросов и бюджета. Разберём три основных типа:
Резидентные прокси
Это IP-адреса реальных домашних пользователей. Они выглядят как обычный интернет-трафик и крайне редко попадают под блокировки. Резидентные прокси — оптимальный выбор для сайтов с агрессивной защитой: маркетплейсы (Wildberries, Ozon), социальные сети, Cloudflare-защищённые ресурсы. Основной минус — более высокая цена по сравнению с дата-центровыми.
Мобильные прокси
IP-адреса мобильных операторов (3G/4G/5G). Их особенность — один IP может использоваться тысячами реальных абонентов одновременно, поэтому блокировать такой адрес сайты крайне не хотят. Мобильные прокси показывают наилучшие результаты там, где резидентные уже начинают блокироваться — например, при высокочастотном парсинге Instagram или работе с API платформ, которые анализируют тип подключения.
Прокси дата-центров
Быстрые и дешёвые IP из серверных дата-центров. Идеально подходят для парсинга сайтов без серьёзной защиты: открытые API, новостные агрегаторы, публичные базы данных. Для задач с rate limiting их нужно больше (так как они чаще попадают в блок-листы), но при правильной ротации они отлично справляются с большими объёмами запросов. Подробнее — на странице прокси дата-центров.
| Тип прокси | Анонимность | Скорость | Цена | Лучший сценарий |
|---|---|---|---|---|
| Резидентные | Очень высокая | Средняя | $$ | Маркетплейсы, соцсети, Cloudflare |
| Мобильные | Максимальная | Средняя | $$$ | Instagram API, высокочастотный парсинг |
| Дата-центры | Средняя | Высокая | $ | Открытые API, публичные данные |
Стратегии ротации IP: per-request, sticky sessions, round-robin
Сам факт наличия прокси ещё не решает проблему — важно правильно ими управлять. Существует несколько стратегий ротации, каждая из которых подходит для своих сценариев.
Per-request ротация (новый IP на каждый запрос)
Каждый HTTP-запрос уходит через новый IP-адрес. Это максимально агрессивная стратегия обхода rate limiting — сервер физически не успевает накапливать счётчик для одного IP. Подходит для:
- Парсинга карточек товаров (каждая карточка — отдельный запрос)
- Сбора данных с поисковых систем
- Любых stateless-запросов, не требующих сессии
Sticky sessions (фиксированный IP на сессию)
Один IP используется на протяжении всей сессии (обычно 1–30 минут). Критически важно для сценариев, где нужна авторизация: войти в аккаунт, выполнить действие, выйти. Если IP меняется между шагами — сервер может заблокировать сессию как подозрительную.
Round-robin с лимитом запросов на IP
Наиболее точная стратегия. Вы знаете лимит сервера (например, 10 запросов в минуту) и распределяете запросы по пулу прокси так, чтобы каждый IP никогда не превышал этот порог. Требует реализации очереди с учётом времени последнего запроса для каждого IP.
Формула расчёта нужного количества прокси:
N прокси = (Целевая скорость запросов/мин) ÷ (Лимит сервера/мин на IP)
Пример: нужно 500 запросов/мин, лимит сервера — 10/мин → нужно минимум 50 прокси.
Добавьте 20% запас на случай блокировок: итого 60 прокси.
Примеры кода на Python: requests, aiohttp, Scrapy
Перейдём к практике. Ниже — готовые шаблоны для трёх наиболее популярных Python-инструментов.
1. requests + ротация прокси вручную
Простейший вариант — список прокси и случайный выбор на каждый запрос:
import requests
import random
import time
PROXIES = [
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
# ... добавьте нужное количество
]
def get_random_proxy():
proxy = random.choice(PROXIES)
return {"http": proxy, "https": proxy}
def fetch_with_retry(url, max_retries=3):
for attempt in range(max_retries):
proxy = get_random_proxy()
try:
response = requests.get(
url,
proxies=proxy,
timeout=10,
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
)
if response.status_code == 429:
print(f"Rate limited on {proxy}, switching...")
time.sleep(1)
continue
return response
except requests.RequestException as e:
print(f"Attempt {attempt+1} failed: {e}")
time.sleep(2)
return None
# Использование
urls = ["https://example.com/item/1", "https://example.com/item/2"]
for url in urls:
result = fetch_with_retry(url)
if result:
print(f"OK: {url} — {len(result.text)} bytes")
2. Умный пул прокси с учётом rate limit
Более продвинутый вариант — класс ProxyPool, который отслеживает время последнего использования каждого IP и не превышает установленный лимит:
import requests
import time
from collections import defaultdict
from threading import Lock
class ProxyPool:
def __init__(self, proxies, rate_limit=10, window=60):
"""
proxies: список строк вида 'http://user:pass@host:port'
rate_limit: максимум запросов с одного IP за window секунд
window: временное окно в секундах
"""
self.proxies = proxies
self.rate_limit = rate_limit
self.window = window
self.usage = defaultdict(list) # proxy -> [timestamps]
self.lock = Lock()
def get_available_proxy(self):
now = time.time()
with self.lock:
for proxy in self.proxies:
# Очищаем устаревшие метки
self.usage[proxy] = [
t for t in self.usage[proxy]
if now - t < self.window
]
if len(self.usage[proxy]) < self.rate_limit:
self.usage[proxy].append(now)
return {"http": proxy, "https": proxy}
return None # Все прокси исчерпали лимит
def fetch(self, url, **kwargs):
proxy = self.get_available_proxy()
if proxy is None:
print("All proxies rate-limited, waiting...")
time.sleep(5)
return self.fetch(url, **kwargs)
try:
response = requests.get(url, proxies=proxy, timeout=10, **kwargs)
return response
except requests.RequestException as e:
print(f"Request failed: {e}")
return None
# Использование
pool = ProxyPool(
proxies=[
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
],
rate_limit=10, # 10 запросов в минуту на IP
window=60
)
for i in range(100):
r = pool.fetch(f"https://example.com/page/{i}")
if r:
print(f"Page {i}: {r.status_code}")
3. aiohttp для асинхронного парсинга
Асинхронный подход позволяет параллельно использовать десятки прокси без блокировки потоков:
import asyncio
import aiohttp
import itertools
PROXIES = [
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
]
proxy_cycle = itertools.cycle(PROXIES)
async def fetch(session, url, proxy):
try:
async with session.get(
url,
proxy=proxy,
timeout=aiohttp.ClientTimeout(total=10)
) as response:
if response.status == 429:
await asyncio.sleep(2)
return None
return await response.text()
except Exception as e:
print(f"Error: {e}")
return None
async def main(urls):
connector = aiohttp.TCPConnector(limit=50)
async with aiohttp.ClientSession(connector=connector) as session:
tasks = [
fetch(session, url, next(proxy_cycle))
for url in urls
]
results = await asyncio.gather(*tasks)
return results
urls = [f"https://example.com/item/{i}" for i in range(200)]
results = asyncio.run(main(urls))
print(f"Collected: {sum(1 for r in results if r is not None)} pages")
4. Scrapy с ротацией через middleware
Для Scrapy существует готовое решение — scrapy-rotating-proxies. Но можно написать собственный middleware:
# middlewares.py
import random
class RotatingProxyMiddleware:
def __init__(self, proxies):
self.proxies = proxies
@classmethod
def from_crawler(cls, crawler):
return cls(proxies=crawler.settings.getlist("PROXY_LIST"))
def process_request(self, request, spider):
proxy = random.choice(self.proxies)
request.meta["proxy"] = proxy
def process_response(self, request, response, spider):
if response.status == 429:
spider.logger.warning(f"Rate limited, proxy: {request.meta.get('proxy')}")
# Можно добавить логику исключения проблемного прокси
return response
# settings.py
PROXY_LIST = [
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
]
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.RotatingProxyMiddleware": 350,
}
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 10
Примеры кода на Node.js: axios, got, Puppeteer
Node.js — популярный выбор для автоматизации браузеров и работы с API. Вот готовые паттерны для работы с прокси.
1. axios с ротацией прокси
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const proxies = [
'http://user:[email protected]:8080',
'http://user:[email protected]:8080',
'http://user:[email protected]:8080',
];
let proxyIndex = 0;
function getNextProxy() {
const proxy = proxies[proxyIndex % proxies.length];
proxyIndex++;
return proxy;
}
async function fetchWithProxy(url, retries = 3) {
for (let i = 0; i < retries; i++) {
const proxyUrl = getNextProxy();
const agent = new HttpsProxyAgent(proxyUrl);
try {
const response = await axios.get(url, {
httpsAgent: agent,
httpAgent: agent,
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
},
});
return response.data;
} catch (error) {
if (error.response?.status === 429) {
console.log(`Rate limited, switching proxy...`);
await new Promise(r => setTimeout(r, 1000));
continue;
}
console.error(`Attempt ${i + 1} failed:`, error.message);
}
}
return null;
}
// Использование
(async () => {
const urls = Array.from({length: 50}, (_, i) => `https://example.com/item/${i}`);
const results = await Promise.allSettled(
urls.map(url => fetchWithProxy(url))
);
const successful = results.filter(r => r.status === 'fulfilled' && r.value).length;
console.log(`Success: ${successful}/${urls.length}`);
})();
2. Puppeteer с прокси и обходом rate limiting
Для сайтов с JavaScript-рендерингом и Cloudflare-защитой нужен headless-браузер:
const puppeteer = require('puppeteer');
const proxies = [
'proxy1.example.com:8080',
'proxy2.example.com:8080',
];
async function scrapeWithProxy(url, proxyHost) {
const browser = await puppeteer.launch({
args: [
`--proxy-server=${proxyHost}`,
'--no-sandbox',
'--disable-setuid-sandbox',
],
headless: true,
});
const page = await browser.newPage();
// Авторизация прокси
await page.authenticate({
username: 'user',
password: 'pass',
});
// Устанавливаем реалистичный User-Agent
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
);
try {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
// Проверяем на rate limit
const status = await page.evaluate(() => document.title);
if (status.includes('429') || status.includes('Too Many')) {
console.log('Rate limited, need to switch proxy');
return null;
}
const data = await page.evaluate(() => {
return document.querySelector('.price')?.textContent || null;
});
return data;
} finally {
await browser.close();
}
}
// Ротация по задачам
(async () => {
const urls = ['https://example.com/product/1', 'https://example.com/product/2'];
for (let i = 0; i < urls.length; i++) {
const proxy = proxies[i % proxies.length];
const result = await scrapeWithProxy(urls[i], proxy);
console.log(`${urls[i]}: ${result}`);
await new Promise(r => setTimeout(r, 500)); // небольшая задержка
}
})();
Продвинутые техники: заголовки, fingerprint, обход Cloudflare
Смена IP — необходимое, но не всегда достаточное условие. Современные системы защиты анализируют десятки параметров запроса. Разберём, что ещё нужно учитывать.
HTTP-заголовки: минимальный обязательный набор
Запрос без нормальных заголовков выглядит как бот даже при смене IP. Всегда добавляйте:
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Cache-Control": "max-age=0",
}
Обработка заголовка Retry-After
При ответе 429 сервер часто указывает, сколько нужно подождать. Правильная обработка этого заголовка позволяет не тратить запросы впустую:
def handle_rate_limit(response):
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
if retry_after:
wait_time = int(retry_after)
print(f"Rate limited. Waiting {wait_time} seconds...")
time.sleep(wait_time + 1) # +1 секунда буфер
else:
# Экспоненциальная задержка если заголовка нет
time.sleep(min(2 ** attempt, 60))
return True
return False
TLS fingerprinting и как его обойти
Продвинутые системы (Cloudflare, Akamai, PerimeterX) анализируют TLS fingerprint — уникальный «отпечаток» вашего TLS-соединения. Стандартная библиотека requests имеет легко узнаваемый fingerprint. Решения:
- curl_cffi (Python) — эмулирует fingerprint Chrome/Firefox на уровне TLS
- tls-client (Go/Python) — аналогичный инструмент с поддержкой разных браузерных профилей
- Playwright/Puppeteer — реальный браузер, идеальный fingerprint по умолчанию
# pip install curl-cffi
from curl_cffi import requests as cffi_requests
response = cffi_requests.get(
"https://cloudflare-protected-site.com/api/data",
impersonate="chrome120", # Эмулируем Chrome 120
proxies={"https": "http://user:[email protected]:8080"}
)
print(response.json())
Управление cookies и сессиями
Если сайт использует cookies для отслеживания сессий, смена IP без смены cookies бессмысленна. При переключении прокси всегда создавайте новую сессию:
import requests
def create_fresh_session(proxy_url):
"""Создаём новую сессию с чистыми cookies для каждого прокси"""
session = requests.Session()
session.proxies = {"http": proxy_url, "https": proxy_url}
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
})
# Cookies не переносятся из предыдущей сессии
return session
# Для каждого нового IP — новая сессия
for proxy in proxies:
session = create_fresh_session(proxy)
response = session.get("https://example.com/protected-page")
# Обрабатываем ответ...
Типичные ошибки при работе с прокси и rate limiting
Даже с правильно настроенными прокси разработчики регулярно наступают на одни и те же грабли. Вот самые частые ошибки и способы их избежать.
Чек-лист: что проверить перед запуском парсера
- ☐ Добавлены реалистичные HTTP-заголовки (User-Agent, Accept, Accept-Language)
- ☐ При смене прокси создаётся новая сессия (новые cookies)
- ☐ Обрабатываются статусы 429, 503, 403 с логикой retry
- ☐ Реализована задержка между запросами (хотя бы 100–500 мс)
- ☐ Количество прокси соответствует целевой скорости запросов
- ☐ Проверена работа прокси перед стартом (health check)
- ☐ Логируются ошибки и статистика по каждому прокси
- ☐ Настроен таймаут для запросов (не более 15–30 секунд)
Ошибка 1: Использование «мёртвых» прокси
Всегда проверяйте прокси перед добавлением в пул и периодически во время работы. Один нерабочий прокси в цикле — это потерянные запросы и таймауты:
def check_proxy(proxy_url, test_url="https://httpbin.org/ip", timeout=5):
try:
r = requests.get(
test_url,
proxies={"http": proxy_url, "https": proxy_url},
timeout=timeout
)
return r.status_code == 200
except:
return False
# Фильтруем рабочие прокси перед стартом
working_proxies = [p for p in PROXIES if check_proxy(p)]
print(f"Working proxies: {len(working_proxies)}/{len(PROXIES)}")
Ошибка 2: Игнорирование типа протокола
HTTP-прокси не могут проксировать HTTPS-трафик напрямую (только через CONNECT). SOCKS5-прокси работают на уровне транспорта и поддерживают любые протоколы. Для большинства современных сайтов используйте SOCKS5 или HTTPS-прокси:
# SOCKS5 прокси в requests (требует pip install requests[socks])
proxies = {
"http": "socks5://user:[email protected]:1080",
"https": "socks5://user:[email protected]:1080",
}
# HTTPS прокси
proxies = {
"http": "https://user:[email protected]:8080",
"https": "https://user:[email protected]:8080",
}
Ошибка 3: Отсутствие экспоненциального backoff
Если после 429 вы сразу повторяете запрос — вы только усугубляете ситуацию. Правильная стратегия — экспоненциальная задержка с джиттером (случайным отклонением):
import random
def exponential_backoff(attempt, base=1, max_wait=60):
"""
attempt: номер попытки (начиная с 0)
base: базовая задержка в секундах
max_wait: максимальная задержка
"""
wait = min(base * (2 ** attempt), max_wait)
# Джиттер ±25% для предотвращения thundering herd
jitter = wait * 0.25 * random.uniform(-1, 1)
return wait + jitter
# Использование в retry-логике
for attempt in range(5):
response = requests.get(url, proxies=proxy)
if response.status_code == 429:
wait = exponential_backoff(attempt)
print(f"Rate limited. Waiting {wait:.1f}s (attempt {attempt+1})")
time.sleep(wait)
else:
break
Ошибка 4: Один поток на все прокси
Если у вас 50 прокси, но один поток выполнения — вы используете максимум 1 прокси одновременно. Используйте ThreadPoolExecutor или асинхронный подход для параллельного использования всего пула:
from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch_url(args):
url, proxy = args
try:
r = requests.get(url, proxies={"https": proxy}, timeout=10)
return url, r.status_code, len(r.text)
except Exception as e:
return url, None, str(e)
# Параллельно используем все прокси
tasks = [(url, proxies[i % len(proxies)]) for i, url in enumerate(urls)]
with ThreadPoolExecutor(max_workers=len(proxies)) as executor:
futures = {executor.submit(fetch_url, task): task for task in tasks}
for future in as_completed(futures):
url, status, size = future.result()
print(f"{url}: {status} ({size})")
Заключение и рекомендации
Rate limiting — решаемая проблема, если подойти к ней системно. Ключевые выводы из этого руководства:
- Прокси-пул, а не один прокси — минимальная единица для серьёзной работы. Количество прокси определяется по формуле: целевая скорость ÷ лимит сервера на IP.
- Стратегия ротации важна — per-request для stateless-запросов, sticky sessions для авторизованных сценариев.
- IP — не единственный параметр — заголовки, cookies, TLS fingerprint и поведенческие паттерны также анализируются системами защиты.
- Обрабатывайте 429 корректно — экспоненциальный backoff, заголовок Retry-After, смена прокси при блокировке.
- Тип прокси зависит от цели — дата-центровые для открытых API, резидентные для маркетплейсов, мобильные для максимальной защиты.
Если вы работаете с парсингом маркетплейсов (Wildberries, Ozon), сбором данных с защищённых API или автоматизацией на высоких скоростях — рекомендуем начать с резидентных прокси: они обеспечивают оптимальный баланс между анонимностью и скоростью, а их IP-адреса практически не попадают в блок-листы. Для задач, где нужна максимальная устойчивость к блокировкам при высокой частоте запросов, стоит рассмотреть мобильные прокси — их IP разделяют тысячи реальных пользователей, что делает блокировку крайне нежелательной для любого сайта.