๐ŸŽ‰ Premium Proxies ยท 3-Day Free TrialClaim Now โ†’
Use Cases

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.

S SparkProxy 1 22 min read
Share
Proxies for Crypto Trading Bots: Limits and Latency

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 typeUsually keyed toDoes a proxy pool help?
REST request weight or raw request countIPYes, directly
WebSocket connection attempts per intervalIPYes
Concurrent WebSocket connectionsIPYes
Streams multiplexed per connectionThe connectionNo
Order placement rate (orders per 10s, per day)Account or API keyNo
Private endpoint call budgets (Kraken counter, Coinbase Advanced)Account or API keyNo
Withdrawal and transfer limitsAccountNo

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:

  1. Pick a cheap, weighted public endpoint on the venue.
  2. Fire 200 requests from IP A with API key K, recording the used-weight header after each one.
  3. 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.

Free trial

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 callDocumented 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.

TopologyTypical added round tripSensible use
Proxy in the same availability zone, exchange in the same region1 to 3 msExtra IPs at near-zero cost
Proxy in the same metro, different provider3 to 10 msRedundant egress, most read paths
Proxy on a different continent from the exchange edge80 to 250 msResearch and backfill only
Rotating residential exit, arbitrary geography200 ms to several seconds, high varianceNot 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 planeOrder execution plane
TrafficDepth streams, trades, klines, REST snapshotsPlace, cancel, amend, account state
VolumeHigh and continuousLow and bursty
Binding limitPer-IP weight and connection capsPer-account order rate
IP strategySeveral proxied exits, sharded by symbolOne static IP, no proxy
Failure toleranceDegrade and resyncFail closed
API keyRead-only keyTrade-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.

SignalWhat it meansCorrect response
HTTP 429 with `Retry-After`You crossed the weight ceilingPark that exit for the stated interval, move its shard
HTTP 418 (Binance)IP banned, escalating durationRemove the exit for the full window, alert a human
Repeated timeouts on one exit onlyProxy or route degraded, not the exchangeFail the exit, confirm with a second before blaming the venue
Exchange 5xx across every exitVenue-side incidentBack off globally, halt new orders, do not rotate
WebSocket closes at a fixed intervalIdle timeout or connection lifetimeScheduled 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 typeLatencyStabilityFit for exchange APIs
Dedicated datacenterLowestHigh, staticBest default for market data
Static ISPLowHigh, static, residential ASNGood where a datacenter ASN is treated poorly
Shared datacenterLowMedium, neighbours consume shared limitsAcceptable for backfill, risky for live data
Rotating residentialHigh and variableLow, exits change under youWrong 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:

  1. Confirm empirically which limits are IP-keyed on your venue, and date the finding.
  2. Move every real-time series off REST polling and onto streams.
  3. Measure p50, p95 and p99 latency direct versus proxied, per exit.
  4. Verify the order path is direct, static, and whitelisted on the exchange key.
  5. Verify the data plane uses a read-only key.
  6. Force a 429 in staging and confirm the pool parks that exit instead of retrying it.
  7. Kill a stream mid-session and confirm the book resyncs from a fresh snapshot rather than patching a gap.
  8. Confirm the staleness guard actually cancels open orders instead of logging a warning.

Then watch these continuously:

MetricAlert when
Used weight as a fraction of budget, per exitSustained above 0.75
Book staleness (now minus last update), per symbolAbove your configured threshold
WebSocket reconnects per hour, per exitAbove your normal baseline
429 and 418 counts, per exitAny 418, or a rising 429 rate
Exit p99 latencyDrifts beyond 2x its 7-day baseline
Healthy exits in poolBelow 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.

Limited-time ยท 50% off

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

Claim Discount

About the Author

This guide was written by the SparkProxy Technical Team. SparkProxy provides dedicated and shared datacenter proxies, static ISP and residential IPs, and a managed Scraping API, and we support teams running continuous market data collection against exchange APIs and public web sources. The goal here was to be specific about where a proxy layer genuinely adds capacity for a trading bot, honest about where it adds none at all, and clear about the jurisdictional line we will not help anyone cross. Questions about pool sizing or exit placement for a specific venue can go to support@sparkproxy.io.

Keep reading

Related articles

Proxies for App Store Optimization (ASO) Data

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.

SparkProxyยทUse Cases