Proxies for Crypto Trading Bots: Limits and Latency
Proxies for crypto trading bots: which exchange rate limits are keyed to your IP, what a proxy hop costs in latency, and how to fail over when throttled.

Proxies for crypto trading bots solve one narrow class of problem well: limits and connection caps that an exchange enforces per IP address. They do nothing for limits keyed to your API key, and they make things worse if you bolt one onto the order path of a latency-sensitive strategy. This guide covers which limits are IP-keyed on the major venues, how to test that empirically instead of guessing, what a proxy hop costs in round-trip time, how to keep long-lived WebSocket order book streams alive behind one, and how to fail over safely when an IP gets throttled.
Key Takeaways
- Request weight and WebSocket connection caps are usually keyed to the IP. Order placement rates are usually keyed to the account or API key. Proxies raise the first ceiling and never touch the second.
- The first fix for a weight problem is not more IPs. It is moving polled REST data onto a WebSocket stream, which costs zero request weight.
- Put proxies on the market data path only. Order traffic goes direct from one static, IP-whitelisted host.
- Rotating residential IPs are the wrong tool here. Exchange APIs authenticate with an HMAC signature, so there is no fingerprint to defeat, only latency and stability to protect.
- When the data plane degrades, the execution plane must fail closed. Stale book plus live orders is the expensive failure mode.
What proxies will not fix
Start here, because most advice on this subject is either useless or a way to get your account frozen.
Geo-restrictions and KYC are off limits. Exchanges block jurisdictions because regulators require them to. Binance restricts users in the United States, several venues exclude sanctioned countries entirely, and specific derivative products are unavailable across whole regions. Routing around that with a proxy breaks the exchange's terms of service, and depending on where you sit it can also break sanctions or securities law. The practical outcome is worse than the legal one for most people: accounts flagged during a KYC review get frozen with positions open, and withdrawals sit on hold while you fail a verification you were never going to pass. This article is written for the operator trading where they are permitted to trade, who needs rate limit headroom, redundancy and predictable latency. Nothing here is about pretending to be somewhere you are not.
Limits keyed to your API key do not care about your IP. Order placement counters, private endpoint budgets on several venues, and daily account quotas follow the credential. Ten IPs multiplied by one API key is still exactly one API key's worth of order throughput. If your bot is hitting an order rate limit, a proxy pool is money spent on nothing. The fix is fewer and smarter orders, or a second sub-account where the exchange permits one.
Microsecond execution is a colocation problem, not a proxy problem. Every proxy adds a hop. If your edge lives in the tail of the latency distribution, the answer is a machine near the matching engine, not an extra network detour. Proxies belong on the read path.
Where the limit actually lives: IP or API key
This is the question that decides whether the rest of the project is worth building. Exchange documentation is inconsistent about it, so treat the table below as a starting hypothesis and verify on your own venue.
| Limit type | Usually keyed to | Does a proxy pool help? |
|---|---|---|
| REST request weight or raw request count | IP | Yes, directly |
| WebSocket connection attempts per interval | IP | Yes |
| Concurrent WebSocket connections | IP | Yes |
| Streams multiplexed per connection | The connection | No |
| Order placement rate (orders per 10s, per day) | Account or API key | No |
| Private endpoint call budgets (Kraken counter, Coinbase Advanced) | Account or API key | No |
| Withdrawal and transfer limits | Account | No |
Binance spot is the clearest published example of the split. Its REST weight budget is documented as per IP, and every response carries X-MBX-USED-WEIGHT-1M so you can watch the counter climb. Order counts are separate and account-scoped, reported through X-MBX-ORDER-COUNT-10S and X-MBX-ORDER-COUNT-1D. Two different counters, two different keys, one request. That header pair tells you more about your architecture than any vendor page will.
Test it in ten minutes instead of guessing. Documentation drifts and exchanges change enforcement without much fanfare. Run this before you buy anything:
- Pick a cheap, weighted public endpoint on the venue.
- Fire 200 requests from IP A with API key K, recording the used-weight header after each one.
- Immediately fire 200 requests from IP B with the same API key K, recording the same header.
If the counter on IP B starts near zero, the limit is IP-keyed and a pool buys you real capacity. If it picks up where IP A left off, the limit follows the key and no amount of network topology will change it. Repeat with two keys on one IP to confirm the other direction. Write the result into your runbook with a date on it, because this is exactly the kind of fact that silently expires.
Understanding concurrent connections in proxies covers how connection caps behave when several workers share one gateway.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Weight-based rate limiting, with the arithmetic
Modern exchange APIs do not count requests, they count cost. Each endpoint carries a weight, and heavier responses cost more. Binance spot publishes weights that scale with the depth you request:
| Endpoint call | Documented request weight |
|---|---|
| `/api/v3/depth?limit=100` | 5 |
| `/api/v3/depth?limit=500` | 25 |
| `/api/v3/depth?limit=1000` | 50 |
| `/api/v3/depth?limit=5000` | 250 |
| `/api/v3/exchangeInfo` | 20 |
Against a 6,000 weight per minute per IP budget, do the multiplication before you write any code. Say you want a 1,000 level book on 40 symbols, refreshed every 5 seconds:
40 symbols x 12 refreshes/min = 480 requests/min
480 requests x 50 weight = 24,000 weight/min
24,000 / 6,000 budget = 4.0x over a single IP's ceiling
Four IPs makes the number fit. That is the answer most articles stop at, and it is the wrong first move. The same book is available as a diff depth WebSocket stream that costs zero REST weight, because streams are pushed rather than polled. Move the 40 symbols onto streams, keep REST for the initial snapshot and periodic resync, and weight consumption drops by more than two orders of magnitude. Now you need one IP, plus a second for redundancy.
The rule that falls out: spend proxy IPs on the requests that genuinely have to be REST, which is usually account state, historical klines backfill, and cross-venue reference data. Anything real time belongs on a socket.
Reading the counter and pacing against it beats a fixed sleep, because weight cost varies per call:
import time
import requests
BUDGET = 6000 # weight per minute per IP, verify on your venue
SAFETY = 0.75 # never spend more than 75% of the ceiling
class WeightGuard:
def __init__(self, session, base="https://api.example-exchange.io"):
self.s, self.base = session, base
self.used = 0
def get(self, path, **params):
if self.used > BUDGET * SAFETY:
# sleep to the top of the next minute window, not a flat delay
time.sleep(60 - (time.time() % 60) + 0.25)
self.used = 0
r = self.s.get(f"{self.base}{path}", params=params, timeout=10)
hdr = r.headers.get("X-MBX-USED-WEIGHT-1M")
if hdr:
self.used = int(hdr) # trust the server, not your own tally
r.raise_for_status()
return r.json()
Trusting the server-reported counter matters because a local tally drifts. Retries, redirects and shared IPs all consume weight you never accounted for.
Wiring a pool into an existing bot is usually a two-line change. Most Python exchange clients accept standard proxy settings, and ccxt passes them through to the underlying HTTP session:
import ccxt
DATA_IPS = [
"http://user:pass@gw.sparkproxy.io:8000?session=md-01",
"http://user:pass@gw.sparkproxy.io:8000?session=md-02",
"http://user:pass@gw.sparkproxy.io:8000?session=md-03",
]
def market_data_client(slot: int):
ex = ccxt.binance({"enableRateLimit": True})
proxy = DATA_IPS[slot % len(DATA_IPS)]
ex.proxies = {"http": proxy, "https": proxy}
return ex
# One client per shard of symbols, each pinned to its own exit IP.
shards = [market_data_client(i) for i in range(len(DATA_IPS))]
Pin a shard of symbols to a slot rather than round-robining every call. Round robin spreads each symbol's history across every IP, so when one exit gets throttled you lose a slice of every series instead of one clean shard you can re-fetch.
Latency: what a proxy hop really costs
A proxy inserts a machine between your bot and the exchange. The cost is the difference between the direct path and the two-leg path, plus a small amount of processing. Steady state is what matters, since a persistent tunnel pays its TLS handshake once and then behaves like a longer wire.
| Topology | Typical added round trip | Sensible use |
|---|---|---|
| Proxy in the same availability zone, exchange in the same region | 1 to 3 ms | Extra IPs at near-zero cost |
| Proxy in the same metro, different provider | 3 to 10 ms | Redundant egress, most read paths |
| Proxy on a different continent from the exchange edge | 80 to 250 ms | Research and backfill only |
| Rotating residential exit, arbitrary geography | 200 ms to several seconds, high variance | Not for exchange traffic |
Those bands are what you should expect to measure, not constants to trust. Measure yours before committing:
import statistics, time, requests
TARGET = "https://api.example-exchange.io/api/v3/time"
def sample(proxies=None, n=50):
s = requests.Session()
s.get(TARGET, proxies=proxies, timeout=10) # warm the connection
lat = []
for _ in range(n):
t0 = time.perf_counter()
s.get(TARGET, proxies=proxies, timeout=10)
lat.append((time.perf_counter() - t0) * 1000)
lat.sort()
return {
"p50": round(statistics.median(lat), 1),
"p95": round(lat[int(n * 0.95) - 1], 1),
"p99": round(lat[-1], 1),
}
direct = sample()
proxied = sample({"https": "http://user:pass@gw.sparkproxy.io:8000"})
print("direct ", direct)
print("proxied", proxied)
print("p95 delta ms:", round(proxied["p95"] - direct["p95"], 1))
Compare p95 and p99, not the median. A proxy that adds 2 ms at the median and 400 ms at p99 looks fine on a dashboard and quietly ruins a strategy that trades the tail.
The colocation question follows from the same measurement. If a strategy's profitability decays inside a few hundred milliseconds, the geography of your compute dominates everything else, and you place the bot in the cloud region where the venue's matching infrastructure lives rather than adding hops to a badly placed one. Binance has long been associated with AWS Tokyo (ap-northeast-1) and Coinbase with AWS us-east-1, though you should verify against current documentation before reserving a year of instances. A proxy cannot recover a 150 ms geographic deficit. It can only add to it.
Keeping long-lived WebSocket streams alive
Order book streams are the part people get wrong, because the proxy behaviour that helps scrapers actively breaks them.
A market data socket is a single TCP connection expected to stay open for hours. Rotating proxies exist to change your exit IP frequently, which tears that connection down. What you want is the opposite: a sticky exit that holds one IP for the life of the session. If your provider exposes session identifiers, pin one per stream, and read what a sticky session proxy is if that concept is new. Static ISP or dedicated datacenter IPs are cleaner still, because there is no session to expire.
Protocol choice matters too. SOCKS5 tunnels arbitrary TCP and handles full duplex traffic without opinions about it. An HTTP proxy works only if it implements CONNECT properly and does not idle out a quiet tunnel. Order book streams go quiet during thin liquidity, and an aggressive idle timeout will drop you at exactly the wrong moment.
import asyncio, json
from websockets_proxy import Proxy, proxy_connect
STREAM = "wss://stream.example-exchange.io/ws/btcusdt@depth@100ms"
EXIT = Proxy.from_url("socks5://user:pass@gw.sparkproxy.io:1080")
async def book_stream(on_event):
backoff = 1
while True:
try:
async with proxy_connect(STREAM, proxy=EXIT,
ping_interval=20, ping_timeout=20,
close_timeout=5) as ws:
backoff = 1
async for raw in ws:
on_event(json.loads(raw))
except Exception as e:
print("stream down:", type(e).__name__, e)
await asyncio.sleep(backoff)
backoff = min(backoff * 2, 30) # never hammer the connect endpoint
Three details separate a stream that survives a week from one that dies nightly.
Respect the connection lifetime. Binance documents a maximum lifetime for a single WebSocket connection, after which the server closes it regardless of health. Plan a scheduled reconnect rather than treating that close as an incident, and stagger reconnects across shards so you do not create your own thundering herd against a per-IP connection cap.
Keepalives are not optional. Exchanges send periodic pings and disconnect clients that stop responding. A proxy in the middle has to pass control frames through. If you see clean closes at suspiciously regular intervals, the proxy is eating your pongs.
Resync the book, do not patch over gaps. Depth diffs carry sequence numbers. If the sequence jumps, your local book is wrong and every price it produces is wrong. Drop it, pull a fresh REST snapshot, and replay buffered diffs from the snapshot's update ID. A bot that keeps trading through a sequence gap is trading on fiction.
def apply_diff(book, msg):
if msg["U"] > book.last_id + 1: # gap detected
raise Resync(f"gap: local {book.last_id}, event starts {msg['U']}")
if msg["u"] <= book.last_id: # stale, already applied
return
book.merge(msg["b"], msg["a"])
book.last_id = msg["u"]
Split the data plane from the execution plane
The architecture that holds up under load separates two kinds of traffic with opposite requirements.
| Market data plane | Order execution plane | |
|---|---|---|
| Traffic | Depth streams, trades, klines, REST snapshots | Place, cancel, amend, account state |
| Volume | High and continuous | Low and bursty |
| Binding limit | Per-IP weight and connection caps | Per-account order rate |
| IP strategy | Several proxied exits, sharded by symbol | One static IP, no proxy |
| Failure tolerance | Degrade and resync | Fail closed |
| API key | Read-only key | Trade-enabled key, IP whitelisted |
The API key whitelist is the operational detail that forces the split, and most write-ups miss it. Every serious exchange lets you bind a trade-enabled key to specific source IPs, and you should use it, because a leaked key that only works from one address is a far smaller incident. The moment you whitelist, order traffic must originate from that exact address, so a rotating exit on the order path breaks trading outright. The two constraints resolve cleanly once you stop running both kinds of traffic through the same egress.
# bot.yaml
market_data:
transport: websocket
exits:
- socks5://user:pass@gw.sparkproxy.io:1080?session=md-01
- socks5://user:pass@gw.sparkproxy.io:1080?session=md-02
shard_by: symbol
max_staleness_ms: 1500
execution:
transport: rest
exit: direct # static host IP, whitelisted on the exchange key
api_key_scope: trade
halt_on_stale_data: true
Read-only keys on the data plane are worth the extra minute of setup. Market data collection at this scale has plenty in common with the pipelines described in proxies for cryptocurrency data, and the same principle applies: the component that only reads should only be able to read.
Failover when an IP gets throttled
Throttling arrives as a status code and escalates if you ignore it. On Binance a 429 is the warning and a 418 is an IP ban, with documented ban durations that escalate from two minutes up to three days for repeat offenders. Retrying harder is the single most expensive mistake available here.
| Signal | What it means | Correct response |
|---|---|---|
| HTTP 429 with `Retry-After` | You crossed the weight ceiling | Park that exit for the stated interval, move its shard |
| HTTP 418 (Binance) | IP banned, escalating duration | Remove the exit for the full window, alert a human |
| Repeated timeouts on one exit only | Proxy or route degraded, not the exchange | Fail the exit, confirm with a second before blaming the venue |
| Exchange 5xx across every exit | Venue-side incident | Back off globally, halt new orders, do not rotate |
| WebSocket closes at a fixed interval | Idle timeout or connection lifetime | Scheduled reconnect with jitter, not a retry loop |
The distinction between rows three and four is what a per-exit circuit breaker gives you. A global breaker cannot tell a bad proxy from a bad exchange, so it either halts everything because of one broken exit or keeps hammering a venue that is already down.
import time, random
class ExitPool:
def __init__(self, exits):
self.exits = {e: 0.0 for e in exits} # exit -> cooldown_until
def acquire(self):
now = time.time()
live = [e for e, until in self.exits.items() if until < now]
if not live:
raise NoHealthyExit(f"all {len(self.exits)} exits cooling down")
return random.choice(live)
def penalise(self, exit_url, status, retry_after=None):
if status == 429:
self.exits[exit_url] = time.time() + (retry_after or 60)
elif status == 418:
self.exits[exit_url] = time.time() + 300 # widen on repeat offences
else:
self.exits[exit_url] = time.time() + 15
The trading-specific half of failover has nothing to do with proxies. When the data plane degrades, the execution plane has to fail closed. A bot still quoting from a book that stopped updating 40 seconds ago is not degraded, it is wrong, and it will fill every one of those quotes against someone who can see the current price.
def guard(book, clock):
age_ms = (clock.now() - book.last_update) * 1000
if age_ms > CONFIG["max_staleness_ms"]:
engine.cancel_all()
engine.halt(reason=f"book stale {age_ms:.0f}ms")
Cancel first, halt second, alert third. General backoff patterns carry over from data collection work, and retry and backoff strategies covers the jitter and cap mechanics in more depth.
Choosing a proxy type for a trading bot
Scraping guides default to residential IPs because scraping targets fingerprint visitors and score IP reputation. Exchange APIs do not work that way. You authenticate with an HMAC signature over your request, so the venue knows exactly who you are regardless of which address the packet came from. There is no trust signal to buy. What you need instead is low, stable latency and an address nobody else is abusing.
| Proxy type | Latency | Stability | Fit for exchange APIs |
|---|---|---|---|
| Dedicated datacenter | Lowest | High, static | Best default for market data |
| Static ISP | Low | High, static, residential ASN | Good where a datacenter ASN is treated poorly |
| Shared datacenter | Low | Medium, neighbours consume shared limits | Acceptable for backfill, risky for live data |
| Rotating residential | High and variable | Low, exits change under you | Wrong tool, breaks long-lived sockets |
Dedicated capacity earns its price on the data plane for one specific reason: a shared IP shares the weight budget. If a stranger on your address is polling the same exchange, their consumption counts against the same per-IP ceiling as yours, and you will spend a week debugging a limit you never crossed. That is the whole argument in one sentence. Where a datacenter ASN causes trouble on a particular venue's web surfaces, ISP proxies give you a residential ASN without giving up the static address.
Data the exchange API does not give you
Plenty of information that moves crypto markets never appears in a REST endpoint. Listing announcements, maintenance and delisting notices, token vesting schedules, governance proposals and status pages are published as web pages, often rendered client side. A bot that wants to react to a listing before the ticker exists has to read the page.
That is a different job from talking to an API, with different failure modes: JavaScript rendering, anti-bot challenges, and layout changes. The SparkProxy Scraping API handles rendering and rotation so your bot only deals with the parsed result:
curl -H "X-API-Key: YOUR_API_KEY" \
"https://scrape.sparkproxy.io/api/v1?url=https://www.example-exchange.io/support/announcement&render_js=true&json_response=true"
Pulling structured fields directly avoids shipping a parser that breaks on the next redesign:
import requests
resp = requests.get(
"https://scrape.sparkproxy.io/api/v1",
headers={"X-API-Key": "YOUR_API_KEY"},
params={
"url": "https://www.example-exchange.io/support/announcement/new-listings",
"render_js": "true",
"wait_for": ".announcement-list",
"json_response": "true",
"tag": "listing-watch",
"extract_rules": '{"titles": ".announcement-list a", "dates": ".announcement-list time"}',
},
timeout=90,
)
resp.raise_for_status()
data = resp.json()
for title, seen in zip(data["titles"], data["dates"]):
if "will list" in title.lower():
alerts.publish({"headline": title, "seen_at": seen})
Two things to budget for. Rendering costs more than a plain fetch, 5 credits with the rotating pool against 1 with render_js=false, per the Scraping API docs, so turn rendering off for pages whose raw HTML already carries the text. And for anything you do not need synchronously, pass callback_url to get a 202 back immediately and have the result POSTed to your service, which keeps a slow page from blocking a polling loop.
Keep this pipeline entirely off the execution path. Announcement scraping is a signal source, and signal sources belong behind the same staleness and sanity checks as everything else feeding the engine.
Pre-flight checklist and monitoring
Before a bot with a proxy layer goes live:
- Confirm empirically which limits are IP-keyed on your venue, and date the finding.
- Move every real-time series off REST polling and onto streams.
- Measure p50, p95 and p99 latency direct versus proxied, per exit.
- Verify the order path is direct, static, and whitelisted on the exchange key.
- Verify the data plane uses a read-only key.
- Force a 429 in staging and confirm the pool parks that exit instead of retrying it.
- Kill a stream mid-session and confirm the book resyncs from a fresh snapshot rather than patching a gap.
- Confirm the staleness guard actually cancels open orders instead of logging a warning.
Then watch these continuously:
| Metric | Alert when |
|---|---|
| Used weight as a fraction of budget, per exit | Sustained above 0.75 |
| Book staleness (now minus last update), per symbol | Above your configured threshold |
| WebSocket reconnects per hour, per exit | Above your normal baseline |
| 429 and 418 counts, per exit | Any 418, or a rising 429 rate |
| Exit p99 latency | Drifts beyond 2x its 7-day baseline |
| Healthy exits in pool | Below the count your shards require |
That last row catches slow decay. Pools rarely fail all at once. They lose one exit at a time until the survivors carry more shards than their weight budget allows, and the symptom looks exactly like an exchange problem. Proxy uptime and reliability is worth reading alongside this if you are still choosing a provider, because for trading traffic the variance in latency matters more than the headline uptime number.
Frequently asked questions
FAQ
They increase the limits keyed to your IP address, such as REST request weight budgets and WebSocket connection caps, because each exit gets its own counter. They do not increase limits keyed to your API key or account, including order placement rates and account-level quotas. Test which is which by running identical calls from two IPs on one key and watching the used-weight header.
No. A proxy adds a network hop and can only increase latency. It raises throughput by giving you more IP-keyed capacity, which is a different thing from speed. If crypto trading bot latency is your binding constraint, place your compute in the cloud region where the exchange's infrastructure sits and keep the order path direct.
No, and you should not try. Exchanges restrict jurisdictions to comply with regulators, so using a proxy to bypass that violates their terms and can breach sanctions or securities law where you live. In practice it also ends with a frozen account and blocked withdrawals at the next KYC review. Use proxies for rate limit headroom and redundancy where you are already permitted to trade.
Datacenter or static ISP proxies, dedicated rather than shared. Exchange APIs authenticate you by HMAC signature, so there is no IP reputation to buy, and residential exits add latency and change under you. Shared datacenter IPs are risky for live data because neighbours consume the same per-IP weight budget you do.
Yes, provided the exit is sticky for the life of the connection and the proxy passes ping and pong control frames through. SOCKS5 fits a long-lived duplex websocket order book stream better than HTTP CONNECT. Rotating exits are unusable here, because rotation tears the connection down by design.
Send a fixed number of weighted requests from IP A with key K and record the counter header, then immediately repeat from IP B with the same key K. A counter that starts near zero on IP B means the limit is IP-keyed. A counter that continues from where IP A left off means it follows the key, and adding IPs will not help.
Get 50% off your first month
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Offer ends soon โ claim it before it's gone
Related articles

Proxies for Freight and Logistics Rate Monitoring
Freight rate monitoring fails when you treat a quote like a price. Model expiry, split the surcharge stack, and know when to buy the data instead.

Proxies for Automotive Listings Aggregation at Scale
Proxies for automotive listings aggregation: VIN joins, cross-portal dedupe, trim normalisation, price history, relist detection, and GDPR-safe schema.

Proxies for App Store Optimization (ASO) Data
Use proxies for app store optimization to track keyword ranks, chart positions, review sentiment, and competitor releases in every country storefront.
