VIZANIXTrading Software Development
Bybit APIBybit API8 min read

Bybit rate limits: designing a request queue that survives a bad minute

Limits never bite when the market is calm. They bite in the minute when your bot urgently needs to cancel, and that is the minute the design has to be built for.

Vizanix engineering · about the author

ARTICLE
8 minreading time
SECTION
Bybit API
PUBLISHED
2026-08-28
CHAPTERS
6
READ NEXT
3
LANGUAGE
written in English
An engineering breakdown, not a rewrite of the docs.
RATE LIMITSTOKEN BUCKETPRIORITYBACKOFFIDEMPOTENCY

Rate limiting looks like a throughput problem and is actually a scheduling problem. Your bot rarely needs its maximum request rate. It needs the right request to go out first when everything is happening at once.

Consider the scenario that actually costs money. Price gaps. Three positions hit their stop conditions simultaneously. The market data stream is flooding. Your quoting logic wants to refresh forty orders. And somewhere in that queue sits the cancel that protects a position. If the queue is first-in-first-out, that cancel goes out after the forty quote refreshes.

Two buckets, not one

Bybit counts limits along more than one axis — per IP and per account UID. A single global counter in your bot models neither correctly. Two bots on one host share the IP budget; one bot with two API keys shares the UID budget per key but not across them.

Model it as it is: a bucket keyed by IP and a bucket keyed by UID, and a request must acquire a token from both before it goes out. This is a few dozen lines and it removes an entire category of mystery rejections.

python
class Bucket:
    """Token bucket. Refills continuously, never in bursts on a timer."""
    def __init__(self, rate_per_sec, burst):
        self.rate, self.capacity = rate_per_sec, burst
        self.tokens, self.ts = burst, time.monotonic()

    async def take(self, n=1):
        while True:
            now = time.monotonic()
            self.tokens = min(self.capacity,
                              self.tokens + (now - self.ts) * self.rate)
            self.ts = now
            if self.tokens >= n:
                self.tokens -= n
                return
            await asyncio.sleep((n - self.tokens) / self.rate)

async def send(req):
    await ip_bucket.take()      # both budgets, always
    await uid_bucket[req.key].take()
    return await http(req)

Priority is the part that matters

Give every request a class and serve the queue by class, not by arrival time. A workable ordering:

PriorityRequestWhy
0 — highestCancel, reduce-only, flattenReduces risk. Must never wait behind anything.
1Stop-loss / take-profit placementProtects an open position that currently has no cover.
2Entry ordersMissing an entry costs opportunity, not capital.
3Quote refresh, amendHigh volume, individually low value.
4 — lowestBalance, position and instrument pollingShould mostly come from the WebSocket stream anyway.

The discipline this enforces is useful beyond rate limiting: it forces you to state, in code, that reducing risk outranks expressing an opinion. Most bots that blow up under load never wrote that down anywhere.

Backoff with jitter, and a ceiling

On 10006 too many visits, the wrong response is an immediate retry — it deepens the hole. The right one is exponential backoff with random jitter, so that a bot with many parallel workers does not resynchronise them into a thundering herd.

Add a ceiling and an alert. A backoff that has been doubling for two minutes is no longer a transient; something structural is wrong and a human should know. Silent infinite retry is how a bot stays “up” while doing nothing.

Retries need a client order ID

A timeout on an order placement is the dangerous case: the request may have succeeded. Retrying blindly can produce two positions. A client-supplied order ID makes the retry idempotent — the venue recognises the duplicate and rejects it instead of executing it.

Generate the ID before the first attempt, keep it for every retry of that logical order, and store it alongside your local order state so that reconciliation after a disconnect can match on it.

Let the venue tell you where you stand

Responses carry headers with the remaining quota. Read them and feed them back into your buckets rather than trusting your own count — your model can drift from reality after retries, restarts, or a second process sharing the IP.

Export the remaining budget as a metric. A quota that trends toward zero during normal operation is telling you that the next volatile minute will hurt, and it is telling you in advance.

Reduce the load instead of scheduling it better

The cheapest request is the one you do not send. Most bots that fight rate limits are polling for information the WebSocket already pushes: positions, balances, order status. Subscribe once, keep local state, and reconcile via REST only after a reconnect or on a slow timer as a safety net.

Batching helps too, where the venue supports it: batched cancels and amends turn a quoting engine's worst moment from forty requests into a handful. That single change is usually worth more than any amount of queue tuning.

This article describes engineering practice. It is not investment advice. Vizanix develops software and does not promise trading returns.

Blog

Read next

Want this running for you?

We write about what we build. If you need it built, get in touch — scoping is free.

Brief

Get a project estimate

Four questions and your contact. No deposit required to talk — if the job is not a fit, we say so straight away.

01What do you need
02Exchange
03Market
04Strategy
05Contacts

Prefer to write directly? Telegram @vx_ceo

Discuss a system