Rate limits and user limits
Hyperliquid meters each IP by request weight, 1200 per minute, and each address by actions: 10,000 plus one per USDC traded.
Hyperliquid meters you twice. Each IP spends request weight on the shared API servers, and each address spends actions it earns by trading. Info requests spend only the first budget; actions spend both.
Say your bot sends 79 orders in one action. The IP pays weight 2, 1 + floor(79 / 40), of its 1200 a minute. The address pays 79 actions from its starting 10,000, and earns 1 back per USDC that trades.
So a key that orders but never trades runs out, while a trading account earns room as it goes.
Each IP spends 1200 weight a minute
Section titled “Each IP spends 1200 weight a minute”Every REST request from an IP draws on this one pool:
| Request | Weight |
|---|---|
Any documented exchange action |
1 + floor(batch_length / 40); 79 orders in one action weigh 2 |
l2Book, allMids, clearinghouseState, orderStatus, spotClearinghouseState, exchangeStatus |
2 |
userRole |
60 |
Every other documented info request |
20 |
recentTrades, historicalOrders, userFills, userFillsByTime, fundingHistory, userFunding, nonUserFundingUpdates, twapHistory, userTwapSliceFills, userTwapSliceFillsByTime, delegatorHistory, delegatorRewards, validatorStats |
base weight, plus more for every 20 items returned |
candleSnapshot |
base weight, plus more for every 60 items returned |
Any explorer request |
40; blockList adds 1 per block, and older blocks not queried recently can weigh more |
That is 10 weight-2 requests a second, or one weight-20 request. WebSocket caps are in the numbers and on WebSocket; the public HyperEVM RPC at rpc.hyperliquid.xyz/evm allows 100 requests per minute.
Each address earns 1 action per USDC traded
Section titled “Each address earns 1 action per USDC traded”The count runs from the address’s creation, on top of an initial 10,000; sub-accounts count as separate users. Once spent, it may send one action every 10 seconds, but a larger cancel allowance still lets it pull its orders. A batch of n orders or cancels is 1 request for the IP limit and n for the address limit.
The docs’ example userRateLimit response:
{"cumVlm": "2854574.593578", "nRequestsUsed": 2890, "nRequestsCap": 2864574, "nRequestsSurplus": 0}nRequestsCap is 10,000 plus the 2,854,574 USDC traded. nRequestsUsed is actions used minus any reserved, and nRequestsSurplus is reserved minus used, both floored at zero.
Open-order, congestion and daily caps are in the numbers. During congestion, the docs advise against resending cancels whose results have already returned: a duplicate still uses block space, which the congestion share measures.
Even a rejection costs: an action refused for a passed expiresAfter still costs 5 times the usual address budget.
Buying actions instead of trading for them
Section titled “Buying actions instead of trading for them”An address can buy capacity with reserveRequestWeight:
{"type": "reserveRequestWeight", "weight": 100000}- Paid from the perps balance: 100,000 extra actions cost 50 USDC.
- Optional
destinationpays for another address, which must already exist. Omit the key when unused: it is skipped in hashing if unset. - The reserve shows up as
nRequestsSurplus.
The numbers
Section titled “The numbers”| Limit | Value |
|---|---|
| REST weight per IP | 1200 per minute |
| Exchange action weight | 1 + floor(batch_length / 40) |
| Info weight | 2, 20 or 60, plus per-item weight on history requests |
| Explorer weight | 40 |
| WebSocket connections per IP | 10; 30 new per minute |
| WebSocket subscriptions per IP | 1000; 10 unique users |
| WebSocket messages sent per IP | 2000 per minute |
| WebSocket posts in flight per IP | 100 |
| HyperEVM public RPC | 100 requests per minute |
| Address budget | 10,000 + 1 per USDC traded |
| When address-limited | 1 action per 10 s |
| Cancel budget | min(limit + 100000, limit * 2) |
| Open orders per user | 1000 + 1 per 5M USDC volume, at most 5000; from 1000 open, new reduce-only and trigger orders are rejected |
| Block space under high congestion | at most 2 x the previous day’s maker share, computed per UTC date and weighted like fee-tier volume (HIP-3 assets in growth mode count for less) |
| Unified and portfolio margin | 50k user actions per day, regardless of volume; standard accounts have no such cap (account modes) |
reserveRequestWeight |
0.0005 USDC per request |
Stale expiresAfter rejection |
costs 5 x the usual address budget |
Quoting needs a 1 per cent fill rate
Section titled “Quoting needs a 1 per cent fill rate”A 100 USDC quote costs one action and earns one per USDC filled, so break-even is a 1 per cent fill rate. Heavy cancel-and-replace spends the 10,000 starting actions, then must trade or buy more.
The docs recommend standard mode for market makers and high-volume bots, because of the 50k daily cap on unified and portfolio margin.
Keeping reads from blocking orders
Section titled “Keeping reads from blocking orders”The costly failure: reads spend the IP budget, and your order can’t be sent. One production bot’s order preparation called /info first, polling hit the IP limit and order placement stopped. The fix:
- Market data on WebSocket: REST traffic fell to about 0.15 requests per second.
- One limiter per IP that budgets weight, not request count, with a slice reserved for
/exchange. - On a 429, pause every caller at once, with exponential backoff and jitter. No
Retry-Afterheader is sent. - Order placement never waits on an info request: cache what it needs (optimising latency).
// Weight-based limiter for one IP. The reserve and backoff values are design choices, not protocol values.const WEIGHT_PER_MINUTE = 1200;const EXCHANGE_RESERVE = 200;let tokens = WEIGHT_PER_MINUTE;let cooldownUntil = 0;let backoffMs = 1_000;
setInterval(() => { tokens = Math.min(WEIGHT_PER_MINUTE, tokens + WEIGHT_PER_MINUTE / 60);}, 1_000);
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
export async function limited(weight: number, isExchange: boolean, send: () => Promise<Response>): Promise<Response> { const floor = isExchange ? 0 : EXCHANGE_RESERVE; while (Date.now() < cooldownUntil || tokens - weight < floor) await sleep(50); tokens -= weight; const res = await send(); if (res.status === 429) { cooldownUntil = Date.now() + backoffMs + Math.random() * backoffMs; backoffMs = Math.min(backoffMs * 2, 60_000); } else { backoffMs = 1_000; } return res;}For the address budget, poll userRateLimit and top up with reserveRequestWeight before nRequestsUsed reaches nRequestsCap. Batching saves IP weight (up to 39 orders weigh the same as one, by 1 + floor(n / 40)) but costs the address budget the same as single orders.
For reading without spending REST weight, see WebSocket. For what a limit looks like when you hit it, see Error responses.