You have set up a scraper, connected proxies, but the API still returns 429 "Too Many Requests" errors or blocks access? The problem is not with the proxies themselves, but with the incorrect strategy for their use. Rate limiting is a protective mechanism of the API that restricts the number of requests from a single IP address over a specified period. In this article, we will explore why blocks occur when working through proxies and how to properly configure the system to bypass limits.
What is API rate limiting and how does it work
Rate limiting is a mechanism to protect APIs from overload and abuse. The service sets a limit on the number of requests that can be made from a single source over a specific time period. For example, popular APIs use such limits:
- Twitter API: 300 requests per 15 minutes for standard access
- Instagram Graph API: 200 requests per hour per app
- Google Maps API: depends on the plan, usually 100-1000 requests per day
- Wildberries API: unofficial limits around 60 requests per minute from a single IP
- Avito API: 10 requests per second for scraping ads
There are several methods for determining the source of requests to which rate limiting is applied:
IP address: the most common method. The API counts the number of requests from a specific IP over a time window.
API key: if you are using authorization via a key, the limit is tied to it regardless of the IP.
User-Agent and fingerprint: some APIs analyze browser headers and create a digital fingerprint of the client.
Session (cookies): the limit can be tied to the user's session via cookies.
When the limit is exceeded, the API returns HTTP status 429 "Too Many Requests" and the Retry-After header, indicating the time until the limit is reset. Some services use a "sliding window," where the limit updates gradually, while others use a fixed window that resets at a specific time.
Why proxies do not automatically save you from rate limiting
Many developers mistakenly believe that simply connecting a proxy allows for an unlimited number of requests. In practice, the following problems arise:
Using a single proxy for all requests
If your script uses the same proxy IP address for all requests, the API sees this as a regular user and applies standard limits. For example, you set up a price scraper for Wildberries through a single residential proxy. The scraper makes 100 requests per minute, but the limit is 60 requests. The result: IP block for 10-30 minutes.
Slow IP rotation
Some use a pool of 5-10 proxies and switch between them sequentially. The problem is that each IP still reaches the limit faster than a full rotation occurs. Suppose you have 10 proxies and a limit of 100 requests per hour per IP. If you make 1000 requests per hour, each proxy will receive 100 requests ā exactly at the limit. Any uneven distribution will lead to blocks.
Ignoring other identification factors
Even with perfect IP rotation, you can still be blocked if:
- All requests come from the same User-Agent (e.g.,
python-requests/2.28.0) - One API key is used for all requests
- Requests come at perfect intervals (every 0.5 seconds) ā this looks like a bot
- Proxy IPs are in the same subnet (e.g., all in the range 192.168.1.x)
IP address reputation
Data center proxies often end up on blacklists because their IPs are used by hundreds of other users for scraping. APIs may apply stricter limits to such addresses or block them outright. For example, Instagram and Facebook aggressively ban data centers, even if you do not exceed official limits.
IP rotation strategies to bypass limits
Proper proxy rotation is key to bypassing rate limiting. Let's consider effective strategies depending on the task.
Rotation after each request
The most aggressive strategy: each request goes through a new IP. Suitable for tasks with very strict limits (1-5 requests per IP) or when you need to spread the load as much as possible. For this, residential proxies with automatic rotation are used ā they provide a pool of millions of IPs, and each request automatically gets a new address.
Example use case: scraping Instagram through an unofficial API, where the limit is 5 requests per minute from one IP. With rotation after each request, you can make 300 requests per minute through 300 different IPs.
Advantages: maximum protection against rate limiting, each IP is used minimally.
Disadvantages: high cost (residential proxies are more expensive), possible delays when changing IPs, harder to maintain sessions.
Time-based rotation (sticky sessions)
The IP address is used for a certain period (5-30 minutes) and then changed to a new one. This strategy is suitable for APIs that require session persistence or when several related requests need to be made from one "user."
Calculating the optimal rotation time: if the API limit is 100 requests per hour, and you plan to make 50 requests through one IP, use a sticky session for 30 minutes. During this time, you will make 25 requests (with even load), which is half the limit.
Pool-based rotation with limit tracking
An advanced strategy: your script keeps track of the number of requests from each IP and automatically switches to a new one when approaching the limit. For example, you have a pool of 20 proxies, and the API limit is 100 requests per hour. The script tracks the counter for each IP and switches to the next one upon reaching 90 requests.
This strategy requires programming logic but provides maximum efficiency: you use each proxy to its full capacity without exceeding limits.
Geographical rotation
Some APIs apply different limits depending on the region. For example, a service may restrict requests from the USA more strictly than from Europe. In such cases, use proxies from different countries and distribute the load among them.
| Rotation Strategy | When to Use | Type of Proxy |
|---|---|---|
| After each request | Strict limits (1-10 requests/IP), scraping social networks | Residential with auto-rotation |
| By time (5-30 min) | Session needed, medium limits (50-200 requests/hour) | Residential sticky or mobile |
| By pool with limit tracking | Large volume scraping, known API limits | Any type with a pool of 10+ IPs |
| Geographical | Regional restrictions, scraping local content | Residential from different countries |
Configuring delays between requests
Even with perfect IP rotation, it is important to properly configure delays between requests. Too fast requests look like an attack, even if they come from different IPs.
Calculating minimum delay
Formula: delay = (time window in seconds / request limit) Ć safety factor
Example: The API allows 100 requests per hour (3600 seconds). Minimum delay = 3600 / 100 = 36 seconds. Adding a safety factor of 1.2: 36 Ć 1.2 = 43 seconds between requests from one IP.
If you are using 10 proxies with rotation, you can make requests every 4.3 seconds (43 / 10) without exceeding the limit on any IP.
Random delays (jitter)
Instead of a fixed delay of 5 seconds, use random intervals, for example, from 3 to 7 seconds. This makes your traffic resemble the actions of a real user. Many bot protection systems analyze patterns: if requests come exactly every 5.0 seconds, it looks suspicious.
Exponential delay on errors
When you receive a 429 error, do not continue sending requests immediately. Use exponential delay: the first attempt after 1 second, the second after 2, the third after 4, the fourth after 8, and so on. This is a standard practice that APIs expect from clients.
Tip: Check the Retry-After header in the API response. It indicates the exact time when you can repeat the request. Use this value instead of arbitrary delays.
Which type of proxy to choose for working with APIs
The choice of proxy type critically affects the success of bypassing rate limiting. Let's analyze the pros and cons of each option for working with APIs.
Residential proxies
Residential proxies use IP addresses of real users assigned by Internet Service Providers. For APIs, this looks like regular home internet.
Advantages for APIs:
- High trust: APIs rarely ban home IPs
- Huge pools: millions of IPs for rotation
- Geographical diversity: IPs from different cities and countries
- Suitable for social networks and strict APIs (Instagram, Facebook, TikTok)
Disadvantages:
- High cost: payment is usually based on traffic (from $5-15 per 1 GB)
- Variable speed: depends on the end user's internet
- Instability: IPs can disconnect at any moment
When to use: scraping Instagram, Facebook, TikTok, working with marketplace APIs (Wildberries, Ozon), any tasks where IP reputation is critical.
Mobile proxies
Mobile proxies use IPs from mobile operators (4G/5G). One IP is often used by thousands of real users simultaneously, so APIs rarely block them.
Advantages for APIs:
- Maximum trust: APIs cannot ban mobile operator IPs
- Ideal for mobile applications and APIs (Instagram, TikTok, Snapchat)
- Automatic IP change upon reconnection (airplane mode)
- One IP can make more requests without being banned
Disadvantages:
- Very high cost: from $50-150 per IP per month
- Small pools: difficult to get hundreds of mobile IPs
- Variable speed: depends on mobile signal quality
When to use: working with mobile APIs, scraping Instagram/TikTok in large volumes when maximum protection from bans is needed.
Data center proxies
Data center proxies are IP addresses of servers located in data centers. They are not associated with real users.
Advantages for APIs:
- Low cost: from $1-5 per IP per month
- High speed: channels of 1-10 Gbps
- Stability: IPs do not disconnect randomly
- Large pools: easy to get hundreds of IPs
Disadvantages:
- Low trust: many APIs block data centers
- IPs are often blacklisted due to other users
- Not suitable for social networks and strict services
When to use: scraping public APIs without strict protection (weather, currency rates, news), working with your own APIs, testing, and development.
| Criterion | Residential | Mobile | Data Centers |
|---|---|---|---|
| API Trust | High | Maximum | Low |
| Pool Size | Millions of IPs | Hundreds of IPs | Thousands of IPs |
| Speed | Average (10-50 Mbps) | Average (5-100 Mbps) | High (100+ Mbps) |
| Cost | $5-15/GB | $50-150/IP/month | $1-5/IP/month |
| For Social Networks | ā Excellent | ā Ideal | ā Not Suitable |
| For Public APIs | ā Good | ā Good (Expensive) | ā Excellent |
Practical implementation: code examples in Python
Let's consider specific examples of implementing rate limiting bypass using proxies. All examples are in Python using the requests library.
Simple rotation from a proxy pool
Basic implementation with cyclic rotation of IPs from a list:
import requests
import time
from itertools import cycle
# List of proxies (format: protocol://user:pass@host:port)
PROXY_LIST = [
'http://user1:pass1@proxy1.example.com:8080',
'http://user2:pass2@proxy2.example.com:8080',
'http://user3:pass3@proxy3.example.com:8080',
]
# Create a cyclic iterator
proxy_pool = cycle(PROXY_LIST)
def make_request(url):
proxy = next(proxy_pool) # Get the next proxy from the pool
proxies = {
'http': proxy,
'https': proxy
}
try:
response = requests.get(url, proxies=proxies, timeout=10)
return response
except requests.exceptions.RequestException as e:
print(f"Error with proxy {proxy}: {e}")
return None
# Example usage
for i in range(10):
response = make_request('https://api.example.com/data')
if response and response.status_code == 200:
print(f"Request {i+1}: success")
time.sleep(2) # Delay between requests
Rotation with limit tracking
A more advanced version that counts requests for each proxy and switches when approaching the limit:
import requests
import time
from collections import defaultdict
class ProxyRotator:
def __init__(self, proxy_list, max_requests_per_ip=90, time_window=3600):
self.proxy_list = proxy_list
self.max_requests = max_requests_per_ip # Request limit per IP
self.time_window = time_window # Time window in seconds
self.request_counts = defaultdict(list) # History of requests for each IP
self.current_index = 0
def get_proxy(self):
"""Returns the proxy with the least number of requests"""
current_time = time.time()
# Clear old records outside the time window
for proxy in self.request_counts:
self.request_counts[proxy] = [
t for t in self.request_counts[proxy]
if current_time - t < self.time_window
]
# Find the proxy with the least number of requests
available_proxies = []
for proxy in self.proxy_list:
count = len(self.request_counts[proxy])
if count < self.max_requests:
available_proxies.append((proxy, count))
if not available_proxies:
# If all proxies have exhausted their limit, wait
oldest_request = min(
min(times) for times in self.request_counts.values() if times
)
wait_time = self.time_window - (current_time - oldest_request) + 1
print(f"All proxies have exhausted their limit. Waiting {wait_time:.0f} sec...")
time.sleep(wait_time)
return self.get_proxy()
# Select the proxy with the least number of requests
proxy = min(available_proxies, key=lambda x: x[1])[0]
self.request_counts[proxy].append(current_time)
return proxy
def make_request(self, url, **kwargs):
proxy = self.get_proxy()
proxies = {'http': proxy, 'https': proxy}
try:
response = requests.get(url, proxies=proxies, timeout=10, **kwargs)
return response
except requests.exceptions.RequestException as e:
print(f"Error with proxy {proxy}: {e}")
return None
# Example usage
PROXY_LIST = [
'http://user:pass@proxy1.example.com:8080',
'http://user:pass@proxy2.example.com:8080',
]
rotator = ProxyRotator(PROXY_LIST, max_requests_per_ip=100, time_window=3600)
for i in range(500): # Make 500 requests
response = rotator.make_request('https://api.example.com/data')
if response and response.status_code == 200:
print(f"Request {i+1}: success")
time.sleep(1) # Minimum delay
Handling 429 errors with exponential delay
Proper handling of the "Too Many Requests" response considering the Retry-After header:
import requests
import time
def make_request_with_retry(url, proxies, max_retries=5):
"""Makes a request with automatic retries on 429 error"""
for attempt in range(max_retries):
try:
response = requests.get(url, proxies=proxies, timeout=10)
if response.status_code == 200:
return response
elif response.status_code == 429:
# Check the Retry-After header
retry_after = response.headers.get('Retry-After')
if retry_after:
wait_time = int(retry_after)
print(f"Rate limit. Waiting {wait_time} sec (from Retry-After)")
else:
# Exponential delay: 2^attempt seconds
wait_time = 2 ** attempt
print(f"Rate limit. Attempt {attempt+1}, waiting {wait_time} sec")
time.sleep(wait_time)
continue
else:
print(f"HTTP Error {response.status_code}")
return None
except requests.exceptions.RequestException as e:
print(f"Connection error: {e}")
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
continue
return None
print(f"Exceeded number of attempts ({max_retries})")
return None
# Example usage
proxies = {
'http': 'http://user:pass@proxy.example.com:8080',
'https': 'http://user:pass@proxy.example.com:8080'
}
response = make_request_with_retry('https://api.example.com/data', proxies)
if response:
print("Data received:", response.json())
Using residential proxies with automatic rotation
Many residential proxy providers offer a single endpoint that automatically changes the IP with each request. Example configuration:
import requests
import random
import time
# Residential proxies with automatic rotation
# Format: protocol://username:password@gateway:port
ROTATING_PROXY = 'http://customer-USER:PASS@proxy.provider.com:12321'
def make_request_rotating(url):
"""Request through rotating proxy (new IP each time)"""
proxies = {
'http': ROTATING_PROXY,
'https': ROTATING_PROXY
}
# Add a random User-Agent for more anonymity
user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
]
headers = {
'User-Agent': random.choice(user_agents)
}
try:
response = requests.get(url, proxies=proxies, headers=headers, timeout=15)
return response
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
return None
# Make 100 requests through different IPs
for i in range(100):
response = make_request_rotating('https://api.example.com/data')
if response and response.status_code == 200:
print(f"Request {i+1}: success, IP changed")
# Random delay of 1-3 seconds
time.sleep(random.uniform(1, 3))
Monitoring limits and error handling
Effective work with APIs requires constant monitoring of limits and proper error handling. Here are key practices:
Analyzing response headers
Many APIs return information about limits in the response headers. Standard headers include:
X-RateLimit-Limitā maximum number of requests in the windowX-RateLimit-Remainingā how many requests are leftX-RateLimit-Resetā time when the limit resets (Unix timestamp)Retry-Afterā how many seconds until you can repeat the request
Example of reading these headers:
response = requests.get(url, proxies=proxies)
# Check limit headers
limit = response.headers.get('X-RateLimit-Limit')
remaining = response.headers.get('X-RateLimit-Remaining')
reset_time = response.headers.get('X-RateLimit-Reset')
if remaining:
remaining = int(remaining)
if remaining < 10:
print(f"Warning! Only {remaining} requests left")
if reset_time:
import datetime
reset_dt = datetime.datetime.fromtimestamp(int(reset_time))
print(f"Limit will reset at {reset_dt}")
Logging and statistics
Keep detailed statistics of requests for each proxy. This will help identify problematic IPs and optimize rotation:
import json
from datetime import datetime
class RequestLogger:
def __init__(self):
self.stats = {}
def log_request(self, proxy, status_code, response_time):
if proxy not in self.stats:
self.stats[proxy] = {
'total': 0,
'success': 0,
'rate_limited': 0,
'errors': 0,
'avg_response_time': 0
}
self.stats[proxy]['total'] += 1
if status_code == 200:
self.stats[proxy]['success'] += 1
elif status_code == 429:
self.stats[proxy]['rate_limited'] += 1
else:
self.stats[proxy]['errors'] += 1
# Update average response time
current_avg = self.stats[proxy]['avg_response_time']
total = self.stats[proxy]['total']
self.stats[proxy]['avg_response_time'] = (
(current_avg * (total - 1) + response_time) / total
)
def print_stats(self):
print("\n=== Proxy Statistics ===")
for proxy, data in self.stats.items():
success_rate = (data['success'] / data['total'] * 100) if data['total'] > 0 else 0
print(f"\nProxy: {proxy}")
print(f" Total requests: {data['total']}")
print(f" Successful: {data['success']} ({success_rate:.1f}%)")
print(f" Rate limited: {data['rate_limited']}")
print(f" Errors: {data['errors']}")
print(f" Average response time: {data['avg_response_time']:.2f}s")
# Usage
logger = RequestLogger()
start_time = time.time()
response = requests.get(url, proxies=proxies)
response_time = time.time() - start_time
logger.log_request(proxy, response.status_code, response_time)
logger.print_stats()
Automatic strategy switching
If you are constantly receiving 429 errors, automatically slow down requests or increase the proxy pool:
class AdaptiveRateLimiter:
def __init__(self, initial_delay=1.0):
self.delay = initial_delay
self.consecutive_429 = 0
def on_success(self):
"""Successful request - can speed up a bit"""
self.consecutive_429 = 0
self.delay = max(0.5, self.delay * 0.95) # Decrease delay by 5%
def on_rate_limit(self):
"""Received 429 - need to slow down"""
self.consecutive_429 += 1
self.delay *= 1.5 # Increase delay by 1.5 times
if self.consecutive_429 > 5:
print("WARNING: Too many 429 errors. Check settings!")
def wait(self):
"""Wait before the next request"""
time.sleep(self.delay)
return self.delay
# Usage
limiter = AdaptiveRateLimiter(initial_delay=2.0)
for i in range(1000):
response = make_request(url)
if response.status_code == 200:
limiter.on_success()
elif response.status_code == 429:
limiter.on_rate_limit()
delay = limiter.wait()
print(f"Request {i+1}, delay: {delay:.2f}s")
Handling captchas and other blocks
Some APIs display captchas instead of directly blocking when limits are exceeded. Signs include:
- Status code 403 with a response body containing "captcha" or "recaptcha"
- Redirect to a captcha page (status 302)
- Special headers like
X-Captcha-Required: true
In such cases, you should:
- Immediately stop using this IP
- Switch to another proxy from the pool
- Increase delays between requests
- Add more variety to User-Agent and other headers
Important: If you regularly encounter captchas when using residential proxies, the problem is likely in behavior patterns (identical headers, too fast requests), not in the IP addresses.
Conclusion
Bypassing API rate limiting when using proxies is not just a technical setup, but a comprehensive strategy that includes the right choice of proxy type, configuring IP rotation, managing delays, and monitoring limits. Key takeaways from the article:
- Proxies alone do not solve the rate limiting problem ā a proper rotation strategy is needed
- For social networks and strict APIs, use residential or mobile proxies; for public APIs, data centers are suitable
- Calculate the size of the proxy pool based on API limits and desired scraping speed
- Always monitor limits and adjust your strategy accordingly