Why Your Hyperliquid Bot Hits the Rate Limit

The hyperliquid api rate limit 429 most bots hit is not a per-request cap. Hyperliquid's REST endpoints share one aggregated weight budget of 1,200 per minute per IP, and every /info request type has its own weight. A naive candle poller that loops over coins and calls candleSnapshot for each spends that budget faster than it refills, and once it is gone every request from that IP is refused until the window rolls.

The fix is not a sleep. It is knowing what each request costs, and then not paying for the same bars twice. This post covers the weight table from Hyperliquid's rate-limits page, the arithmetic for a typical scanner, and the cached batch route at /api/v1/hyperliquid/candles/batch that returns up to 25 coins in one call.

What Does Each Hyperliquid Info Request Cost?

These are the weights Hyperliquid publishes for its REST API (checked October 2026). The exchange shares one budget across all of them: 1,200 weight per minute per IP.

RequestWeightNotes
l2Book, allMids, clearinghouseState, orderStatus, spotClearinghouseState, exchangeStatus2The cheap reads
candleSnapshot20Plus an additional weight per 60 candles returned
fundingHistory, userFills, recentTrades, historicalOrders and similar list reads20Plus an additional weight per 20 items returned
userRole60The most expensive info request
Every other documented /info type20The default
Exchange actions (orders, cancels)1 + floor(batch_length / 40)An unbatched action is 1
Explorer requests40blockList adds 1 per block

Two things in that table surprise people. First, allMids and l2Book are ten times cheaper than a candle pull, so a price-only loop is fine and a candle loop is not. Second, the per-item surcharges mean a 1,000-bar candleSnapshot costs more than a 100-bar one, which matters for a backfill.

Actions also carry an address-based limit that info requests do not: one request per 1 USDC of cumulative volume, with a 10,000-request starting buffer, and one request every 10 seconds once that is exhausted. WebSockets have their own caps (10 connections, 1,000 subscriptions, 2,000 messages sent per minute). The docs do not describe the error body, but in practice the REST API answers an exhausted budget with HTTP 429.

How Fast Does a Candle Poller Burn 1,200 Weight?

Take a scanner that watches 25 perps on the 1h chart and refreshes every 30 seconds. Each refresh is 25 candleSnapshot calls at 20 weight, so 500 weight before the per-60-bar surcharge. Two refreshes a minute is 1,000 weight. Add a few metaAndAssetCtxs reads for funding and open interest at 20 each and the budget is gone. The third sweep in that minute fails.

Now the same scanner on 50 coins: a single sweep is 1,000 weight. You get one refresh a minute at best, and nothing else from that IP. The usual symptoms are:

The structural problem is that every bot re-downloads bars that have not changed. A 1h candle closes once an hour. Fetching the trailing 200 bars every 30 seconds re-sends 199 bars you already hold, and pays 20 weight each time for the privilege.

How Do I Get Candles for 25 Hyperliquid Coins in One Request?

GET /api/v1/hyperliquid/candles/batch returns the same Hyperliquid bars (they come from candleSnapshot) for up to 25 coins in one call, keyed by coin. The bars are cached on our side, so repeat pulls do not touch Hyperliquid at all and your IP's weight budget is untouched.

curl -H "X-API-Key: cdk_live_your_key" \
  "https://cryptodataapi.com/api/v1/hyperliquid/candles/batch?coins=BTC,ETH,SOL,HYPE,kPEPE&interval=1h&limit=200&closed_only=true"

A representative response (example values, trimmed):

{
  "interval": "1h",
  "limit": 200,
  "series": {
    "BTC": [{"timestamp": 1761800400000, "open": 70210.0, "high": 70480.0,
             "low": 70105.0, "close": 70390.0, "volume": 1834.2}, "..."],
    "ETH": ["..."], "SOL": ["..."], "HYPE": ["..."]
  },
  "count": 4,
  "pending": ["kPEPE"],
  "complete": false,
  "retry_after_s": 3,
  "closed_only": true,
  "atr_period": null,
  "forming_bar_timestamp": null
}

The contract is explicit, and worth reading once:

Coin names resolve case-insensitively against Hyperliquid's own names, so kPEPE works and so does kpepe. symbols is accepted as an alias for coins, and an atr=14 parameter adds a Wilder ATR to every bar.

Open Interest and Prices for Every Asset, One Call Each

The other two reads a scanner needs are the whole-universe ones, and both are a single request here.

GET /api/v1/hyperliquid/open-interest returns every listed Hyperliquid perp in one assets array. Each row carries coin, funding_rate, open_interest (base-coin units), open_interest_usd, mark_price, oracle_price, premium, day_ntl_vlm and prev_day_px. The envelope adds total_oi_usd, as_of (ms, when the snapshot was fetched from Hyperliquid), generated_at (ms, when the response was built) and coverage, which is {universe_total, delisted_excluded, rows}: Hyperliquid keeps delisted markets in its universe and this route drops them.

curl -H "X-API-Key: cdk_live_your_key" \
  "https://cryptodataapi.com/api/v1/hyperliquid/open-interest"

GET /api/v1/hyperliquid/prices is the allMids equivalent: a prices map of coin to mid, a count, and the same as_of / generated_at pair. The gap between the two timestamps is the payload's staleness, and both are null only on a cache row written before the field existed.

Because the rate for every perp is already on the open-interest route (assets[].funding_rate), a funding scan across the universe is one request, not one per coin.

Python: Retry Only What Is Pending

The loop below handles a cold cache correctly. It asks once, then re-requests only the coins named in pending after retry_after_s, and never re-downloads a series it already holds.

import time, httpx

BASE = "https://cryptodataapi.com/api/v1"
H = {"X-API-Key": "cdk_live_your_key", "Accept-Encoding": "gzip"}

def batch_candles(coins, interval="1h", limit=200):
    series, todo = {}, list(coins)
    while todo:
        r = httpx.get(f"{BASE}/hyperliquid/candles/batch", headers=H, timeout=40,
                      params={"coins": ",".join(todo), "interval": interval,
                              "limit": limit, "closed_only": "true", "wait": 10})
        r.raise_for_status()
        body = r.json()
        series.update(body["series"])
        todo = body["pending"]
        if todo:
            time.sleep(body["retry_after_s"] or 3)
    return series

bars = batch_candles(["BTC", "ETH", "SOL", "HYPE", "kPEPE"])
print({c: len(v) for c, v in bars.items()})

After the first fill, pull a small limit on each bar close and append. An 8-coin limit=80 batch is about 66 KB and returns in milliseconds from cache; a limit=800 batch of the same coins is roughly 670 KB uncompressed and 146 KB with gzip. Pull deep history once, then top up.

Direct Polling vs the Cached Batch Route

Direct candleSnapshot loop/hyperliquid/candles/batch
Requests for 25 coins251
Hyperliquid weight spent per sweep500 + surcharges, from your IP0 on a warm cache; a cold coin costs about 20 weight on our side, once
Partial failureSome coins 429, some succeed, you sort it outcomplete / pending / retry_after_s tell you exactly what to retry
Forming barAlways included, you strip itclosed_only=true removes it
4h bars for liquid perpsFetched on demandRefilled for the ~60 most liquid perps just after each 4h close, so the first read after a close is instant
Your own quotaHyperliquid's 1,200 weight/min per IPOur per-key limit: 10/min on Free, 30/min on Pro, 120/min on Pro Plus

The honest caveat is that a cold start still costs Hyperliquid weight somewhere. A first pull of many uncached coins takes about a minute of budget on our side, which is why the route answers with pending rather than holding your connection open. After that, repeat pulls are served from cache.

How to Use This in a Bot (and Where It Does Not Help)

Use the batch route for the scan: one call per bar close for your whole universe, closed_only=true, a trailing limit sized to your longest indicator. Use /hyperliquid/open-interest for funding and OI across every coin in the same tick, and /hyperliquid/prices for mids between bars. All three are available on a free key, which allows 10 requests a minute and 1,000 a day once the email is confirmed.

What it does not do:

Fees for sizing are on /hyperliquid/meta (fees.perps.taker is 0.00045), and funding plus OI aligned to the same bar grid is /backtesting/hl-funding-bars, joined on timestamp. The full parameter list is in the docs.

Frequently asked questions

What is the Hyperliquid API rate limit?

REST requests from one IP share an aggregated weight limit of 1,200 per minute. Most /info requests cost 20 weight, l2Book and allMids cost 2, userRole costs 60, and candleSnapshot adds weight per 60 candles returned.

How many Hyperliquid coins can I fetch candles for in one request?

Up to 25 with GET /api/v1/hyperliquid/candles/batch. The response has a series map keyed by coin, a pending list for coins still fetching on a cold cache, a complete flag and a retry_after_s hint.

What does retry_after_s mean on the candles batch endpoint?

It is how long to wait, 3 to 30 seconds, before re-requesting only the coins named in pending. It is null when nothing is pending. The background fetch keeps running, so the retry joins it rather than starting over.

Does closed_only=true change which bars I get?

Yes. It returns the last limit completed bars with no forming bar, and forming_bar_timestamp is null. Use it for strategies that act on a closed candle.

How do I read open interest for every Hyperliquid perp at once?

GET /api/v1/hyperliquid/open-interest returns every listed perp in one assets array with funding_rate, open_interest, open_interest_usd, mark_price and oracle_price, plus a coverage block that says how many delisted markets were excluded.