If you need to build historical candlestick datasets, monitor order books across dozens of trading pairs, or track real-time prices for quantitative strategies, you need to know how to scrape the Binance REST API with proxies. Binance exposes rich public market-data endpoints, but its weight-based rate limiting is aggressive: a single IP that hammers /api/v3/depth can exhaust its per-IP weight budget in minutes and earn a temporary ban. This guide maps every key endpoint, explains the weight system, and gives you runnable Python and Node.js code that rotates residential proxies through gate.proxyhat.com to stay under the limit.
Compliance caveat: This guide covers public market data only — endpoints that require no authentication and are documented in Binance's official API docs. Always respect Binance's Terms of Use, honor rate-limit weights, and prefer the official API access path where terms require it. Do not scrape authenticated or private endpoints without explicit authorization.
Binance API Scraping: Endpoint Map and Weight Costs
Binance's REST API lives at https://api.binance.com. The public market-data endpoints you'll use most frequently are all under /api/v3/. Each endpoint carries a request weight that counts against your per-IP limit. Binance returns the current weight consumption in the X-MBX-USED-WEIGHT-1M response header on every reply.
| Endpoint | Purpose | Default Weight | Key Parameters |
|---|---|---|---|
/api/v3/klines | Candlestick / OHLCV data | 1–2 (varies with limit) | symbol, interval, limit (max 1000) |
/api/v3/depth | Order book snapshot | 5–20 (varies with limit) | symbol, limit (5/10/20/50/100/500/1000) |
/api/v3/ticker/24hr | 24-hour rolling stats | 1–40 (single vs all symbols) | symbol (optional) |
/api/v3/ticker/price | Latest trade price | 1–2 (single vs all) | symbol (optional) |
The critical insight: weight is per IP, not per API key. For public market-data endpoints, no API key is required, so the IP address of the requester is the sole identity Binance uses for rate limiting. This is exactly why rotating residential proxies through ProxyHat's global IP pool is so effective — each exit IP gets its own weight budget.
For full details on weights and limits, see Binance's official REST API documentation, which is the authoritative source for current weight values.
Why Binance's Weight-Based Rate Limiting Burns Through IPs
Binance enforces a per-IP weight budget of approximately 6,000 weight units per minute (the exact figure may vary by endpoint group and can change). Every request consumes weight based on the endpoint and parameters. When your cumulative weight approaches the limit, Binance responds with HTTP 429 (Too Many Requests). If you continue sending requests after a 429, Binance escalates to HTTP 418, which indicates an automatic IP ban — typically lasting 2 to 15 minutes for first offenses, but longer for repeat violators.
The Retry-After header on a 429 tells you how many seconds to wait before retrying. The X-MBX-USED-WEIGHT-1M header on every successful response tells you how much of your budget you've consumed in the current minute window. A well-behaved scraper reads this header and throttles accordingly.
Here's why a naive /api/v3/depth poll is dangerous: requesting limit=1000 for a single symbol costs 20 weight per call. If you poll 10 symbols every second, that's 200 weight/second, or 12,000 weight/minute — double the budget. You'll hit 429 in under 30 seconds. With limit=500, each call costs 10 weight, so the same poll rate consumes 6,000 weight/minute — right at the ceiling, with zero margin for retries or other endpoints.
The Binance.com vs Binance.US Geo-Split
Another gotcha: Binance.com restricts access from US IP addresses, returning HTTP 451 (Unavailable For Legal Reasons) for some endpoints. Conversely, Binance.US (a separate entity at api.binance.us) is only accessible from US IPs. If your infrastructure runs in a US datacenter and you need api.binance.com data, you'll need a non-US exit IP. ProxyHat's geo-targeting solves this cleanly — you can request -country-DE for Binance.com access or -country-US for Binance.US.
ProxyHat Setup: Rotating Residential Proxies for Binance
ProxyHat provides residential, mobile, and datacenter proxies through a single gateway: gate.proxyhat.com. HTTP traffic uses port 8080; SOCKS5 uses port 1080. Geo-targeting and session control are embedded in the username field, not the URL path.
| Parameter | Value |
|---|---|
| Gateway hostname | gate.proxyhat.com |
| HTTP port | 8080 |
| SOCKS5 port | 1080 |
| HTTP URL format | http://USERNAME:PASSWORD@gate.proxyhat.com:8080 |
| SOCKS5 URL format | socks5://USERNAME:PASSWORD@gate.proxyhat.com:1080 |
Username flags let you control geo-targeting and session stickiness:
user-country-US:pass— US exit IP (for Binance.US)user-country-DE:pass— German exit IP (for Binance.com)user-country-DE-city-berlin:pass— city-level targetinguser-session-abc123:pass— sticky session (same IP for session ID)
For Binance scraping, residential proxies are the best choice because they appear as real ISP-assigned IPs, making them less likely to be flagged by Binance's anti-abuse systems. Datacenter IPs may work but carry higher ban risk. See ProxyHat pricing for residential proxy plans.
Code Example 1: Raw Proxy Rotation with Python requests
This example fetches klines for BTCUSDT using a rotating residential proxy. Each request gets a fresh IP because no session flag is set — ProxyHat rotates automatically.
import requests
import time
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
PROXY_USER = "user-country-DE"
PROXY_PASS = "your_password"
PROXY_URL = f"http://{PROXY_USER}:{PROXY_PASS}@gate.proxyhat.com:8080"
BINANCE_BASE = "https://api.binance.com"
session = requests.Session()
session.proxies = {"http": PROXY_URL, "https": PROXY_URL}
def fetch_klines(symbol: str, interval: str, limit: int = 500) -> list:
"""Fetch candlestick data with weight-aware throttling."""
url = f"{BINANCE_BASE}/api/v3/klines"
params = {"symbol": symbol, "interval": interval, "limit": limit}
for attempt in range(5):
resp = session.get(url, params=params, timeout=15)
# Read the weight header on every response
used_weight = resp.headers.get("X-MBX-USED-WEIGHT-1M", "unknown")
logging.info(f"{symbol} {interval} status={resp.status_code} weight_1m={used_weight}")
if resp.status_code == 200:
# Back off proactively if we're near the limit
if used_weight != "unknown" and int(used_weight) > 5000:
logging.warning(f"Weight {used_weight} near limit, sleeping 10s")
time.sleep(10)
return resp.json()
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 5))
logging.warning(f"429 rate limited, waiting {retry_after}s")
time.sleep(retry_after)
continue
if resp.status_code == 418:
logging.error("418 IP banned — backing off 60s")
time.sleep(60)
continue
if resp.status_code == 451:
logging.error("451 geo-restricted — try a different country flag")
raise RuntimeError("Geo-blocked, change proxy country")
resp.raise_for_status()
raise RuntimeError(f"Failed after 5 attempts for {symbol}")
# Fetch 500 candles of 1h BTCUSDT
klines = fetch_klines("BTCUSDT", "1h", limit=500)
print(f"Got {len(klines)} candles, first close: {klines[0][4]}")
Code Example 2: ProxyHat SDK with Per-Request IP Rotation
The ProxyHat SDK wraps proxy management, giving you clean per-request rotation without manually constructing proxy URLs. This example fetches /api/v3/ticker/24hr for multiple symbols concurrently using httpx with async support.
import httpx
import asyncio
import logging
from proxyhat import ProxyHatClient # ProxyHat SDK
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
BINANCE_BASE = "https://api.binance.com"
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT", "XRPUSDT"]
# Initialize ProxyHat SDK — rotating residential, Germany for Binance.com access
ph = ProxyHatClient(
username="user-country-DE",
password="your_password",
protocol="http",
# SDK handles gate.proxyhat.com:8080 internally
)
async def fetch_24hr_ticker(client: httpx.AsyncClient, symbol: str) -> dict:
url = f"{BINANCE_BASE}/api/v3/ticker/24hr"
params = {"symbol": symbol}
for attempt in range(5):
# Get a fresh rotating proxy URL for each request
proxy_url = ph.get_proxy_url() # returns http://user-country-DE:pass@gate.proxyhat.com:8080
async with httpx.AsyncClient(proxy=proxy_url, timeout=15) as proxied_client:
try:
resp = await proxied_client.get(url, params=params)
used_weight = resp.headers.get("X-MBX-USED-WEIGHT-1M", "unknown")
logging.info(f"{symbol} status={resp.status_code} weight={used_weight}")
if resp.status_code == 200:
data = resp.json()
return {"symbol": symbol, "price": data["lastPrice"], "volume": data["volume"]}
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 5))
logging.warning(f"{symbol} 429, waiting {retry_after}s")
await asyncio.sleep(retry_after)
continue
if resp.status_code == 418:
logging.error(f"{symbol} 418 banned, backing off 60s")
await asyncio.sleep(60)
continue
resp.raise_for_status()
except httpx.ProxyError as e:
logging.warning(f"{symbol} proxy error: {e}, retrying")
await asyncio.sleep(2 ** attempt)
except httpx.TimeoutException:
logging.warning(f"{symbol} timeout, retrying")
await asyncio.sleep(2 ** attempt)
raise RuntimeError(f"Failed for {symbol}")
async def main():
# Limit concurrency to 5 to avoid overwhelming any single IP
semaphore = asyncio.Semaphore(5)
async def bounded_fetch(symbol):
async with semaphore:
return await fetch_24hr_ticker(None, symbol)
results = await asyncio.gather(*[bounded_fetch(s) for s in SYMBOLS])
for r in results:
print(f"{r['symbol']}: ${r['price']} (vol: {r['volume']})")
asyncio.run(main())
Code Example 3: Sticky Sessions for Paginated Klines Backfills
When backfilling historical klines, you'll paginate through thousands of candles. Using a sticky session keeps the same IP for the entire backfill, which means your weight budget is predictable and the X-MBX-USED-WEIGHT-1M header accurately reflects your consumption. Rotate to a new session (new IP) when weight gets high.
import requests
import time
import logging
from datetime import datetime, timedelta
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
BINANCE_BASE = "https://api.binance.com"
MAX_LIMIT = 1000 # Binance allows max 1000 candles per klines request
WEIGHT_THRESHOLD = 5000 # Rotate session before hitting 6000
class KlinesBackfiller:
def __init__(self, proxy_pass: str, country: str = "DE"):
self.proxy_pass = proxy_pass
self.country = country
self.session_id = None
self.session = requests.Session()
self._new_session()
def _new_session(self):
"""Start a new sticky session with a fresh IP."""
import uuid
self.session_id = f"backfill-{uuid.uuid4().hex[:8]}"
proxy_user = f"user-country-{self.country}-session-{self.session_id}"
proxy_url = f"http://{proxy_user}:{self.proxy_pass}@gate.proxyhat.com:8080"
self.session.proxies = {"http": proxy_url, "https": proxy_url}
logging.info(f"New sticky session: {self.session_id}")
def fetch_klines_page(self, symbol: str, interval: str, start_time: int, limit: int = MAX_LIMIT) -> list:
url = f"{BINANCE_BASE}/api/v3/klines"
params = {
"symbol": symbol,
"interval": interval,
"startTime": start_time,
"limit": limit,
}
for attempt in range(5):
resp = self.session.get(url, params=params, timeout=20)
used_weight = int(resp.headers.get("X-MBX-USED-WEIGHT-1M", 0))
if resp.status_code == 200:
logging.info(f"{symbol} weight={used_weight} session={self.session_id}")
# Rotate to fresh IP if weight is getting high
if used_weight > WEIGHT_THRESHOLD:
logging.info(f"Weight {used_weight} > {WEIGHT_THRESHOLD}, rotating session")
self._new_session()
time.sleep(1) # brief cooldown
return resp.json()
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 5))
logging.warning(f"429, waiting {retry_after}s then rotating")
time.sleep(retry_after)
self._new_session()
continue
if resp.status_code == 418:
logging.error("418 banned, rotating IP and waiting 60s")
self._new_session()
time.sleep(60)
continue
resp.raise_for_status()
raise RuntimeError(f"Failed after 5 attempts")
def backfill(self, symbol: str, interval: str, start_ms: int, end_ms: int) -> list:
"""Backfill klines from start_ms to end_ms (Unix milliseconds)."""
all_klines = []
current_start = start_ms
while current_start < end_ms:
page = self.fetch_klines_page(symbol, interval, current_start)
if not page:
break
all_klines.extend(page)
# Move start to the close time of the last candle + 1ms
current_start = page[-1][6] + 1 # index 6 is closeTime
logging.info(f"{symbol} fetched {len(all_klines)} candles so far")
# Respect rate limit — klines with limit=1000 costs ~2 weight
time.sleep(0.2) # ~5 req/s, ~10 weight/s
return all_klines
# Backfill 30 days of 1m BTCUSDT candles
now_ms = int(time.time() * 1000)
start_ms = now_ms - (30 * 24 * 3600 * 1000) # 30 days ago
backfiller = KlinesBackfiller(proxy_pass="your_password", country="DE")
klines = backfiller.backfill("BTCUSDT", "1m", start_ms, now_ms)
print(f"Total candles: {len(klines)}")
Code Example 4: Weight-Aware Depth Polling with Exponential Backoff
Order book polling is the most weight-expensive operation. This example polls /api/v3/depth for multiple symbols with a weight budget controller that tracks X-MBX-USED-WEIGHT-1M and pauses before hitting the limit.
import requests
import time
import logging
from collections import defaultdict
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
BINANCE_BASE = "https://api.binance.com"
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
DEPTH_LIMIT = 100 # weight = 5 per request
MAX_WEIGHT_PER_MIN = 5500 # leave margin below 6000
PROXY_USER = "user-country-DE"
PROXY_PASS = "your_password"
PROXY_URL = f"http://{PROXY_USER}:{PROXY_PASS}@gate.proxyhat.com:8080"
class WeightAwarePoller:
def __init__(self):
self.session = requests.Session()
self.session.proxies = {"http": PROXY_URL, "https": PROXY_URL}
self.current_weight = 0
self.weight_reset_time = time.time() + 60
self.consecutive_errors = 0
def _check_weight(self):
"""Pause if we're near the weight limit."""
now = time.time()
if now > self.weight_reset_time:
self.current_weight = 0
self.weight_reset_time = now + 60
if self.current_weight >= MAX_WEIGHT_PER_MIN:
sleep_duration = self.weight_reset_time - now
logging.info(f"Weight {self.current_weight} >= {MAX_WEIGHT_PER_MIN}, sleeping {sleep_duration:.1f}s")
time.sleep(max(sleep_duration, 0))
self.current_weight = 0
self.weight_reset_time = time.time() + 60
def fetch_depth(self, symbol: str, limit: int = DEPTH_LIMIT) -> dict:
url = f"{BINANCE_BASE}/api/v3/depth"
params = {"symbol": symbol, "limit": limit}
for attempt in range(5):
self._check_weight()
try:
resp = self.session.get(url, params=params, timeout=15)
except requests.exceptions.ProxyError:
backoff = min(2 ** attempt, 30)
logging.warning(f"{symbol} proxy error, backing off {backoff}s")
time.sleep(backoff)
continue
used_weight = int(resp.headers.get("X-MBX-USED-WEIGHT-1M", self.current_weight + 5))
self.current_weight = used_weight
if resp.status_code == 200:
self.consecutive_errors = 0
data = resp.json()
return {
"symbol": symbol,
"bids": data["bids"][:10],
"asks": data["asks"][:10],
}
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 10))
logging.warning(f"{symbol} 429, sleeping {retry_after}s")
time.sleep(retry_after)
self.current_weight = 0
self.weight_reset_time = time.time() + 60
continue
if resp.status_code == 418:
logging.error(f"{symbol} 418 banned, sleeping 120s")
time.sleep(120)
self.current_weight = 0
self.weight_reset_time = time.time() + 60
continue
resp.raise_for_status()
self.consecutive_errors += 1
if self.consecutive_errors >= 10:
raise RuntimeError("Too many consecutive errors, aborting")
return None
def poll_loop(self, symbols: list, interval_sec: float = 2.0):
"""Poll depth for all symbols every interval_sec."""
while True:
for symbol in symbols:
depth = self.fetch_depth(symbol)
if depth:
best_bid = float(depth["bids"][0][0]) if depth["bids"] else 0
best_ask = float(depth["asks"][0][0]) if depth["asks"] else 0
logging.info(f"{symbol} bid={best_bid} ask={best_ask} spread={best_ask - best_bid:.4f}")
time.sleep(0.5) # stagger requests
time.sleep(interval_sec)
poller = WeightAwarePoller()
try:
poller.poll_loop(SYMBOLS, interval_sec=5.0)
except KeyboardInterrupt:
logging.info("Stopped by user")
Code Example 5: Node.js (axios) with Proxy Rotation
For JavaScript/TypeScript teams, here's an equivalent implementation using axios with a proxy agent. This example fetches ticker prices for multiple symbols with retry logic and weight tracking.
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent');
const BINANCE_BASE = 'https://api.binance.com';
const PROXY_USER = 'user-country-DE';
const PROXY_PASS = 'your_password';
// HTTP proxy via gate.proxyhat.com:8080
const HTTP_PROXY_URL = `http://${PROXY_USER}:${PROXY_PASS}@gate.proxyhat.com:8080`;
// SOCKS5 alternative via gate.proxyhat.com:1080
const SOCKS5_PROXY_URL = `socks5://${PROXY_USER}:${PROXY_PASS}@gate.proxyhat.com:1080`;
const agent = new HttpsProxyAgent(HTTP_PROXY_URL);
const SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT', 'XRPUSDT',
'ADAUSDT', 'DOGEUSDT', 'AVAXUSDT', 'DOTUSDT', 'LINKUSDT'];
const MAX_CONCURRENCY = 5;
const MAX_WEIGHT_PER_MIN = 5500;
let currentWeight = 0;
let weightResetTime = Date.now() + 60000;
function checkWeight() {
const now = Date.now();
if (now > weightResetTime) {
currentWeight = 0;
weightResetTime = now + 60000;
}
if (currentWeight >= MAX_WEIGHT_PER_MIN) {
const sleepMs = weightResetTime - now;
console.log(`Weight ${currentWeight} >= ${MAX_WEIGHT_PER_MIN}, sleeping ${sleepMs}ms`);
return new Promise(resolve => setTimeout(resolve, Math.max(sleepMs, 0)));
}
return Promise.resolve();
}
async function fetchTickerPrice(symbol, attempt = 0) {
await checkWeight();
try {
const url = `${BINANCE_BASE}/api/v3/ticker/price`;
const resp = await axios.get(url, {
params: { symbol },
httpsAgent: agent,
timeout: 15000,
});
const usedWeight = parseInt(resp.headers['x-mbx-used-weight-1m'] || '0', 10);
currentWeight = usedWeight;
console.log(`${symbol} price=${resp.data.price} weight=${usedWeight}`);
return { symbol, price: parseFloat(resp.data.price) };
} catch (err) {
if (err.response) {
const status = err.response.status;
const usedWeight = parseInt(
err.response.headers['x-mbx-used-weight-1m'] || '0', 10
);
currentWeight = usedWeight;
if (status === 429) {
const retryAfter = parseInt(err.response.headers['retry-after'] || '5', 10);
console.warn(`${symbol} 429, waiting ${retryAfter}s`);
await new Promise(r => setTimeout(r, retryAfter * 1000));
currentWeight = 0;
weightResetTime = Date.now() + 60000;
if (attempt < 5) return fetchTickerPrice(symbol, attempt + 1);
}
if (status === 418) {
console.error(`${symbol} 418 banned, waiting 120s`);
await new Promise(r => setTimeout(r, 120000));
currentWeight = 0;
weightResetTime = Date.now() + 60000;
if (attempt < 3) return fetchTickerPrice(symbol, attempt + 1);
}
if (status === 451) {
console.error(`${symbol} 451 geo-blocked — switch country flag`);
throw new Error('Geo-blocked');
}
}
const backoff = Math.min(Math.pow(2, attempt) * 1000, 30000);
console.warn(`${symbol} error, backing off ${backoff}ms`);
await new Promise(r => setTimeout(r, backoff));
if (attempt < 5) return fetchTickerPrice(symbol, attempt + 1);
throw err;
}
}
async function main() {
const results = [];
// Process in batches of MAX_CONCURRENCY
for (let i = 0; i < SYMBOLS.length; i += MAX_CONCURRENCY) {
const batch = SYMBOLS.slice(i, i + MAX_CONCURRENCY);
const batchResults = await Promise.all(
batch.map(sym => fetchTickerPrice(sym).catch(e => ({ symbol: sym, error: e.message })))
);
results.push(...batchResults);
}
console.log('\nResults:');
results.forEach(r => {
if (r.error) console.log(`${r.symbol}: ERROR ${r.error}`);
else console.log(`${r.symbol}: $${r.price}`);
});
}
main().catch(console.error);
Code Example 6: curl Quick Test
Before writing any code, verify your proxy setup with a single curl command:
# Test klines endpoint through ProxyHat residential proxy (Germany exit)
curl -x "http://user-country-DE:your_password@gate.proxyhat.com:8080" \
"https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1h&limit=5" \
-H "Accept: application/json" \
-D - # dump headers to see X-MBX-USED-WEIGHT-1M
# Test ticker/price through SOCKS5
curl -x "socks5://user-country-DE:your_password@gate.proxyhat.com:1080" \
"https://api.binance.com/api/v3/ticker/price?symbol=ETHUSDT"
# Test Binance.US with US exit IP
curl -x "http://user-country-US:your_password@gate.proxyhat.com:8080" \
"https://api.binance.us/api/v3/ticker/price?symbol=BTCUSD"
When to Use WebSocket Streams Instead of REST Polling
Binance offers public WebSocket streams at wss://stream.binance.com:9443 that push real-time kline and depth updates. For live data, WebSockets are almost always better than REST polling:
| Use Case | REST Polling | WebSocket Streams |
|---|---|---|
| Historical backfill | ✅ Best (paginated klines) | ❌ Not designed for history |
| Real-time price feed | ⚠️ Latency, weight cost | ✅ Push-based, ~0 weight |
| Live order book | ⚠️ 5–20 weight per poll | ✅ Diff updates, low overhead |
| Multi-symbol snapshot | ✅ One request, all symbols | ❌ Need one stream per symbol |
| Batch processing / ETL | ✅ Natural fit | ❌ Requires stream management |
The rule of thumb: use REST for historical data and batch snapshots, use WebSocket for continuous real-time monitoring. If you're polling /api/v3/depth more than once per second per symbol, switch to the <symbol>@depth WebSocket stream. You'll save enormous weight budget and get lower latency. For more on web scraping strategies at scale, see our web scraping use case guide.
Common Mistakes and Edge Cases
1. Ignoring the X-MBX-USED-WEIGHT-1M Header
The single most common mistake. This header is your real-time fuel gauge. If you don't read it, you're flying blind and will hit 429s unpredictably. Every code example above reads and logs this header.
2. Using Datacenter Proxies for High-Frequency Polling
Datacenter IP ranges are well-known and more aggressively rate-limited by exchanges. Residential proxies distribute your requests across real ISP IPs, reducing the chance of collective bans. For Binance specifically, residential proxies from non-US countries give the best results for api.binance.com.
3. Not Handling HTTP 451 (Geo-Restriction)
If you accidentally route through a US IP to api.binance.com, you'll get 451 errors. Always set the country flag explicitly: -country-DE for Binance.com, -country-US for Binance.US.
4. No Exponential Backoff on 429/418
Retrying immediately after a 429 is the fastest path to a 418 ban. Always respect the Retry-After header and add exponential backoff for non-429 errors (timeouts, proxy errors).
5. Over-Concurrent Requests from a Single IP
Even with weight headroom, sending 50 concurrent requests from one IP looks suspicious. Use semaphores or connection pools to limit concurrency to 5–10 per IP. With ProxyHat's rotating proxies, each request naturally gets a different IP, but sticky sessions need explicit concurrency control.
Key Takeaways
- Weight is per IP — rotating residential proxies through
gate.proxyhat.com:8080gives each request its own ~6,000/min budget.- Always read
X-MBX-USED-WEIGHT-1M— it's your real-time rate limit indicator. Throttle or rotate when it exceeds 5,000.- Handle 429 and 418 distinctly — 429 means slow down (check
Retry-After); 418 means you're banned (wait longer, rotate IP).- Use sticky sessions for backfills — the
-session-abc123username flag keeps one IP for predictable weight tracking during pagination.- Prefer WebSocket for real-time data — REST polling
/api/v3/depthat 1Hz is wasteful; use<symbol>@depthstreams instead.- Geo-target with country flags —
-country-DEfor Binance.com,-country-USfor Binance.US to avoid HTTP 451.- Respect the Terms of Use — only collect public market data, honor weight limits, and check ProxyHat docs for the latest gateway configuration.
FAQ
What is Binance REST API scraping with proxies?
Binance REST API scraping with proxies is the practice of collecting public market data (klines, depth, ticker prices) from Binance's REST endpoints while routing requests through rotating proxy IPs. Because Binance's rate limits are per-IP (approximately 6,000 weight units per minute), rotating residential proxies through a gateway like gate.proxyhat.com:8080 distributes weight consumption across many IPs, preventing any single IP from being banned.
Why does proxy rotation matter for Binance API scraping?
Binance enforces weight-based rate limits per IP address, not per API key. A single IP polling /api/v3/depth with limit=1000 (20 weight per call) at 10 symbols/second burns through the 6,000/min budget in 30 seconds, triggering HTTP 429 and then HTTP 418 bans. Rotating residential proxies give each request a fresh IP with its own budget, dramatically increasing sustainable throughput.
Which proxy type works best for Binance API scraping?
Residential proxies are the best choice for Binance API scraping. They use real ISP-assigned IP addresses, making them less likely to be flagged by Binance's anti-abuse systems compared to datacenter IPs. For Binance.com access, use non-US residential exits (e.g., -country-DE). For Binance.US, use US residential exits (-country-US). SOCKS5 on port 1080 is available if you need protocol-level flexibility.
How do you avoid IP bans when scraping the Binance REST API?
To avoid bans: (1) always read the X-MBX-USED-WEIGHT-1M header and throttle before reaching 6,000; (2) respect Retry-After on 429 responses; (3) rotate IPs per request using ProxyHat's rotating residential proxies; (4) use sticky sessions for paginated backfills and rotate sessions when weight exceeds ~5,000; (5) limit concurrency to 5–10 per IP; (6) prefer WebSocket streams for real-time data instead of REST polling.
Can I use the Binance REST API without an API key for market data?
Yes. Public market-data endpoints like /api/v3/klines, /api/v3/depth, /api/v3/ticker/24hr, and /api/v3/ticker/price do not require authentication. However, you still consume per-IP weight on every request, so rate limiting applies regardless of whether you use an API key. Always check Binance's official API documentation for the current weight values and terms.






